ReycyclerView:setLayoutFrozen/isLayoutFrozen

f:id:shaunkawano:20190414191718p:plain

setLayoutFrozen/isLayoutFrozenという(面白い名前の)APIを見つけたのでメモ程度に紹介です。

業務でCoordinatorLayout + RecyclerViewを用いた画面を実装中に見つけました。画面起動をすると、RecyclerViewが意図せず微妙にスクロールしてしまうという問題があり、原因などが掴みきれず、どのように修正するか、暫定対応含めいろいろ方法そ漁っていたときに見つけたプロパティです。(根本原因はまだ分かっていないです。もし知っている方などいたら教えていただけると嬉しいです🙏)

※そもそも自分の場合のような、意図しないスクロールが走るからといってレイアウトを凍結させるようなことをするのは本質的ではないのですが、ユーザーのタッチイベントによるレイアウト処理やスクロール処理をどうしても制限したい場合があれば、もしかしたらこのAPIが使えるかもしれません。

なお、この記事で参照されているRecyclerViewのソースコードのバージョンはandroidx.recyclerview:recyclerview:1.0.0です。

mLayoutFrozen

RecyclerViewクラスの内部には、

boolean mLayoutFrozen;

上記のようにフィールドが定義されており、このmLayoutFrozenのtrue/falseを切り替えるのがsetLayoutFrozen APIです。mLayoutFrozenはRecyclerViewのpublicなAPIや内部ロジックなど、様々な箇所で参照されています。たとえば、RecyclerViewには特定の位置へとスクロールするためのscrollToPositionというAPIがありますが、この関数の内部でも、mLayoutFrozenが参照されています。

public void scrollToPosition(int position) {
  if (mLayoutFrozen) {
    return;
  }
  ... 
}

上記では、mLayotuFrozenの値がtrueの場合はスクロール処理など何もせずにreturnするというガード句が記述されています。このように、setLayoutFrozenのAPIを用いてこのプロパティの値を切り替えることで、RecyclerView内部で本来発生するレイアウト処理やスクロール処理を制御できます。

setLayoutFrozen(true / false)

名前の通りレイアウトを凍結させるかどうかを指定するAPIです。trueで凍結しfalseで凍結を解除します。凍結すると、RecyclerViewのレイアウト処理やスクロールを解凍するまで発生させないように設定できます。

この関数はRecyclerViewがレイアウトを更新している最中やスクロールの最中には呼び出しできません。setLayoutFrozen関数の内部では、まずRecyclerViewが現在レイアウト処理またはスクロール処理を行っている最中かどうかを判定し、もし何らかの処理を行っている最中であればIllegalStateExceptionが投げられるような実装になっています。ちゃんとした理由はわかっていませんが、きっと描画処理の途中で動きを止めてしまうことで予期せぬ描画だったり挙動が発生しやすくなるからではないかと予想しています。

もしレイアウトを解凍する場合、

  • mLayoutFrozenの値をfalseにしrequestLayoutを必要に応じて呼び出します。

もしレイアウトを凍結する場合、

  • 空のタッチイベントをonTouchEventに送り、
  • mLayoutFrozenの値をtrueにし、
  • スクロール処理を終了させています。

以下、該当するsetLayoutFrozenのコードです。

public void setLayoutFrozen(boolean frozen) {
  if (frozen != mLayoutFrozen) {
    assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
      if (!frozen) {
        mLayoutFrozen = false;
          if (mLayoutWasDefered && mLayout != null && mAdapter != null) {
            requestLayout();
          }
            mLayoutWasDefered = false;
          } else {
            final long now = SystemClock.uptimeMillis();
            MotionEvent cancelEvent = MotionEvent.obtain(
                now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            onTouchEvent(cancelEvent);
            mLayoutFrozen = true;
            mIgnoreMotionEventTillDown = true;
            stopScroll();
          }
      }
  }
}

isLayoutFrozen

isLayoutFrozenは純粋にmLayoutFrozenフィールドを返すだけです。

public boolean isLayoutFrozen() {
  return mLayoutFrozen;
}

ちなみに

自分の困っていた勝手にスクロールしちゃう問題は、周りのAndoridメンバーのアイディアでRecyclerViewのdescendantFocusabilityFOCUS_BLOCK_DESCENDANTSに設定することで不要なフォーカスが当たらないようにして解消しました。(satoshun氏ありがとうございます🙏)

setLayoutFrozenを用いるよりも健全な対応策かと思います。

https://developer.android.com/reference/android/view/ViewGroup#setDescendantFocusability(int)

まとめ

  • RecyclerViewにはsetLayoutFrozenというAPIがあり、これによりRecyclerViewを「凍結」し、RecyclerViewのレイアウト処理やスクロールを「解凍」するまで発生させないように設定できる。
  • setLayoutFrozenはレイアウト処理やスクロール処理を行っている最中の呼び出しはできないように制限されている。
  • RecyclerView内部ではmLayoutFrozenというフィールドが定義されておりRecyclerView内部の様々な場所から参照されている

知らないAPIに出くわしたときはどんな形でもいいのでメモにして忘れないようにしていきたい。。

以上です!