RecyclerView in RecyclerViewだとAppBarLayoutがスクロールしないことがある
Nested Scroll(RecyclerView in RecyclerView)とAppBarの組み合わせでうまくいかないことがあったのでメモします。
前提
RecyclerViewが入れ子になってNested Scrollする画面で、
- Parent
RecyclerView
はvertical scroll - Child
RecyclerView
はhorizontal scroll
といった挙動をします。
ParentのRecyclerView
は CoordinatorLayout
を親に持っていて、AppBarLayout
がParentのRecyclerView
のスクロールに応じてアニメーションするといった挙動を想定しています。
問題
ParentのRecyclerView
をスクロールさせてもAppBarLayoutは変化しません。
(水色のViewがAppBarLayoutに包まれています。)
(同じParentにスクロールしないchildを入れてやり、その上からスクロールを開始するとAppBarはちゃんと正常にアニメーションします。)
直し方
Child RecyclerView
の NestedScrollingEnabled()
にfalseを設定してやればうまくいきます。
コードからだとこんな感じ
childRecyclerView.isNestedScrollingEnabled = false
xmlからも指定できます
<android.support.v7.widget.RecyclerView ...(省略)... android:nestedScrollingEnabled="false"
上記のようにChild `RecyclerViewがnested scrollしないことを明示的に指定するだけでAppBarがアニメーションするようになります。
何が起こったか?
setNestedScrollingEnabled()
を設定することによってどこが変わるかというと、NestedScrollingChildHelper#dispatchNestedPreScroll()
がfalseを返すようになります。
RecyclerViewのdispatchNestedPreScroll()
を呼んだ先でNestedScrollingChildHelper#dispatchNestedPreScroll()
-> ViewParentCompat.onNestedPreScroll()
-> CoordinatorLayout#onNestedPreScroll()
-> AppBarLayout.BaseBehavior#onNestedPreScroll()
と伝播していきます。
Scrollしない時はAppBarLayoutの onNestedPreScroll()
のdyの値が正常に渡って来ません。
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) { if (this.isNestedScrollingEnabled()) { ViewParentCompat.onNestedPreScroll(parent, this.mView, dx, dy, consumed, type); } return false; }
ここでisNestedScrollingEnabled()
がfalseを返さないと本来子のRecyclerViewはVerticalなscrollをしないはずなのにonNestedPreScroll()
のdyの値が変なまま伝播していってしまってうまくAppBarLayoutがアニメーションできないのかな?と予想しています。(RecyclerViewが巨大すぎて細かい部分まで追えていないのでもし違っていたらコメントで指摘してもらえると助かります。)
AppBarLayout.BaseBehavior#onNestedPreScroll()
でscroll()
が呼ばれます。
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dx, int dy, int[] consumed, int type) { if (dy != 0) { int min; int max; if (dy < 0) { min = -child.getTotalScrollRange(); max = min + child.getDownNestedPreScrollRange(); } else { min = -child.getUpNestedPreScrollRange(); max = 0; } if (min != max) { consumed[1] = this.scroll(coordinatorLayout, child, dy, min, max); this.stopNestedScrollIfNeeded(dy, child, target, type); } } }
この先HeaderBehavior#scroll()
がsetHeaderTopBottomOffset()
などを呼んで最終的にView#offsetTopAndBottom()
が呼ばれます。
ここで不正に渡って来たdyの値によって、スクロールしていなかったことになっているんじゃないかなぁ。
ちなみにsetNestedScrollingEnabled()
はデフォルトでtrueを返すの?と思った人がいるかもしれませんが、このフラグはRecyclerViewのコンストラクタでtrueが設定されます。
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { boolean nestedScrollingEnabled = true; // defaultでtrue if (attrs != null) { if (VERSION.SDK_INT >= 21) { a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, defStyle, defStyleRes); nestedScrollingEnabled = a.getBoolean(0, true); // xmlで指定されていれば反映する a.recycle(); } this.setNestedScrollingEnabled(nestedScrollingEnabled); }