言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

RecyclerView in RecyclerViewだとAppBarLayoutがスクロールしないことがある

Nested Scroll(RecyclerView in RecyclerView)とAppBarの組み合わせでうまくいかないことがあったのでメモします。

前提

RecyclerViewが入れ子になってNested Scrollする画面で、

  • Parent RecyclerViewはvertical scroll
  • Child RecyclerViewはhorizontal scroll

といった挙動をします。

ParentのRecyclerViewCoordinatorLayoutを親に持っていて、AppBarLayoutがParentのRecyclerViewのスクロールに応じてアニメーションするといった挙動を想定しています。

問題

ParentのRecyclerViewをスクロールさせてもAppBarLayoutは変化しません。 (水色のViewがAppBarLayoutに包まれています。)

f:id:muumuumuumuu:20181204090257g:plain:w200

(同じParentにスクロールしないchildを入れてやり、その上からスクロールを開始するとAppBarはちゃんと正常にアニメーションします。)

直し方

Child RecyclerViewNestedScrollingEnabled() にfalseを設定してやればうまくいきます。

コードからだとこんな感じ

childRecyclerView.isNestedScrollingEnabled = false

xmlからも指定できます

<android.support.v7.widget.RecyclerView 
    ...(省略)...
    android:nestedScrollingEnabled="false"

上記のようにChild `RecyclerViewがnested scrollしないことを明示的に指定するだけでAppBarがアニメーションするようになります。

f:id:muumuumuumuu:20181204091114g:plain:w200

何が起こったか?

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);
    }