草庐IT

android - 如何同步 2 个 RecyclerView 的滚动首位?

coder 2023-11-19 原文

背景

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

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

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

问题

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

暂时假设单元格具有相同的高度:

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

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

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

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

我尝试过的

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

使用 smoothScrollToPosition 使事情变得更糟(尽管它看起来确实更好),因为如果我猛扑过去,在某个时候另一个 RecyclerView 会控制我与之交互的 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"/>

主 Activity

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 个项目的屏幕截图),我不能不要滚动更多以完整显示它:

我什至不认为底部的 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 和 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) 为真。然后两个回收器都停止,“其他”回收器与“这个”回收器同步。

关于android - 如何同步 2 个 RecyclerView 的滚动首位?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47282276/

有关android - 如何同步 2 个 RecyclerView 的滚动首位?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐