如何同步2个RecyclerViews的滚动第一个位置?

and*_*per 13 android android-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>
Run Code Online (Sandbox Code Playgroud)

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"/>
Run Code Online (Sandbox Code Playgroud)

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"/>
Run Code Online (Sandbox Code Playgroud)

主要活动

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

问题

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

  2. 如何修改代码以支持平滑滚动,而不会导致突然让其他RecyclerView成为控件的问题?


编辑:在更新这里的示例代码后,具有不同大小的单元格(因为最初更接近我所拥有的问题,如前所述),我注意到捕捉不起作用.

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

https://github.com/DevExchanges/SnappingRecyclerview

因此,我使用'GravitySnapHelper'代替LinearSnapHelper.似乎工作得更好,但仍然有同步问题,并在滚动时触摸.


编辑:我终于修复了所有同步问题,即使单元格大小不同,它也可以正常工作.

还有一些问题:

  1. 如果您使用一个RecyclerView,然后触摸另一个,它会有非常奇怪的滚动行为.可能滚动的方式比它应该更多.

  2. 滚动不平滑(同步和投掷时),因此看起来不太好.

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

在此输入图像描述

我甚至不认为底部的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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

kal*_*lik 3

结合这两个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);
    }
Run Code Online (Sandbox Code Playgroud)

当然,您可以组合两个 if 子句,因为主体是相同的。为了可读性,我认为将它们分开是很好的。

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

if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
    if (masterView == otherRecyclerView) {
        thisRecyclerView.stopScroll();
        otherRecyclerView.stopScroll();
        syncScroll(1, 1);
    }
    masterView = thisRecyclerView;
}
Run Code Online (Sandbox Code Playgroud)

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