【问题标题】:Vertical RecyclerView nested inside vertical RecyclerView垂直 RecyclerView 嵌套在垂直 RecyclerView 内
【发布时间】:2021-09-12 23:12:30
【问题描述】:

我已经花了几个小时/几天的时间阅读这个主题,但仍然找不到有用的东西。我正在尝试将固定高度的垂直滚动RecyclerView 放在另一个垂直滚动RecyclerView 的行中。

大部分建议是“将垂直滚动的RecyclerView 放在另一个垂直滚动的RecyclerView 中是犯罪行为”......但我不明白为什么这会如此糟糕。

事实上,这种行为与 StackOverflow 上的许多页面几乎完全相同(例如this one...确实是这个问题,至少在移动设备上查看时),其中代码部分具有固定(或最大)高度,并且垂直滚动,并且包含在本身垂直滚动的页面中。发生的情况是,当焦点位于代码部分时,滚动发生在该部分内,当它到达该部分滚动范围的上/下端时,滚动发生在外部页面内。这是很自然的,不是邪恶的。

这是我的recycler_view_row_outer.xml(外层RecyclerView内的一行):

<com.google.android.material.card.MaterialCardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    style="@style/MyCardView"
    app:cardElevation="4dp"
    app:strokeColor="?attr/myCardBorderColor"
    app:strokeWidth="0.7dp"
    card_view:cardCornerRadius="8dp" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <TextView
            style="@style/MyTextView.Section"
            android:id="@+id/list_title" />

        <LinearLayout
            style="@style/MyLinearLayoutContainer"
            android:id="@+id/list_container"
            android:layout_below="@+id/list_title" >
        </LinearLayout>

    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

这是我的recycler_view_row_inner.xml(内部RecyclerView内的一行):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/my_constraint_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@+id/list_container" >

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view_inner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbarStyle="outsideOverlay"
        android:scrollbars="vertical"
        app:layout_constrainedHeight="true"
        app:layout_constraintBottom_toBottomOf="@+id/my_constraint_layout"
        app:layout_constraintEnd_toEndOf="@+id/my_constraint_layout"
        app:layout_constraintHeight_max="750dp"
        app:layout_constraintHeight_min="0dp"
        app:layout_constraintStart_toStartOf="@+id/my_constraint_layout"
        app:layout_constraintTop_toTopOf="@+id/my_constraint_layout" >
    </androidx.recyclerview.widget.RecyclerView>

</androidx.constraintlayout.widget.ConstraintLayout>

使用上面的内部布局,我尝试按照this post 中提出的方法,创建一个具有固定/最大高度的内部RecyclerView...但它不起作用。

我膨胀内部布局(添加到外部布局中的list_container/containerView)并如下设置我的内部recyclerView

View inflatedView = getLayoutInflater().inflate(R.layout.recycler_view_row_inner, containerView, false);
RecyclerView recyclerView = inflatedView.findViewById(R.id.recycler_view_inner);
// set adapter, row data, etc

但这所做的只是创建一个固定高度的内行,该行不会在外行内滚动...内行的溢出内容刚刚被切断,我无法到达它的下半部分,因为它只是滚动外行。

知道如何进行这项工作吗?

【问题讨论】:

  • 您是否评估了使用嵌套滚动视图和回收站视图的解决方案?理想情况下,这应该处理开箱即用的嵌套滚动行为。

标签: android android-recyclerview


【解决方案1】:

方法一:使用两个嵌套的 RecyclerViews

Github sample

优点:

  • 视图被回收(即良好的性能)
  • 半无缝滚动(更新 3 和 4 后)

缺点:

  • 当到达内部远端项目时,在从内部滚动到外部滚动的过渡期间以编程方式传播的滚动不像手势那样平滑/自然。
  • 复杂的代码。

好吧,我不会解决垂直嵌套RecyclerViews 的性能问题;但请注意:

  • 内部RecyclerView可能失去了回收视图的能力;因为显示的外部 recyclerView 行应该完全加载它们的项目。 (谢天谢地,根据以下更新 1,这不是一个正确的假设)
  • 我在 ViewHolder 而不是 onBindViewHolder 中声明了一个适配器实例,通过在每次回收视图时不为内部 RecyclerView 创建新的适配器实例来获得更好的性能。

演示应用程序将一年中的月份表示为外部RecyclerView,将每个月的天数表示为内部RecyclerView

外部 RecyclerView 注册 OnScrollListener 每次滚动时,我们都会对内部 RV 进行检查:

  • 如果外部向上滚动:检查内部第一项是否显示。
  • 如果外部向下滚动:检查内部最后一项是否显示。
    outerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            if (dy > 0) //scrolled to BOTTOM
                outerAdapter.isOuterScrollingDown(true, dy);
            else if (dy < 0) //scrolled to TOP
                outerAdapter.isOuterScrollingDown(false, dy);
        }
    });

在外部适配器中:

    public void isOuterScrollingDown(boolean scrollDown, int value) {
        if (scrollDown) {
            boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
            if (!isLastItemShown) onScrollListener.onScroll(-value);
            enableOuterScroll(isLastItemShown);

        } else {
            boolean isFirstItemShown = currentFirstItem == 1;
            if (!isFirstItemShown) onScrollListener.onScroll(-value);
            enableOuterScroll(isFirstItemShown);
        }
        if (currentRV != null)
            currentRV.smoothScrollBy(0, 10 * value);
    }

如果相关项目未显示,那么我们决定禁用外部 RV 滚动。这由具有回调的侦听器处理,该回调接受传递给自定义 LinearLayoutManager 类的布尔值到外部 RV。

同样为了重新启用外部 RV 的滚动:内部 RecyclerView 注册 OnScrollListener 以检查是否显示内部第一个/最后一个项目。

innerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {

        if (!recyclerView.canScrollVertically(1) // Is it not possible to scroll more to bottom (i.e. Last item shown)
                && newState == RecyclerView.SCROLL_STATE_IDLE) {
            enableOuterScroll(true);

        } else if (!recyclerView.canScrollVertically(-1) // Is it possible to scroll more to top (i.e. First item shown)
                && newState == RecyclerView.SCROLL_STATE_IDLE) {
            enableOuterScroll(true);
        }
    }
});

仍然存在由于禁用/启用滚动而出现的故障;在下一次滚动之前,我们不能将滚动顺序传递给另一个 RV。这是通过反转初始外部 RV 滚动值来稍微操纵的;并使用currentRV.smoothScrollBy(0, 10 * initialScroll) 向内部使用任意滚动值。我希望有人可以提出任何其他替代方案。

更新 1

  • 内部RecyclerView可能失去了回收视图的能力;因为显示的外部 recyclerView 行应该完全加载它们的项目。

谢天谢地,这不是正确的假设,视图是通过使用 List 跟踪内部适配器中的回收项目列表来回收的,其中包含当前加载的项目:

假设某个月有 1000 天“2 月,因为它总是被压迫:)”,然后向上/向下滚动以注意加载的列表并确保 onViewRecycled() 被调用。

public class InnerRecyclerAdapter extends RecyclerView.Adapter<InnerRecyclerAdapter.InnerViewHolder> {

    private final ArrayList<Integer> currentLoadedPositions = new ArrayList<>();

    @Override
    public void onBindViewHolder(@NonNull InnerViewHolder holder, int position) {
        holder.tvDay.setText(String.valueOf(position + 1));
        currentLoadedPositions.add(position);
        Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
    }

    @Override
    public void onViewRecycled(@NonNull InnerViewHolder holder) {
        super.onViewRecycled(holder);
        currentLoadedPositions.remove(Integer.valueOf(holder.getAdapterPosition()));
        Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
    }
    
    // Rest of code is trimmed

}

日志:

onViewRecycled: 1000 [0]
onViewRecycled: 1000 [0, 1]
onViewRecycled: 1000 [0, 1, 2]
onViewRecycled: 1000 [0, 1, 2, 3]
onViewRecycled: 1000 [0, 1, 2, 3, 4]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

更新 2

仍然存在故障,因为禁用/启用滚动;在下一次滚动之前,我们不能将滚动顺序传递给另一个 RV。这是通过反转初始外部 RV 滚动值来稍微操纵的;并使用currentRV.smoothScrollBy(0, 10 * initialScroll) 向内部使用任意滚动值。我希望有人可以提出任何其他替代方案。

  • 使用更大的任意值(如 30)使语法滚动看起来更流畅>>currentRV.smoothScrollBy(0, 30 * initialScroll)

  • 并且在不反转滚动的情况下滚动外层滚动,使其在滚动的同一方向上也看起来更自然:

if (scrollDown) {
    boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
    if (!isLastItemShown) onScrollListener.onScroll(value);
    enableOuterScroll(isLastItemShown);

} else {
    boolean isFirstItemShown = currentFirstItem == 1;
    if (!isFirstItemShown) onScrollListener.onScroll(value);
    enableOuterScroll(isFirstItemShown);
}

更新 3

问题:在从外部到内部RecyclerView 的转换过程中出现故障,因为在决定是否可以滚动内部之前调用外部的onScroll()

通过使用OnTouchListener 到外部RecyclerView 并覆盖onTouch() 并返回true 来消费事件(这样onScrolled() 就不会被调用),直到我们决定内部可以接管滚动。

private float oldY = -1f;
outerRecyclerView.setOnTouchListener((v, event) -> {
    Log.d(LOG_TAG, "onTouch: ");
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            oldY = -1;
            break;

        case MotionEvent.ACTION_MOVE:
            float newY = event.getRawY();
            Log.d(LOG_TAG, "onTouch: MOVE " + (oldY - newY));

            if (oldY == -1f) {
                oldY = newY;
                return true; // avoid further listeners (i.e. addOnScrollListener)

            } else if (oldY < newY) { // increases means scroll UP
                outerAdapter.isOuterScrollingDown(false, (int) (oldY - newY));
                oldY = newY;

            } else if (oldY > newY) { // decreases means scroll DOWN
                outerAdapter.isOuterScrollingDown(true, (int) (oldY - newY));
                oldY = newY;
            }
            break;
    }
    return false;
});

更新 4

  • 每当内部 RV 滚动到其顶部或底部边缘时,启用从内部到外部的滚动过渡 RecyclerView,以便它继续以成比例的速度滚动到外部 RV。

滚动速度的灵感来自this post。通过在触摸的内部 RV 的 OnTouchListenerOnScrollListener 中应用第一项/最后一项检查,并在全新的触摸事件中重置内容,即在 MotionEvent.ACTION_DOWN

  • 在内部和外部RecyclerViews 中禁用过度滚动模式

预览:

方法二:在 NestedScrollView 中包裹外层 RecyclerView

Github sample

RecyclerView嵌套滚动的主要问题是它没有实现NestedScrollView实现的NestedScrollingParent3接口;所以RecyclerView 无法处理子视图的嵌套滚动。因此,尝试通过将外部RecyclerView 包裹在NestedScrollView 中来补偿NestedScrollView,并禁用外部RecyclerView 的滚动

优点:

  • 简单的代码(您根本不必操作内/外滚动)
  • 没有故障
  • 无缝滚动

缺点:

  • 由于外部RecyclerView 的视图没有被回收,因此性能低下,因此它们必须全部加载后才能显示在屏幕上。

原因:由于NestedScrollView的性质>>查看(1)(2)讨论回收问题的问题:

方法三:使用 ViewPager2 作为外部 RecyclerView

Github sample

使用 ViewPager2 在内部使用 RecyclerView 可以解决回收视图的问题,但一次只能显示一个页面(一个外部行)。

优点:

  • 使用NestedScrollableHost 时无故障和无缝滚动
  • 视图被回收,因为ViewPager2 中有一个内部RecyclerView

缺点:

  • 每页仅显示一个项目

所以我们可能会通过研究来解决这个问题:

  • 如何在每页显示多个视图
  • 如何包装页面内容

【讨论】:

  • 这看起来确实很有趣。我会检查样品。但我注意到的一件事是第一个警告:"The inner RecyclerView probably lose the ability of recycling views; because the shown rows of the outer recyclerView should load their items entirely"。这对我来说是一个很大的负面影响,因为其中一些内部 RV 有很多行(这是我从普通 ViewGroup 迁移到 RV 的部分原因),所以我真的需要避免一次添加所有项。有什么办法可以避免吗?
  • @drmrbrewer 谢天谢地,这不是一个正确的假设。请查看答案中的更新部分
  • 我尝试了示例(在您的 UPDATE 2 更改之后)。它非常接近。它仍然感觉有点小故障,因为即使在滚动内部 RV 时,外部 RV 有时也会滚动一点点(不是在任何一个极端)。此外,当实际触摸/滚动月份名称时,似乎无法滚动外部 RV……当这些“部分”关闭(内部 RV 隐藏)时,这一点尤其重要,因此它主要 显示外部 RV(月份行)。我很惊讶它需要付出如此多的努力才能接近实现应该恕我直言自动发生的事情。
  • @drmrbrewer 请看UPDATE 3第一种方法,第二种和第三种方法
  • 谢谢。您的回答对我有很大帮助,我相信其他人也会觉得它有用。对我来说,方法 2 是最吸引人的,因为它相对简单(只是将 RV 包装在 NSV 中)。在我的情况下,外部 RV 相对较短且非常简单(实际上只是部分标题,与您的月份名称非常相似,但更多),所以从一开始就必须布置所有行并不是什么大问题(如果它允许我保留 RV 模型的其他优点)。
【解决方案2】:

这是一种使用NestedScrollingParent3 接口改进RecyclerView 的方法。 NestedRecyclerView 类用作外部 RecyclerView,而库存 RecyclerView 用作内部项目视图。

以下代码基于来自 Widgetlabs here 的代码,该代码在 MIT 许可证下获得许可。下面显示的代码已对原始代码进行了大量修改。

操作 当外层的RecyclerView被触摸时,可以按预期上下滚动,而内层的RecyclerView也随之拖动。

当内部的RecyclerView被触摸时,可以上下滚动而不影响外部的RecyclerView。当内部 RecyclerView 向下或向上达到最大范围时,外部 RecyclerView 开始滚动。一旦外部 RecyclerView 开始滚动,它就捕获了手势,并且在另一个“向下”事件之前不会释放它。这与 RecyclerViewNestedScrollView 中的行为方式不同,其中外部 RecyclerView 滚动但内部 RecyclerView 将在滚动时恢复使用滚动手势方向变化。本质上,内部的 RecyclerView 永远不会放弃控制权。

以下视频演示了NestedRecyclerView的操作。在视频中,内部 RecyclerView 具有白色边框和橙色和红色行。其他的都是外部的RecyclerView

NestedRecyclerView

open class NestedRecyclerView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr), NestedScrollingParent3 {

    private var nestedScrollTarget: View? = null
    private var nestedScrollTargetWasUnableToScroll = false
    private val parentHelper by lazy { NestedScrollingParentHelper(this) }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        // Nothing special if no child scrolling target.
        if (nestedScrollTarget == null) return super.dispatchTouchEvent(ev)

        // Inhibit the execution of our onInterceptTouchEvent for now...
        requestDisallowInterceptTouchEvent(true)
        // ... but do all other processing.
        var handled = super.dispatchTouchEvent(ev)

        // If the first dispatch yielded an unhandled event or the descendant view is unable to
        // scroll in the direction the user is scrolling, we dispatch once more but without skipping
        // our onInterceptTouchEvent. Note that RecyclerView automatically cancels active touches of
        // all its descendants once it starts scrolling so we don't have to do that.
        requestDisallowInterceptTouchEvent(false)
        if (!handled || nestedScrollTargetWasUnableToScroll) {
            handled = super.dispatchTouchEvent(ev)
        }

        return handled
    }

    // We only support vertical scrolling.
    override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int) =
        nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0

    /*  Introduced with NestedScrollingParent2. */
    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int) =
        onStartNestedScroll(child, target, axes)

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
        if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
            // A descendant started scrolling, so we'll observe it.
            setTarget(target)
        }
        parentHelper.onNestedScrollAccepted(child, target, axes)
    }

    /*  Introduced with NestedScrollingParent2. */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
            // A descendant started scrolling, so we'll observe it.
            setTarget(target)
        }
        parentHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            dispatchNestedPreScroll(dx, dy, consumed, null)
        } else {
            super.onNestedPreScroll(target, dx, dy, consumed)
        }
    }

    /*  Introduced with NestedScrollingParent2. */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        onNestedPreScroll(target, dx, dy, consumed)
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int
    ) {
        if (target === nestedScrollTarget && dyUnconsumed != 0) {
            // The descendant could not fully consume the scroll. We remember that in order
            // to allow the RecyclerView to take over scrolling.
            nestedScrollTargetWasUnableToScroll = true
            // Let the parent start to consume scroll events.
            target.parent?.requestDisallowInterceptTouchEvent(false)
        }
    }

    /*  Introduced with NestedScrollingParent2. */
    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int
    ) {
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
    }

    /*  Introduced with NestedScrollingParent3. */
    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
    }

    /* From ViewGroup */
    override fun onStopNestedScroll(child: View) {
        // The descendant finished scrolling. Clean up!
        setTarget(null)
        parentHelper.onStopNestedScroll(child)
    }

    /*  Introduced with NestedScrollingParent2. */
    override fun onStopNestedScroll(target: View, type: Int) {
        // The descendant finished scrolling. Clean up!
        setTarget(null)
        parentHelper.onStopNestedScroll(target, type)
    }

    /*  Introduced with NestedScrollingParent2. */
    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            false
        } else {
            super.onNestedPreFling(target, velocityX, velocityY)
        }
    }

    /* In ViewGroup for API 21+. */
    override fun onNestedFling(
        target: View,
        velocityX: Float,
        velocityY: Float,
        consumed: Boolean
    ) =
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            false
        } else {
            super.onNestedFling(target, velocityX, velocityY, consumed)
        }

    private fun setTarget(target: View?) {
        nestedScrollTarget = target
        nestedScrollTargetWasUnableToScroll = false
    }
}

现在需要注意的是:前面的代码已经轻(非常轻地)测试了RecyclerView 环境很复杂,将嵌套滚动引入到 RecyclerView 作为该滚动的父级使其更加复杂且容易出错。尽管如此,这是一项有趣的 IMO 研究。

【讨论】:

  • 有趣的方法,谢谢。外部RecyclerView 实际上是RecyclerView 还是现在是ScrollView?我特别喜欢使用RecyclerView,因为它可以与实现FilterableRecyclerView.Adapter 相关联。
  • @drmrbrewer 外部视图是一个 NestedRecyclerView 如答案所示。内部可滚动部分只是一个普通的 RecyclerView。所以它是真正的 RecyclerViews 嵌套。
  • @drmrbrewer 您应该能够将外部 RecyclerView 替换为 NestedRecyclerView 以查看滚动行为。如果不是这样,我很想知道。
【解决方案3】:

这是 Zain 示例的扩展,它避免了任何自定义滚动逻辑并选择 NestedScrollViewRecyclerView 的组合

Github Link

这里方法的主要区别是让NestedScrollView 处理复杂的滚动计算,以拦截/调度滚动事件到嵌套布局。

我为嵌套的回收器添加了一个LinearLayout 容器,重量为 1,因此它们的高度一致。

fwiw,我们还可以使用 RecyclerView.setRecycledViewPool()RecyclerView.setRecycledViewPool() 跨回收器视图共享视图池以提高性能

PS:此解决方案对性能的一个明确影响是,在设置时将加载 m * n 视图,其中:m - 行数,n - 内部回收器的可见视图。

因此,对于较大的 m 值,这不会很好地扩展。

【讨论】:

    【解决方案4】:

    查看Github Sample:

    1. 在回收器适配器中扩展 BaseRecyclerAdapter(您可以在 github 中获得整个代码)并实现以下方法。

      class ListAdapter : BaseRecyclerAdapter<ListDataModel, ChildDataModel>() {
      
          // Pass parent and child model in BaseRecyclerAdapter
      
          override fun getLayoutIdForType(): Int = R.layout.item_parent  // Provide parent recycler item id
      
          override fun getLayoutIdForChild(): Int = R.layout.item_child  // Provide child recycler item id
      
          // Click event fot parent recycler item
          override fun onParentItemClick(triple: Triple<Int, Any, View>, viewDataBinding: ViewDataBinding) {
              val data = triple.second as ListDataModel // Here you can get parent item data for clicked item
              val position = triple.first // Get position of clicked item
              val view = triple.third // Get view of clicked item
      
              when (view.id) {
                  R.id.text_movie_year -> {
                      // Call this function where you want to expand collapse childView
                      expandCollapse(triple.first, viewDataBinding)
                  }
              }
          }
      
          // Click event for child rectycler item
          override fun onChildItemClicked(triple: Triple<Int, Any, View>, parentIndex: Int) {
          val data = triple.second as ChildDataModel // Here you can get child item data for clicked item
          val position = triple.first // Get position of clicked item
          val view = triple.third // Get view of clicked item
      
              when (view.id) {
                  R.id.img_download_movie -> {
                      // Here you can perform your action
                  }
              }
          }
      }
      

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-10
      • 1970-01-01
      • 1970-01-01
      • 2020-12-02
      • 2015-11-30
      • 2016-01-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多