【问题标题】:How to sync scrolling first-positions of 2 RecyclerViews?如何同步 2 个 RecyclerViews 的滚动第一位置?
【发布时间】:2018-04-27 04:40:15
【问题描述】:

背景

我有 2 个 RecyclerView 实例。一个是水平的,一个是垂直的。

它们都显示相同的数据并具有相同数量的项目,但方式不同,并且每个单元格的大小不一定相同。

我希望滚动一个将与另一个同步,以便显示在一个上的第一个项目将始终显示在另一个上(作为第一个)。

问题

即使我已经成功地使它们同步(我只是选择哪个是“主”,以控制另一个的滚动),滚动的方向似乎会影响它的工作方式。

假设细胞的高度相等:

如果我向上/向左滚动,它或多或少会按照我的预期工作:

但是,如果我向下/向右滚动,它确实会让另一个 RecyclerView 显示另一个的第一项,但通常不会作为第一项:

注意:在上面的屏幕截图中,我在底部的 RecyclerView 中滚动,但与顶部的结果类似。

如果像我写的那样,单元格的大小不同,情况会变得更糟:

我尝试过的

我尝试使用其他方式滚动并转到其他位置,但所有尝试都失败了。

使用 smoothScrollToPosition 让事情变得更糟(尽管它看起来确实更好),因为如果我一掷千金,在某些时候另一个 RecyclerView 会控制我与之交互的那个。

我想我应该使用滚动的方向,以及其他 RecyclerView 上当前显示的项目。

这是当前(示例)代码。请注意,在实际代码中,单元格的大小可能不相等(有些高,有些短,等等......)。代码中的一行代码使单元格具有不同的高度。

activity_main.xml

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/topReccyclerView" android:layout_width="0dp" android:layout_height="100dp"
        android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp"
        android:orientation="horizontal" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/horizontal_cell"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/bottomRecyclerView" android:layout_width="0dp" android:layout_height="0dp"
        android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
        android:layout_marginTop="8dp" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/topReccyclerView"
        tools:listitem="@layout/horizontal_cell"/>
</android.support.constraint.ConstraintLayout>

horizo​​ntal_cell.xml

<TextView
    android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="100dp" android:layout_height="100dp"
    android:gravity="center" tools:text="@tools:sample/lorem"/>

vertical_cell.xml

<TextView
    android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="50dp"
    android:gravity="center" tools:text="@tools:sample/lorem"/>

MainActivity

class MainActivity : AppCompatActivity() {
    var masterView: View? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val inflater = LayoutInflater.from(this)
        topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
                holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
            }

            override fun getItemCount(): Int {
                return 100
            }

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
            }
        }

        bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
                holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                // this makes the heights of the cells different from one another:
                holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
            }

            override fun getItemCount(): Int {
                return 100
            }

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
            }
        }
        LinearSnapHelper().attachToRecyclerView(topReccyclerView)
        LinearSnapHelper().attachToRecyclerView(bottomRecyclerView)
        topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
        bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
    }

    inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
        var lastItemPos: Int = Int.MIN_VALUE
        val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)

        override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            Log.d("AppLog", "onScrollStateChanged:$thisRecyclerViewId $newState")
            when (newState) {
                RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
                    Log.d("AppLog", "setting $thisRecyclerViewId to be master")
                    masterView = thisRecyclerView
                }
                RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
                    Log.d("AppLog", "resetting $thisRecyclerViewId from being master")
                    masterView = null
                    lastItemPos = Int.MIN_VALUE
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            if ((dx == 0 && dy == 0) || (masterView != null && masterView != thisRecyclerView))
                return
            //            Log.d("AppLog", "onScrolled:$thisRecyclerView $dx-$dy")
            val currentItem = (thisRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
            if (lastItemPos == currentItem)
                return
            lastItemPos = currentItem
            otherRecyclerView.scrollToPosition(currentItem)
//            otherRecyclerView.smoothScrollToPosition(currentItem)
            Log.d("AppLog", "currentItem:" + currentItem)
        }
    }
}

问题

  1. 如何让另一个 RecycerView 的第一项始终与当前控制的一项相同?

  2. 如何修改代码以支持平滑滚动,而不会导致突然让另一个 RecyclerView 成为控制者?


编辑:在此处使用不同大小的单元格更新示例代码后(因为最初这更接近我遇到的问题,如前所述),我注意到捕捉效果不佳。

这就是我选择使用这个库来正确捕捉它的原因:

https://github.com/DevExchanges/SnappingRecyclerview

所以我使用“GravitySnapHelper”而不是 LinearSnapHelper。似乎效果更好,但仍然存在同步问题,并且在滚动时会触摸。


编辑: 我终于解决了所有同步问题,即使单元格大小不同,它也能正常工作。

还有一些问题:

  1. 如果你打开一个 RecyclerView,然后再触摸另一个,它会出现非常奇怪的滚动行为。可能会滚动得更多。

  2. 滚动不流畅(同步和投掷时),所以看起来不太好。

  3. 遗憾的是,由于捕捉(实际上我可能只需要顶部 RecyclerView),它会导致另一个问题:底部 RecyclerView 可能会部分显示最后一个项目(包含 100 个项目的屏幕截图),我可以t 滚动更多以完全显示它:

我什至不认为底部的 RecyclerView 应该对齐,除非顶部的 RecyclerView 被触摸。遗憾的是,这就是我目前所得到的一切,没有同步问题。

这是我找到的所有修复后的新代码:

class MainActivity : AppCompatActivity() {
    var masterView: View? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val inflater = LayoutInflater.from(this)
        topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
                holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
            }

            override fun getItemCount(): Int = 1000

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
            }
        }

        bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
                holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
            }

            override fun getItemCount(): Int = 1000

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
            }
        }
        // GravitySnapHelper is available from : https://github.com/DevExchanges/SnappingRecyclerview
        GravitySnapHelper(Gravity.START).attachToRecyclerView(topReccyclerView)
        GravitySnapHelper(Gravity.TOP).attachToRecyclerView(bottomRecyclerView)
        topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
        bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
    }

    inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
        var lastItemPos: Int = Int.MIN_VALUE
        val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)

        override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            when (newState) {
                RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
                    masterView = thisRecyclerView
                }
                RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
                    masterView = null
                    lastItemPos = Int.MIN_VALUE
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            if (dx == 0 && dy == 0 || masterView !== null && masterView !== thisRecyclerView) {
                return
            }
            val otherLayoutManager = otherRecyclerView.layoutManager as LinearLayoutManager
            val thisLayoutManager = thisRecyclerView.layoutManager as LinearLayoutManager
            val currentItem = thisLayoutManager.findFirstCompletelyVisibleItemPosition()
            if (lastItemPos == currentItem) {
                return
            }
            lastItemPos = currentItem
            otherLayoutManager.scrollToPositionWithOffset(currentItem, 0)
        }
    }
}

【问题讨论】:

  • 编辑你的截图,它们似乎坏了。
  • 在我的浏览器中,它们渲染得很好。

标签: android android-recyclerview


【解决方案1】:

结合两个RecyclerViews,有四种运动情况:

一个。向左滚动水平回收器

b.向右滚动

c。将垂直回收器滚动到顶部

d。滚动到底部

案例 a 和 c 不需要照顾,因为它们开箱即用。对于情况 b 和 d,您需要做两件事:

  1. 知道您在哪个回收站(垂直或水平)以及滚动的方向(向上或向下分别向左或向右)和
  2. 根据otherRecyclerView 中可见项目的数量计算(列表项的)偏移量(如果屏幕更大,偏移量也需要更大)。

解决这个问题有点繁琐,但结果很简单。

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
            if (masterView == otherRecyclerView) {
                thisRecyclerView.stopScroll();
                otherRecyclerView.stopScroll();
                syncScroll(1, 1);
            }
            masterView = thisRecyclerView;
        } else if (newState == RecyclerView.SCROLL_STATE_IDLE && masterView == thisRecyclerView) {
            masterView = null;
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerview, int dx, int dy) {
        super.onScrolled(recyclerview, dx, dy);
        if ((dx == 0 && dy == 0) || (masterView != null && masterView != thisRecyclerView)) {
            return;
        }
        syncScroll(dx, dy);
    }

    void syncScroll(int dx, int dy) {
        LinearLayoutManager otherLayoutManager = (LinearLayoutManager) otherRecyclerView.getLayoutManager();
        LinearLayoutManager thisLayoutManager = (LinearLayoutManager) thisRecyclerView.getLayoutManager();
        int offset = 0;
        if ((thisLayoutManager.getOrientation() == HORIZONTAL && dx > 0) || (thisLayoutManager.getOrientation() == VERTICAL && dy > 0)) {
            // scrolling horizontal recycler to left or vertical recycler to bottom
            offset = otherLayoutManager.findLastCompletelyVisibleItemPosition() - otherLayoutManager.findFirstCompletelyVisibleItemPosition();
        }
        int currentItem = thisLayoutManager.findFirstCompletelyVisibleItemPosition();
        otherLayoutManager.scrollToPositionWithOffset(currentItem, offset);
    }

当然,您可以将两个 if 子句结合起来,因为它们的主体是相同的。为了便于阅读,我认为最好将它们分开。

第二个问题是在“第一个”回收器仍在滚动时触摸相应的“其他”回收器时同步。下面的代码(包括在上面)是相关的:

if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
    if (masterView == otherRecyclerView) {
        thisRecyclerView.stopScroll();
        otherRecyclerView.stopScroll();
        syncScroll(1, 1);
    }
    masterView = thisRecyclerView;
}

newState 等于SCROLL_STATE_DRAGGING 当回收器被触摸并拖动一点时。因此,如果这是在触摸相应的“其他”回收器后触摸(& 拖动),则第二个条件 (masterView == otherRecyclerview) 为真。然后两个回收器都停止,“其他”回收器与“这个”回收器同步。

【讨论】:

  • 这可行,但smoothScrollToPosition 可能会导致问题。尝试在一个 RecyclerView 上投掷,然后在两者都滚动时触摸另一个。我认为它会导致类似于比赛条件的事情。现在我已经尝试过了,即使我改用scrollToPosition 也会发生这种情况。我该怎么做才能解决它?
  • 好的,我明白了。有一种方法可以使两个列表不同步。等一下,我也会解决这个问题的。
  • 你知道为什么会这样吗?也许我应该让它们都停止滚动并让用户触摸的那个成为主人并设置停止的位置。
  • 我看不出你做了什么改变。另外,你能告诉我怎么做我写的吗?一旦用户触摸视图,它就会成为主人并阻止它们滚动......?
  • 我认为代码在项目大小不相等的情况下也存在问题:它可能会像第一个项目一样几乎完美地滚动到正确的项目,因为它之前的项目仍然会显示,只是部分。如果您愿意,我需要制作一个更好的 POC 来显示这个特定问题。为了简单起见,我在问题的 POC 中写了它(并且也写了一个注释),因为我相信这不会成为问题。
【解决方案2】:

1-) 布局管理器

当前smoothScrollToPosition 不会将元素带到顶部。所以让我们编写一个新的布局管理器。让我们覆盖这个布局管理器的smoothScrollToPosition

public class TopLinearLayoutManager extends LinearLayoutManager
{
    public TopLinearLayoutManager(Context context, int orientation)
    {
        //orientation : vertical or horizontal
        super(context, orientation, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)
    {
        RecyclerView.SmoothScroller smoothScroller = new TopSmoothScroller(recyclerView.getContext());
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }

    private class TopSmoothScroller extends LinearSmoothScroller
    {
        TopSmoothScroller(Context context)
        {
            super(context);
        }

        @Override
        public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)
        {
            return (boxStart - viewStart);
        }
    }
}

2-) 设置

    //horizontal one
    RecyclerView rvMario = (RecyclerView) findViewById(R.id.rvMario);

    //vertical one
    RecyclerView rvLuigi = (RecyclerView) findViewById(R.id.rvLuigi);

    final LinearLayoutManager managerMario = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.HORIZONTAL, false);
    rvMario.setLayoutManager(managerMario);
    ItemMarioAdapter adapterMario = new ItemMarioAdapter(itemList);
    rvMario.setAdapter(adapterMario);

     //Snap to start by using Ruben Sousa's RecyclerViewSnap
    SnapHelper snapHelper = new GravitySnapHelper(Gravity.START);
    snapHelper.attachToRecyclerView(rvMario);

    final TopLinearLayoutManager managerLuigi = new TopLinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL);
    rvLuigi.setLayoutManager(managerLuigi);
    ItemLuigiAdapter adapterLuigi = new ItemLuigiAdapter(itemList);
    rvLuigi.setAdapter(adapterLuigi);

3-) 滚动监听器

rvMario.addOnScrollListener(new RecyclerView.OnScrollListener()
{
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy)
    {
     super.onScrolled(recyclerView, dx, dy);

     //get firstCompleteleyVisibleItemPosition
     int firstCompleteleyVisibleItemPosition = managerMario.findFirstCompletelyVisibleItemPosition();

     if (firstCompleteleyVisibleItemPosition >= 0)
     {  
      //vertical one, smooth scroll to position
      rvLuigi.smoothScrollToPosition(firstCompleteleyVisibleItemPosition);
     }
    }

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState)
    {
     super.onScrollStateChanged(recyclerView, newState);
    }
});

4-) 输出

【讨论】:

  • 看起来很有希望,但只适用于一个RecyclerView。如果我将相同的OnScrollListener 添加到另一个,事情变得非常缓慢并且不再可能投掷。
  • 它有一些问题:底部的 RecyclerView 不与顶部的同步。在顶部投掷不会像在底部那样滚动和快速。 3. 尝试像在顶部一样在底部添加同步会导致其他滚动问题,不允许通过 flinguing 进行平滑滚动。另外,附注:请提供完整代码或使用与我编写的变量和其他内容相同的名称。你们每次都用 Java 写就够了,即使代码是在 Kotlin 中的(我每次都需要转换......)。
  • 这段代码有什么作用:return (boxStart + (boxEnd - boxStart) / 1000) - (viewStart + (viewEnd - viewStart) / 1000);?为什么要除以 1000?
  • @KalaBalik 好问题。文档在这里:developer.android.com/reference/android/support/v7/widget/…, int, int, int, int) 。可悲的是,即使在阅读之后,我也不明白这意味着什么。它返回确定视图是否应该可见所需的滚动量?!
  • 我想我误解了。我给出的示例仅在一个 recyclerview 同步另一个的情况下有效。 @KalaBalik 除以 1000 没有任何意义,我不小心发布了我尝试过的内容。我已经更新了它应该是什么。 boxStart 表示recyclerview 顶部的y 轴,viewStart 表示视图顶部的y 轴。 boxStart - viewStart 计算在顶部给定位置显示视图所需的滚动量。根据卷轴的方向,有时是负数,有时是正数。
【解决方案3】:

以 Burak 的 TopLinearLayoutManager 为基础,但更正了 OnScrollListener 的逻辑,我们终于可以进行平滑滚动和正确的捕捉(水平的 RecyclerView)。

public class MainActivity extends AppCompatActivity {
    View masterView = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity);
        final LayoutInflater inflater = LayoutInflater.from(this);
        final RecyclerView topRecyclerView = findViewById(R.id.topReccyclerView);
        RecyclerView.Adapter adapterTop = new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                return new ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false));
            }

            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                ((TextView) holder.itemView).setText(String.valueOf(position));
                holder.itemView.setBackgroundColor(position % 2 == 0 ? Integer.valueOf(0xffff0000) : Integer.valueOf(0xff00ff00));
            }

            @Override
            public int getItemCount() {
                return 100;
            }

            class ViewHolder extends RecyclerView.ViewHolder {
                final TextView textView;

                ViewHolder(View itemView) {
                    super(itemView);
                    textView = itemView.findViewById(R.id.textView);
                }
            }
        };
        topRecyclerView.setAdapter(adapterTop);

        final RecyclerView bottomRecyclerView = findViewById(R.id.bottomRecyclerView);
        RecyclerView.Adapter adapterBottom = new RecyclerView.Adapter() {
            int baseHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, getResources().getDisplayMetrics());

            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                return new ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false));
            }

            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                ((TextView) holder.itemView).setText(String.valueOf(position));
                holder.itemView.setBackgroundColor((position % 2 == 0) ? Integer.valueOf(0xffff0000) : Integer.valueOf(0xff00ff00));
                holder.itemView.getLayoutParams().height = baseHeight + (position % 3 == 0 ? 0 : baseHeight / (position % 3));
            }

            @Override
            public int getItemCount() {
                return 100;
            }

            class ViewHolder extends RecyclerView.ViewHolder {
                final TextView textView;

                ViewHolder(View itemView) {
                    super(itemView);
                    textView = itemView.findViewById(R.id.textView);
                }
            }
        };
        bottomRecyclerView.setAdapter(adapterBottom);

        TopLinearLayoutManager topLayoutManager = new TopLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL);
        topRecyclerView.setLayoutManager(topLayoutManager);
        TopLinearLayoutManager bottomLayoutManager = new TopLinearLayoutManager(this, LinearLayoutManager.VERTICAL);
        bottomRecyclerView.setLayoutManager(bottomLayoutManager);

        final OnScrollListener topOnScrollListener = new OnScrollListener(topRecyclerView, bottomRecyclerView);
        final OnScrollListener bottomOnScrollListener = new OnScrollListener(bottomRecyclerView, topRecyclerView);
        topRecyclerView.addOnScrollListener(topOnScrollListener);
        bottomRecyclerView.addOnScrollListener(bottomOnScrollListener);

        GravitySnapHelper snapHelperTop = new GravitySnapHelper(Gravity.START);
        snapHelperTop.attachToRecyclerView(topRecyclerView);
    }

    class OnScrollListener extends RecyclerView.OnScrollListener {
        private RecyclerView thisRecyclerView;
        private RecyclerView otherRecyclerView;
        int lastItemPos = Integer.MIN_VALUE;

        OnScrollListener(RecyclerView thisRecyclerView, RecyclerView otherRecyclerView) {
            this.thisRecyclerView = thisRecyclerView;
            this.otherRecyclerView = otherRecyclerView;
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                masterView = thisRecyclerView;
            } else if (newState == RecyclerView.SCROLL_STATE_IDLE && masterView == thisRecyclerView) {
                masterView = null;
                lastItemPos = Integer.MIN_VALUE;
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerview, int dx, int dy) {
            super.onScrolled(recyclerview, dx, dy);
            if ((dx == 0 && dy == 0) || (masterView != thisRecyclerView)) {
                return;
            }
            int currentItem = ((TopLinearLayoutManager) thisRecyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (lastItemPos == currentItem) {
                return;
            }
            lastItemPos = currentItem;
            otherRecyclerView.getLayoutManager().smoothScrollToPosition(otherRecyclerView, null, currentItem);
        }
    }
}

【讨论】:

  • 它有多个问题: 1. 顶部 recyclerView 左侧的小弹跳会在其捕捉结束时跳过一个单元格。 2.从顶部向左移动到单元格“0”,底部可能会到达“1”。 3. 顶部的滚动似乎不如底部快。 3.在底部,如果你快速飞到顶部,它会停在其他地方。有时可以勉强滚动。
  • OnScrolled(见edit)对地址 1. 和我相信 2. 的小改动。数字 3 是底部视图上更长的投掷距离和更高的投掷速度的结果。 4. 我无法复制。
  • 1.现在看来还可以。 2.你可以在顶部的RecyclerView上甩一甩,然后再次触摸它来停止它。 3. 应该是平滑的,就像在其他 RecyclerView 上一样。 4. 现在好像很少见了。尝试在底部时这样做。 5.如果你在底部的recyclerView(上/下)上一扔,然后通过触摸顶部停止它,它们就不再同步了。看看我得到了什么:i.stack.imgur.com/4qli9.png。 6.如果我在顶部的RecyclerView上一扔,向右,然后触摸底部的RecyclerView,可能会更糟:i.stack.imgur.com/ztsWw.png
【解决方案4】:

另一个在我的设备上运行良好的简单解决方案

变量

RecyclerView horizontalRecyclerView, verticalRecyclerView;
LinearLayoutManager horizontalLayoutManager, verticalLayoutManager;

ArrayList<String> arrayList = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();

RecyclerView代码

horizontalRecyclerView = findViewById(R.id.horizontalRc);
verticalRecyclerView = findViewById(R.id.verticalRc);

horizontalRecyclerView.setHasFixedSize(true);
verticalRecyclerView.setHasFixedSize(true);

horizontalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
verticalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);

horizontalRecyclerView.setLayoutManager(horizontalLayoutManager);
verticalRecyclerView.setLayoutManager(verticalLayoutManager);

for (int i = 0; i < 50; i++) {
     arrayList.add("" + i);
     arrayList2.add("" + i);
 }

 MyDataAdapter horizontalAdapter = new MyDataAdapter(this, arrayList);
 MyDataAdapter verticalAdapter = new MyDataAdapter(this, arrayList2);

 horizontalRecyclerView.setAdapter(horizontalAdapter);
 verticalRecyclerView.setAdapter(verticalAdapter);

内部逻辑RecyclerView.addOnScrollListener

 horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
                verticalLayoutManager.scrollToPositionWithOffset(pos, 20);

            }

        });


        verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
                horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);


            }
        });

希望对大家有所帮助

完整代码

public class Main4Activity extends AppCompatActivity {

    RecyclerView horizontalRecyclerView, verticalRecyclerView;
    LinearLayoutManager horizontalLayoutManager, verticalLayoutManager;

    ArrayList<String> arrayList = new ArrayList<>();
    ArrayList<String> arrayList2 = new ArrayList<>();

    boolean isVertical = true, isHorizontal = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main4);


        horizontalRecyclerView = findViewById(R.id.horizontalRc);
        verticalRecyclerView = findViewById(R.id.verticalRc);

        horizontalRecyclerView.setHasFixedSize(true);
        verticalRecyclerView.setHasFixedSize(true);

        horizontalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        verticalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);

        horizontalRecyclerView.setLayoutManager(horizontalLayoutManager);
        verticalRecyclerView.setLayoutManager(verticalLayoutManager);

        for (int i = 0; i < 50; i++) {
            arrayList.add("" + i);
            arrayList2.add("" + i);
        }

        MyDataAdapter horizontalAdapter = new MyDataAdapter(this, arrayList);
        MyDataAdapter verticalAdapter = new MyDataAdapter(this, arrayList2);

        horizontalRecyclerView.setAdapter(horizontalAdapter);
        verticalRecyclerView.setAdapter(verticalAdapter);

        horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
                verticalLayoutManager.scrollToPositionWithOffset(pos, 20);


            }

        });


        verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
                horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);


            }
        });



       /* horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
                verticalLayoutManager.scrollToPositionWithOffset(pos, 20);

                *//*if (isHorizontal) {
                    int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
                    verticalLayoutManager.scrollToPositionWithOffset(pos, 20);
                    Log.e("isHorizontal", "TRUE");
                    isVertical = false;
                } else {
                    isHorizontal = true;
                }*//*

            }

           *//* @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                isVertical = true;
            }*//*
        });


        verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            *//* @Override
             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                 super.onScrollStateChanged(recyclerView, newState);
                 isHorizontal = true;
             }
 *//*
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
                horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);

               *//* if (isVertical) {
                    int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
                    horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);
                    Log.e("isVertical", "TRUE");
                    isHorizontal = false;
                } else {
                    isVertical = true;
                }*//*


            }
        });*/


    }
}

适配器代码

public class MyDataAdapter extends RecyclerView.Adapter<MyDataAdapter.ViewHolder> {

    Context context;
    ArrayList<String> arrayList;


    public MyDataAdapter(Context context, ArrayList<String> arrayList) {
        this.context = context;
        this.arrayList = arrayList;
    }

    @Override
    public MyDataAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.temp, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MyDataAdapter.ViewHolder holder, int position) {

        if (position % 2 == 0) {
            holder.tvNumber.setBackgroundResource(R.color.colorGreen);
        } else {
            holder.tvNumber.setBackgroundResource(R.color.colorRed);
        }
        holder.tvNumber.setText(arrayList.get(position));
    }

    @Override
    public int getItemCount() {
        return arrayList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        TextView tvNumber;

        public ViewHolder(View itemView) {
            super(itemView);
            tvNumber = itemView.findViewById(R.id.tvNumber);
        }
    }
}

活动布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <android.support.v7.widget.RecyclerView
        android:id="@+id/horizontalRc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/verticalRc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="20dp"
        android:visibility="gone" />



</LinearLayout>    

临时布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="40dp">

    <TextView
        android:id="@+id/tvNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="50dp" />


</LinearLayout>

颜色

<color name="colorGreen">#307832</color>
<color name="colorRed">#ff4c4c</color>

【讨论】:

  • 20 是多少?
  • @androiddeveloper sir 调用scrollToPositionWithOffset(position, 20) 将布局使得 item[position] 的底部比 RecyclerView 的底部高 20 像素。来自文档developer.android.com/reference/android/support/v7/widget/… 的信息
  • 为什么要把它放在底部上方 20 像素处?
  • @androiddeveloper 先生,这只是为了测试用例,我放了 20 个像素,我们也可以设置 0 insetad of 20
  • 你能显示完整的代码吗?我记得我经历了很多问题,并且我已经尝试过 scrollToPositionWithOffset。它有一些问题阻止我使用它。
猜你喜欢
  • 1970-01-01
  • 2012-03-03
  • 2010-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-22
  • 1970-01-01
相关资源
最近更新 更多