【问题标题】:Sync scrolling of multiple RecyclerViews多个 RecyclerView 的同步滚动
【发布时间】:2015-06-08 06:48:54
【问题描述】:

我有一个 ViewPager,每页显示一个片段。这个片段包含一个 RecyclerView 中的项目列表。项目列表的大小始终相同,项目的视图也具有相同的高度。滚动其中一个 RecyclerViews 时,我希望其他 RecyclerViews 同时滚动相同的距离。如何同步 RecyclerViews 的滚动?

【问题讨论】:

标签: android android-recyclerview


【解决方案1】:

这是我的解决方案。代码越少越好……

lvDetail & lvDetail2 是你想要保持同步的 RecyclerViews。

    final RecyclerView.OnScrollListener[] scrollListeners = new RecyclerView.OnScrollListener[2];
    scrollListeners[0] = new RecyclerView.OnScrollListener( )
    {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy)
        {
            super.onScrolled(recyclerView, dx, dy);
            lvDetail2.removeOnScrollListener(scrollListeners[1]);
            lvDetail2.scrollBy(dx, dy);
            lvDetail2.addOnScrollListener(scrollListeners[1]);
        }
    };
    scrollListeners[1] = new RecyclerView.OnScrollListener( )
    {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy)
        {
            super.onScrolled(recyclerView, dx, dy);
            lvDetail.removeOnScrollListener(scrollListeners[0]);
            lvDetail.scrollBy(dx, dy);
            lvDetail.addOnScrollListener(scrollListeners[0]);
        }
    };
    lvDetail.addOnScrollListener(scrollListeners[0]);
    lvDetail2.addOnScrollListener(scrollListeners[1]);

【讨论】:

  • 有时这两个不同步(因为我们依赖scrollBy)。然后呢?
  • 如果你扔掉其中一辆房车,它们就会不同步。
  • 伙计们,我怎样才能实现这个不同步?我试着到处乱扔,没有效果,效果很好……
【解决方案2】:

我相信了解它的工作原理对您很重要,因此我将解释我设计解决方案所遵循的整个过程。请注意,此示例仅适用于两个 RecyclerView,但使用更多 RecyclerView 就像使用 RecyclerView 数组一样简单。

想到的第一个选项是监听两个 ScrollView 上的滚动变化,当其中一个滚动时,在另一个上使用 scrollBy(int x, int y)。不幸的是,以编程方式滚动也会触发侦听器,因此您最终会陷入循环。

要解决这个问题,您需要设置一个 OnItemTouchListener,它在 RecyclerView 被触摸时添加适当的 ScrollListener,并在滚动停止时将其移除。这几乎完美无缺,但如果您在长 RecyclerView 中执行快速投掷,然后在完成之前再次滚动它,则只会传输第一个滚动。

要解决此问题,您需要确保仅在 RecyclerView 空闲时添加 OnScrollListener。

我们看一下源码:

    public class SelfRemovingOnScrollListener extends RecyclerView.OnScrollListener {

    @Override
    public final void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            recyclerView.removeOnScrollListener(this);
        }
    }
}

这是您需要扩展 OnScrollListeners 的类。这样可以确保在需要时将其移除。

然后我有两个监听器,每个 RecyclerView 一个:

private final RecyclerView.OnScrollListener mLeftOSL = new SelfRemovingOnScrollListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mRightRecyclerView.scrollBy(dx, dy);
    }
}, mRightOSL = new SelfRemovingOnScrollListener() {

    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mLeftRecyclerView.scrollBy(dx, dy);
    }
};

然后在初始化时您可以设置 OnItemTouchListeners。最好为整个视图设置一个监听器,但是 RecyclerView 不支持这个。 OnItemTouchListeners 无论如何都不会造成问题:

    mLeftRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        private int mLastY;

        @Override
        public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
        MotionEvent e) {
            Log.d("debug", "LEFT: onInterceptTouchEvent");

            final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
            if (!ret) {
                onTouchEvent(rv, e);
            }
            return Boolean.FALSE;
        }

        @Override
        public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
            Log.d("debug", "LEFT: onTouchEvent");

            final int action;
            if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mRightRecyclerView
                    .getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                mLastY = rv.getScrollY();
                rv.addOnScrollListener(mLeftOSL);
            }
            else {
                if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                    rv.removeOnScrollListener(mLeftOSL);
                }
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
            Log.d("debug", "LEFT: onRequestDisallowInterceptTouchEvent");
        }
    });

    mRightRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        private int mLastY;

        @Override
        public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
        MotionEvent e) {
            Log.d("debug", "RIGHT: onInterceptTouchEvent");

            final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
            if (!ret) {
                onTouchEvent(rv, e);
            }
            return Boolean.FALSE;
        }

        @Override
        public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
            Log.d("debug", "RIGHT: onTouchEvent");

            final int action;
            if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mLeftRecyclerView
                    .getScrollState
                            () == RecyclerView.SCROLL_STATE_IDLE) {
                mLastY = rv.getScrollY();
                rv.addOnScrollListener(mRightOSL);
            }
            else {
                if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                    rv.removeOnScrollListener(mRightOSL);
                }
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
            Log.d("debug", "RIGHT: onRequestDisallowInterceptTouchEvent");
        }
    });
}

还要注意,在我的特殊情况下,RecyclerViews 不是第一个接收到触摸事件的,所以我需要拦截它。如果这不是您的情况,您可以(应该)将代码从 onInterceptTouchEvent(...) 合并到 onTouchEvent(...)。

最后,如果您的用户尝试同时滚动您的两个 RecyclerView,这将导致崩溃。这里可能的最佳工作质量解决方案是在包含 RecyclerViews 的直接父级中设置 android:splitMotionEvents="false"

您可以查看此代码here 的示例。

【讨论】:

  • +1,在我的情况下(最小:棒棒糖,编译:棉花糖),省略 android:splitMotionEvents="false" 不会导致崩溃,而是导致视图不同步。
  • 如果你在 recyclerview 还在稳定的时候开始拖动,那么视图会不同步。
  • @EpicPandaForce 请在github.com/Stoyicker/AligningRecyclerView 上查看我的库。它在play.google.com/store/apps/… 中使用,在商品列表中的视频中,您可以看到在结算完成之前如何拖拉,然后再拖拉不会导致不同步。
  • 但是当recycle view items也是recycleviews时怎么办?在那种情况下如何组织同步滚动?
  • 好先生。但是你能提示一下如何将它用于两个具有不同高度和相同物品的回收站视图吗?坦克
【解决方案3】:

我想我找到了一个非常简单且简短的答案。

正如 Jorge Antonio Díaz-Benito 所说,“想到的第一个选项是监听两个 ScrollView 上的滚动变化,当其中一个滚动时,在另一个上使用 scrollBy(int x, int y)。不幸的是,以编程方式滚动也会触发侦听器,因此您最终会陷入循环。”

所以你需要解决这个问题。如果你只是跟踪谁在滚动,他们就不会循环。

解决方案

public class SelfScrolListener extends RecyclerView.OnScrollListener {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            viewIsScrolling = -1;
        }
    }
}

这是您自定义的 OnScrollListener,用于检查 scrollState 是否为 IDLE。这是真的-> 没有人在滚动。所以`int viewIsScolling = -1

现在您需要检测是否可以滚动。 这是代码:

int viewIsScrolling = 1;
boolean firstIsTouched = false;
boolean secondIsTouched = false;
SelfScrolListener firstOSL= new SelfScrolListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);

        if (firstIsTouched) {
             if (viewIsScrolling == -1) {
                 viewIsScrolling = 0;
             }
             if (viewIsScrolling == 0) {
                 secondRecyclerView.scrollBy(dx, dy);
             }
        }
    }
};
SelfScrolListener secondOSL= new SelfScrolListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if(secondIsTouched){
             if (viewIsScrolling == -1) {
                 viewIsScrolling = 1;
             }
             if (viewIsScrolling == 1) {
                 firstRecyclerView.scrollBy(dx, dy);
             }
        }
    }
};
firstRecyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        firstIsTouched= true;
        return false;
    }
});
secondRecyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        secondIsTouched= true;
        return false;
    }
});
firstRecyclerView.addOnScrollListener(firstOSL);
secondRecyclerView.addOnScrollListener(secondOSL);

viewIsScrolling = 一个全局 int 并在开头设置为 -1;没有人滚动的状态。您可以根据需要发布尽可能多的回收视图。

【讨论】:

  • 我尝试了类似的方法,但第一个 RecyclerView 根本不滚动。如果您知道如何提供帮助,您可以查看我的问题here。谢谢
  • Bug,当你一扔
【解决方案4】:

此解决方案处理投掷。演示项目here

假设您有三个水平的 RecyclerView。

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv0"
        android:layout_width="match_parent"
        android:layout_height="@dimen/itemSize"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv1"
        android:layout_width="match_parent"
        android:layout_height="@dimen/itemSize"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="horizontal"
        android:layout_marginTop="5dp"
        app:layout_constraintTop_toBottomOf="@id/rv0"
        />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv2"
        android:layout_width="match_parent"
        android:layout_height="@dimen/itemSize"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="horizontal"
        android:layout_marginTop="5dp"
        app:layout_constraintTop_toBottomOf="@id/rv1"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 为每个 RecyclerView 添加标签。例如:
val rvs = arrayListOf(rv0, rv1, rv2)

for (i in 0 until rvs.size) {
    rvs[i].tag = i
}
  1. 为每个 RecyclerView 添加相同的 OnItemTouchListener 以了解哪个 RecyclerView 处于活动状态。如果已经有一个活动的 RecyclerView 则在其上调用 stopScroll(如果活动的 RecyclerView 处于 fling 状态,它会防止不同步)
var touchedRVTag = -1

val itemTouchListener = object : RecyclerView.OnItemTouchListener {
    override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}

    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean 
    {
        val rvTag = rv.tag as Int


        if (touchedRVTag != -1 && touchedRVTag != rvTag) {
            rvs[touchedRVTag].stopScroll()
        }

        touchedRVTag = rvTag

        return false
    }

    override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
}

for (i in 0 until rvs.size) {
    rvs[i].addOnItemTouchListener(itemTouchListener)
}
  1. 为每个 RecyclerView 添加相同的 OnScrollListener 以滚动其他 RecyclerView
val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        val currentRVTag = recyclerView.tag as Int

        if (currentRVTag == touchedRVTag) {
            for (rvTag in 0 until rvs.size) {
                if (rvTag != currentRVTag) {
                    rvs[rvTag].scrollBy(dx, 0)
                }
            }
        }
    }
}

for (i in 0 until rvs.size) {
    rvs[i].addOnScrollListener(scrollListener)
}

PS:如果你想滚动任何 RecyclerView 以编程方式设置它的标签然后滚动:

touchedRVTag = rv0.tag as Int
rv0.scrollBy(dx, 0)

【讨论】:

    【解决方案5】:

    kotlin 版本,这个还不能处理 fling

    class ScrollSynchronizer {
        val boundRecyclerViews: ArrayList<RecyclerView> = ArrayList()
        var verticalOffset = 0
    
        private val scrollListener = object: RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                // scroll every other rv to the same position
                boundRecyclerViews.filter { it != recyclerView }.forEach { targetView ->
                    targetView.removeOnScrollListener(this)
                    targetView.scrollBy(dx, dy)
                    targetView.addOnScrollListener(this)
                }
            }
    
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    verticalOffset = recyclerView.computeVerticalScrollOffset()
                }
            }
        }
    
        fun add(view: RecyclerView) {
            with (view) {
                if (boundRecyclerViews.contains(view)) {
                    removeOnScrollListener(scrollListener)
                    boundRecyclerViews.remove(this)
                }
                addOnScrollListener(scrollListener)
                boundRecyclerViews.add(this)
            }
        }
    
        fun scrollToLastOffset(rv: RecyclerView) {
            rv.removeOnScrollListener(scrollListener)
            val currentOffset = rv.computeVerticalScrollOffset()
            if (currentOffset != verticalOffset) {
                rv.scrollBy(0, verticalOffset - currentOffset)
            }
            rv.addOnScrollListener(scrollListener)
        }
    
        fun disable() = boundRecyclerViews.forEach { it.removeOnScrollListener(scrollListener) }
    
        fun enable() = boundRecyclerViews.forEach { it.addOnScrollListener(scrollListener) }
    
        fun addAll(recyclerViews: List<RecyclerView>) = recyclerViews.forEach { add(it) }
    }
    

    编辑:在@Groovee60 评论和推荐之后,将onScrolled 替换为:

                // scroll every other rv to the same position
                val otherRvs = boundRecyclerViews.filter { it != recyclerView }
                otherRvs.forEach { targetView -> targetView.removeOnScrollListener(this) }
                otherRvs.forEach { targetView -> targetView.scrollBy(dx, dy) }
                otherRvs.forEach { targetView -> targetView.addOnScrollListener(this) }
    

    【讨论】:

    • 您好,我发现您的方法是最容易集成的,并且在添加另一个触摸侦听器以禁用投掷手势后效果很好。但是需要进行一项改进 - 不是删除目标视图的侦听器,然后在调用 scrollBy() 后重新添加它,而是应该在循环中删除所有侦听器,在简化版本中调整其他侦听器的滚动循环,然后在循环中重新添加所有侦听器。
    • @Groovee60 是的,这在当时是非常实验性的,在我正在处理的实际项目中,我最终使用 ScrollViews 而不是 RecyclerViews,因为内容是静态的,但最终做了一个 ScrollViewSynchronizer 类具有相同类型的功能,并做了与您所说的类似的事情(如果我记得的话)。
    【解决方案6】:

    首先考虑使用NestedScrollView 作为RecyclerViews 的父级。这可能需要一些额外的调整,但总体思路是一样的:

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
    </android.support.v4.widget.NestedScrollView>
    

    如果由于某些原因无法执行此操作,您可以通过编程方式同步滚动。只需要避免onScrolled() 事件回调的无限循环,正如其他答案中已经提到的那样。换句话说,当您开始以编程方式滚动时,您在 onScroll() 内什么都不做,直到您以编程方式完成滚动。并且不要以编程方式滚动最初滚动的RecyclerView

    把所有的Recycler同步进去

    List<RecyclerView> syncRecyclers
    

    打电话

    addSyncListeners()
    

    享受

    public class SyncScrollActivity extends AppCompatActivity {
        private List<RecyclerView> syncRecyclers;
        private boolean isProgrammaticallyScrolling = false;
    
    
        private void addSyncListeners() {
            for (RecyclerView recyclerView : syncRecyclers) {
                recyclerView.addOnScrollListener(new SyncOnScrollListener());
            }
        }
    
        private class SyncOnScrollListener extends RecyclerView.OnScrollListener {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                if (!isProgrammaticallyScrolling) {
                    isProgrammaticallyScrolling = true;
                    scrollAll(recyclerView, dx, dy);
                    isProgrammaticallyScrolling = false;
                }
            }
        }
    
        private void scrollAll(RecyclerView exceptRecycler, int dx, int dy) {
            for (RecyclerView recyclerView : syncRecyclers) {
                if (!recyclerView.equals(exceptRecycler)) {
                    recyclerView.scrollBy(dx, dy);
                }
            }
        }
    }
    

    【讨论】:

    • 我同意 NestedScrollView 解决方案在编写完整的滚动同步机制之前确实应该被视为第一个,尽管:1. NestedScrollView 应该只包含一个子项,因此只需要将回收器视图嵌套在另一个布局中,然后有用。 2. NestedScrollView 只适用于垂直滚动。对于水平滚动,我们需要一种不同的方法。
    【解决方案7】:

    在您的寻呼机适配器中创建变量,该变量将负责保存其页面的当前scrollY 位置,并且每当您的getItem(position) 将调用更新ListView positoin 时,请查看CacheFragmentStatePagerAdapter in https://github.com/ksoichiro/Android-ObservableScrollView/blob/master/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java

    【讨论】:

    • 据我了解,这不允许我同步 RecyclerViews 的滚动。如果创建了包含 RecyclerView 的 Fragment,它会将 RecyclerView 设置为当前位置,但它不考虑 RecyclerView 的偏移量。此外,如果 Fragment 没有重新创建,它将不会更新到正确的位置。
    • 它只是一个示例代码或提示,研究并实现您的逻辑,即每次在 y 轴上增加/减少时更新 scrollY
    • 嗯,我可以看到ObservableRecyclerViewObservableScrollViewCallbacks 对于找出RecyclerView 之一的滚动很有用,但这仍然让我对如何设置一无所知到另一个 RecyclerView 的滚动距离。
    • 使用视图寻呼机,在 viewpager 的 onPageSelected 上添加您选择的更新回收站视图
    • 我在ViewPager 中同时在屏幕上显示了两个页面,所以很遗憾这不是一个选项。此外,这不会考虑当前可见项目的偏移量。
    【解决方案8】:

    我找到了一个非常简单的解决方案,没有任何标志,而且非常动态

    第一件事是您必须创建RecyclerView.OnScrollListener 并将该侦听器订阅到您希望的每个水平布局。 RecyclerView.OnScrollListener:

     val scrollListener = object : RecyclerView.OnScrollListener() {
                    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                        super.onScrolled(recyclerView, dx, dy)
                        horizontalScroll(this, recyclerView, dx)
                    }
                }
    
    
    
    fun horizontalScroll(
                scrollListener: RecyclerView.OnScrollListener,
                currentRecycler: RecyclerView,
                dx: Int
            ) {
                for (i in 0..parentRecyclerView.childCount) {
                    val childRecyclerView =
                        parentRecyclerView.getChildAt(i)?.findViewById<RecyclerView>(R.id.recyclerView)
                    if (currentRecycler != childRecyclerView) {
                        childRecyclerView?.removeOnScrollListener(scrollListener)
                        childRecyclerView?.scrollBy(dx, 0)
                        childRecyclerView?.addOnScrollListener(scrollListener)
                    }
                }
            }
    

    请注意,您的 RecyclerView 必须具有相同的layout.id。在具有水平行的垂直 RecyclerView 的情况下很有用

    【讨论】:

      猜你喜欢
      • 2017-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-20
      • 1970-01-01
      相关资源
      最近更新 更多