拦截RecyclerView在NestedScrollView中向下滑动

Intercepting RecyclerView downwards fling in NestedScrollView

我有以下布局:

    <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>

当从 fixedBannercard1 向下滚动(手指向上)时,appBarLayout 首先折叠,然后 nestedScrollView 向下滚动。但是,如果从 recyclerView 向下滚动,recyclerView 开始滚动。我希望 nestedScrollViewrecyclerView 之前先向下滚动。

我已经尝试使用在 nestedScrollView 上设置的自定义 CardViewBehavior,它会覆盖 onNestedPreScroll 以在 appBarLayout 未完全折叠且仍有滚动范围时使用滚动增量 nestedScrollView.

但是,如果我在 recyclerView 上滑动得足够快,recyclerViewnestedScrollView 完全滚动到底部之前就开始快速滑动。我尝试在 CardViewBehavior 中重写 onNestedPreFlingonNestedFling,但当 RecyclerView 开始自行投掷时,这两个方法似乎从未被调用过。

如何确保 nestedScrollViewrecyclerView 开始滚动之前滚动到底部?

扩展 NestedScrollView 并直接覆盖 onNestedPreScroll 方法。

(在 https://www.androiddesignpatterns.com/2018/01/experimenting-with-nested-scrolling.html 上找到了解决方案)

谢谢@Henry,我已经成功地重新创建了您在 kotlin 中@AlexLockwood 提到的 the article 的行为。我已经处理这个问题两天了,其他答案通常建议使用 WRAP_CONTENT 属性并且基本上打败了 RecyclerView 的目的,因为 View 不再被回收。我提供下面的代码以防将来对某人有所帮助。

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

然后我们可以使用NestedScrollLayout如下:

<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>