Search code examples
androidandroid-recyclerviewandroid-coordinatorlayoutandroid-nestedscrollview

Intercepting RecyclerView downwards fling in NestedScrollView


I have the following layout:

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/rootLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:id="@+id/appBarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <android.support.design.widget.CollapsingToolbarLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

                <ImageView
                    android:layout_width="match_parent"
                    android:layout_height="160dp"
                    app:layout_collapseMode="parallax" />

                <android.support.v7.widget.Toolbar
                    android:layout_width="match_parent"
                    android:layout_height="52dp"
                    app:layout_collapseMode="pin" />
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.widget.NestedScrollView
            android:id="@+id/nestedScrollView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true"
            app:layout_behavior="com.example.CardViewBehavior">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <FrameLayout
                    android:id="@+id/fixedBanner"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                </FrameLayout>

                <FrameLayout
                    android:id="@+id/card1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <!-- random content -->
                </FrameLayout>

                <FrameLayout
                    android:id="@+id/card2"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                    <android.support.v7.widget.RecyclerView
                        android:id="@+id/recyclerView"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent" />
                </FrameLayout>
            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>
</layout>

When scrolling down (finger goes upwards) from fixedBanner or card1, the appBarLayout collapses first, then nestedScrollView scrolls down. However, if scrolling down from recyclerView, recyclerView starts scrolling. I want nestedScrollView to scroll down first before recyclerView.

I have tried using a custom CardViewBehavior set on nestedScrollView which overrides onNestedPreScroll to consume scroll deltas if appBarLayout is not fully collapsed and there is still range to scroll in nestedScrollView.

However, if I swipe fast enough on recyclerView, recyclerView starts flinging before nestedScrollView has fully scrolled to bottom. I tried overriding onNestedPreFling and onNestedFling in CardViewBehavior, but it seems those two methods were never called when RecyclerView starts flinging by itself.

How can I ensure that nestedScrollView scrolls to bottom before recyclerView starts scrolling?


Solution

  • Thanks @Henry, I have successfully recreate the behaviour from the article you mentioned by @AlexLockwood in kotlin. I've dealing with this problem for two days, the other answers usually suggest using WRAP_CONTENT attribute and basically defeat the purpose of RecyclerView as the View is not being recycled anymore. I'm providing the code below just in case this may help someone in the future.

    class NestedScrollLayout(
        context: Context,
        attrs: AttributeSet?
    ) : NestedScrollView(context, attrs),
        NestedScrollingParent3 {
    
        private var parentHelper = NestedScrollingParentHelper(this)
    
        override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
            return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
        }
    
        override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
            parentHelper.onNestedScrollAccepted(child, target, axes)
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type)
        }
    
        override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
            if(target is RecyclerView) {
                if ((dy < 0 && isRvScrolledToTop(target)) || (dy > 0 && !isNsvScrolledToBottom(this))) {
                    scrollBy(0, dy)
                    consumed[1] = dy
                    return
                }
            }
            dispatchNestedPreScroll(dx, dy, consumed, null, type)
        }
    
        override fun onNestedScroll(
            target: View,
            dxConsumed: Int,
            dyConsumed: Int,
            dxUnconsumed: Int,
            dyUnconsumed: Int,
            type: Int
        ) {
            val oldScrollY = scrollY
            scrollBy(0, dyUnconsumed)
            val mConsumed = scrollY - oldScrollY
            val mUnconsumed = dyUnconsumed - mConsumed
            dispatchNestedScroll(0, mConsumed, 0, mUnconsumed, null, type)
        }
    
        override fun onStopNestedScroll(target: View, type: Int) {
            parentHelper.onStopNestedScroll(target, type)
            stopNestedScroll(type)
        }
    
        override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
            Log.println(Log.ASSERT, "NestedScrollLayout:onStartNestedScroll", "Requested")
            return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH)
        }
    
        override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
            onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH)
        }
    
        override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
            onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH)
        }
    
        override fun onNestedScroll(
            target: View,
            dxConsumed: Int,
            dyConsumed: Int,
            dxUnconsumed: Int,
            dyUnconsumed: Int
        ) {
            onNestedScroll(
                target,
                dxConsumed,
                dyConsumed,
                dxUnconsumed,
                dyUnconsumed,
                ViewCompat.TYPE_TOUCH
            )
        }
    
        override fun onStopNestedScroll(target: View) {
            onStopNestedScroll(target, ViewCompat.TYPE_TOUCH)
        }
    
        override fun getNestedScrollAxes(): Int {
            return parentHelper.nestedScrollAxes
        }
    
        companion object {
            private fun isNsvScrolledToBottom(nsv: NestedScrollView): Boolean {
                return !nsv.canScrollVertically(1)
            }
    
            private fun isRvScrolledToTop(rv: RecyclerView): Boolean {
                rv.layoutManager?.let { lm ->
                    return when (lm) {
                        is LinearLayoutManager -> {
                            lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPosition() == 0
                        }
                        is GridLayoutManager -> {
                            lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPosition() == 0
                        }
                        is StaggeredGridLayoutManager -> {
                            lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPositions(
                                intArrayOf(0)
                            )[0] == 0
                        }
                        else -> lm.findViewByPosition(0)?.top == 0
                    }
                }
                return false
            }
        }
    }
    

    Then we can use NestedScrollLayout as follows:

    <com.organization.appname.NestedScrollLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        ...
    
        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="400dp" />
    
        ...
    
    </com.organization.appname.NestedScrollLayout>