RecyclerViewに表示しきれなかったアイテムはスクロールを行うことで表示されるようになっています。
スクロールは「外部入力(指でスクリーン上をタッチしてスライド)によるスクロール」の他に、「プログラムによるスクロール」をすることも出来ます。
今回はこのアイテムのスクロールについてまとめてみました。
目次
外部入力によるスクロール
外部入力とは、指でスクリーン上をタッチしてスライドしたり、マウスなどのポインティングデバイスでスクリーン上をドラッグしてスライドしたり、何かしらの外部装置を使った操作のことです。
携帯端末はタッチスクリーンの装備が標準なので、前者の操作が主流でしょう。
指でタッチしてスライド
図は指を使ってスクロールする様子です。
勢いよくスワイプ後にスクリーンから指を離しても、スクロールはしばらく継続します。
末尾(または先頭)のアイテムに到達すると、EdgeEffectのアニメーションが表示されて、それ以上のスクロールが出来ないことを知らせます。
スクロールの状態を取得(OnScrollListener)
アイテムのスクロールが行われている時に、登録されたリスナー(OnScrollListener)へコールバックが返り、スクロールの状態が通知されます。
スクロールの状態の意味はサンプル中のコメントに書いた通りです。
... val _listener = object : RecyclerView.OnScrollListener(){ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { Log.i("OnScrollListener", "onScrollStateChanged newState = %s" .format(getStringOfScrollState(newState))) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { Log.i("OnScrollListener", "onScrolled dx=%3d dy=%3d" .format(dx, dy)) } } rcySample.addOnScrollListener(_listener) ...
// SCROLL_STATE_IDLE // スクロールしていない // SCROLL_STATE_DRAGGING // タッチ入力などの外部入力によってドラッグされている // SCROLL_STATE_SETTLING // 外部からの制御下にはないが、最終的なポジションへスクロールしている private fun getStringOfScrollState(state: Int): String { return when(state) { RecyclerView.SCROLL_STATE_IDLE -> "0:IDLE(スクロールしていない状態)" RecyclerView.SCROLL_STATE_DRAGGING -> "1:DRAGGING(ドラッグされている状態)" RecyclerView.SCROLL_STATE_SETTLING -> "2:SETTLING(スクロールしている状態)" else -> "UNKNOWN" } }
...: onScrollStateChanged newState = 1:DRAGGING(ドラッグされている状態) ...: onScrolled dx= 0 dy= 1 ...: onScrolled dx= 0 dy= 34 ...: onScrolled dx= 0 dy= 81 ...: onScrolled dx= 0 dy=144 ...: onScrolled dx= 0 dy= 41 ...: onScrollStateChanged newState = 2:SETTLING(スクロールしている状態) ...: onScrolled dx= 0 dy= 88 ...: onScrolled dx= 0 dy= 51 ...: onScrollStateChanged newState = 0:IDLE(スクロールしていない状態)
プログラムによるスクロール
スクロールを行う3つのメソッドが用意されています。
RecyclerView#scrollToPosition(ポジションへスクロール)
指定されたポジションへ移動します。
メソッド名に「Scroll」と付いていますがスクロールしません。指定されたポジションへ「Jump」と言った方が妥当な動作です。
使用方法: scrollToPosition(int position) position 移動先のポジション(0 ~ アイテム数-1)
【後方へ移動】
rcySample.scrollToPosition(8)
【前方へ移動】
rcySample.scrollToPosition(2)
スクロールしないので、OnScrollListenerによるスクロールの状態は通知されません。
RecyclerView#smoothScrollToPosition(ポジションへスクロール)
指定されたポジションへスクロールします。
使用方法: smoothScrollToPosition(int position) position 移動先のポジション(0 ~ アイテム数-1)
【後方へ移動】
rcySample.smoothScrollToPosition(32)
rcySample.smoothScrollToPosition(2)
ドラッグしないので、OnScrollListenerによるスクロールの状態はSETTLINGとIDLEの2つが通知されます。
RecyclerView#smoothScrollBy(指定した移動量をスクロール)
指定した移動量(ピクセル値)をスクロールします。
使用方法1: smoothScrollBy( int dx, int dy, Interpolator interpolator, int duration) dx x方向の移動量[px] +:後方/-:前方、VERTICALの時は0を指定 dy y方向の移動量[px] +:後方/-:前方、HORIZONTALの時は0を指定 interpolator アニメーションのタイプ nullの時はデフォルトのInterpolator duration アニメーションの時間[ms] duration > 0の時、dulationをそのまま使用 UNDEFINED_DURATIONの時、自動計算 上記以外の時、スクロール無し 使用方法2: smoothScrollBy(int dx, int dy, Interpolator interpolator) -> smoothScrollBy(dx, dy, interpolator, UNDEFINED_DURATION)と同じ 使用方法3: smoothScrollBy(int dx, int dy) -> smoothScrollBy(dx, dy, null)と同じ ※デフォルトのInterpolator 均等に減速していくような変化のアニメーションになる ※durationの自動計算 スクロールが目で追える速度のアニメーションになる
【後方へ移動】
rcySample.smoothScrollBy(0, 1220, LinearInterpolator(), 700)
rcySample.smoothScrollBy(0, -1220, LinearInterpolator(), 700)
ドラッグしないので、OnScrollListenerによるスクロールの状態はSETTLINGとIDLEの2つが通知されます。
スクロール後の表示ポジションを検出
アイテム数が多くなると、スクロールを繰り返すうちに、全体に対する表示されているアイテムのポジションが分からなくなることがあります。
そのような時のために、アイテムの位置関係をユーザに知らせることができたら便利です。
表示されているアイテムのポジションは次のようにして検出できます。
... val _listener = object : RecyclerView.OnScrollListener(){ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { ... } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val _manager = recyclerView.layoutManager as LinearLayoutManager val _items = "%d - %d / %d".format( _manager.findFirstVisibleItemPosition(), _manager.findLastVisibleItemPosition(), _manager.itemCount ) txtItems.setText(_items) // TextViewへアイテムの位置関係を表示 } } rcySample.addOnScrollListener(_listener) ...
Complete付きのメソッドは完全に表示されているアイテムのポジションになります。
スクロールが先頭・末尾に到達したことを検出
スクロールによりアイテムが末尾または先頭に到達すると、EdgeEffectアニメーションが発動して、それ以上のアイテムが存在しないことをユーザへ知らせてくれます。
このタイミングでプログラムを動かしたい場合があります。
例えば、続きのアイテムデータを読み込む場合などです。
先頭・末尾に到達したことは次のようにして検出できます。
使用方法: View#canScrollVertically(int direction) 縦スクロール View#canScrollHorizontally(int direction) 横スクロール direction direction > 0の場合 true:後方へスクロール可能、false:不可 direction ≦ 0の場合 true:前方へスクロール可能、false:不可
... val _listener = object : RecyclerView.OnScrollListener(){ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { ... } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if(! recyclerView.canScrollVertically(-1)) Toast.makeText(this@MainActivity, "先頭のアイテム!", Toast.LENGTH_LONG).show() if(! recyclerView.canScrollVertically(1)) Toast.makeText(this@MainActivity, "末尾のアイテム!", Toast.LENGTH_LONG).show() } } rcySample.addOnScrollListener(_listener) ...
検出で使用したcanScrollVerticallyメソッドの中身は次のようになっています。
... public boolean canScrollVertically(int direction) { final int offset = computeVerticalScrollOffset(); final int range = computeVerticalScrollRange() - computeVerticalScrollExtent(); if (range == 0) return false; if (direction < 0) { return offset > 0; } else { return offset < range - 1; } } ...
引数のdirectionは名前が示す通り、+-でスクロールの向きを示しているに過ぎません。int型ですが、大きさは意味を持たないので注意が必要です。
関連記事: