如何同步2个RecyclerView的滚动首位? [英] How to sync scrolling first-positions of 2 RecyclerViews?

查看:56
本文介绍了如何同步2个RecyclerView的滚动首位?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

我有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)
        }
    }
}

解决方案

将两个RecyclerView结合起来,有四种移动情况:

a.将卧式回收机向左滚动

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)为真.然后,两个回收站都停止运行,并且另一个"回收站与这个"回收站同步.

Background

I have 2 RecyclerView instances. One is horizontal, and the second is vertical.

They both show the same data and have the same amount of items, but in different ways, and the cells are not necessary equal in size through each of them .

I wish that scrolling in one will sync with the other, so that the first item that's shown on one, will always be shown on the other (as the first).

The problem

Even though I've succeeded making them sync (I just choose which one is the "master", to control the scrolling of the other), the direction of the scrolling seems to affect the way it works.

Suppose for a moment the cells have equal heeight:

If I scroll up/left, it works as I intended, more or less:

However, if I scroll down/right, it does let the other RecyclerView show the first item of the other, but usually not as the first item:

Note: on the above screenshots, I've scrolled in the bottom RecyclerView, but a similar result will be with the top one.

The situation gets much worse if, as I wrote, the cells have different sizes:

What I've tried

I tried to use other ways of scrolling and going to other positions, but all attempts fail.

Using smoothScrollToPosition made things even worse (though it does seem nicer), because if I fling, at some point the other RecyclerView takes control of the one I've interacted with.

I think I should use the direction of the scrolling, together with the currently shown items on the other RecyclerView.

Here's the current (sample) code. Note that in the real code, the cells might not have equal sizes (some are tall, some are short, etc...). One of the lines in the code makes the cells have different height.

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>

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

The questions

  1. How do I let the other RecycerView to always have the first item the same as the currently controlled one?

  2. How to modify the code to support smooth scrolling, without causing the issue of suddenly having the other RecyclerView being the one that controls ?


EDIT: after updating the sample code here with having different sizes of cells (because originally that's closer to the issue I have, as I described before), I noticed that the snapping doesn't work well.

That's why I chose to use this library to snap it correctly:

https://github.com/DevExchanges/SnappingRecyclerview

So instead of LinearSnapHelper, I use 'GravitySnapHelper'. Seems to work better, but still have the syncing issues, and touching while it scrolls.


EDIT: I've finally fixed all syncing issues, and it works fine even if the cells have different sizes.

Still has some issues:

  1. If you fling on one RecyclerView, and then touch the other one, it has very weird behavior of scrolling. Might scroll way more than it should.

  2. The scrolling isn't smooth (when syncing and when flinging), so it doesn't look well.

  3. Sadly, because of the snapping (which I actually might need only for the top RecyclerView), it causes another issue: the bottom RecyclerView might show the last item partially (screenshot with 100 items), and I can't scroll more to show it fully :

I don't even think that the bottom RecyclerView should be snapping, unless the top one was touched. Sadly this is all I got so far, that has no syncing issues.

Here's the new code, after all the fixes I've found:

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

解决方案

Combining the two RecyclerViews, there are four cases of movement:

a. Scrolling the horizontal recycler to the left

b. Scrolling it to the right

c. Scrolling the vertical recycler to the top

d. Scrolling it to the bottom

Cases a and c don't need to be taken care of since they work out of the box. For cases b and d you need to do two things:

  1. Know which recycler you are in (vertical or horizontal) and which direction the scroll went (up or down resp. left or right) and
  2. calculate an offset (of list items) from the number of visible items in otherRecyclerView (if the screen is bigger the offset needs to be bigger, too).

Figuring this out was a bit fiddly, but the result is pretty straight forward.

    @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);
    }

Of course you could combine the two if clauses since the bodies are the same. For the sake of readability, I thought it is good to keep them apart.

The second problem was syncing when the respective "other" recycler was touched while the "first" recycler was still scrolling. Here the following code (included above) is relevant:

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

newState equals SCROLL_STATE_DRAGGING when the recycler is touched and dragged a little bit. So if this is a touch (& drag) after a touch on the respective "other" recycler, the second condition (masterView == otherRecyclerview) is true. Both recyclers are stopped then and the "other" recycler is synced with "this" one.

这篇关于如何同步2个RecyclerView的滚动首位?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆