【问题标题】:RecyclerView sets wrong MotionLayout state for its itemsRecyclerView 为其项目设置错误的 MotionLayout 状态
【发布时间】:2020-11-20 04:56:01
【问题描述】:

首先:我创建了一个显示此问题的示例项目。现在我开始认为这是 RecyclerView 或 MotionLayout 中的错误。

https://github.com/muetzenflo/SampleRecyclerView

这个项目的设置与下面描述的有点不同:它使用数据绑定在 MotionLayout 状态之间切换。但结果是一样的。只需切换状态并在项目之间滑动即可。迟早您会遇到具有错误 MotionLayout 状态的 ViewHolder。


所以主要问题是:

当从一种 MotionLayout 状态转换到另一种状态时,屏幕外的 ViewHolders 不会正确更新。


这就是问题所在/到目前为止我发现了什么:

我正在使用 RecyclerView。

它只有一种项目类型,即 MotionLayout(因此 RV 的每个项目都是 MotionLayout)。

这个 MotionLayout 有 2 个状态,我们称它们为 State big 和 State small

所有项目都应始终具有相同的状态。因此,每当状态从 big => small 切换时,从那时起所有项目都应该在 small 中。

但是发生的情况是状态更改为small,并且大多数(!)项目也正确更新。但是旧状态总是留下一两个项目。我很确定这与回收的 ViewHolders 有关。当使用下面的适配器代码(不在示例项目中)时,这些步骤会可靠地产生问题:

  1. 从第 1 项向右滑动到第 2 项
  2. big 更改为small
  3. small改回big
  4. 从第 2 项向左滑动到第 1 项 => 项目 1 现在处于 small 状态,但应该处于 big 状态

其他发现:

  • 在第 4 步之后,如果我继续向左滑动,还会有 1 个处于 small 状态的项目(可能是第 4 步中回收的 ViewHolder)。之后没有其他项目是错误的。

  • 从第 4 步开始,我继续滑动一些项目(比如说 10 个),然后一直向后滑动,不再有任何项目处于错误的 small 状态。错误的回收 ViewHolder 似乎随后被纠正。

我尝试了什么?

  • 我尝试在转换完成时致电notifyDataSetChanged()
  • 我尝试保留一组本地已创建的 ViewHolders 以直接在它们上调用转换
  • 我尝试使用数据绑定将 motionProgress 设置为 MotionLayout
  • 我尝试将viewHolder.isRecycable(true|false) 设置为在过渡期间阻止回收
  • 我搜索了 this great in-depth article about RVs 以获取下一步尝试的提示

有人遇到过这个问题并找到了好的解决方案吗?

只是为了避免混淆:bigsmall 并不表示我要折叠或展开每个项目!它只是motionlayouts子项的不同排列的名称。

class MatchCardAdapter() : DataBindingAdapter<Match>(DiffCallback, clickListener) {

    private val viewHolders = ArrayList<RecyclerView.ViewHolder>()

    private var direction = Direction.UNDEFINED

    fun setMotionProgress(direction: MatchCardViewModel.Direction) {
        if (this.direction == direction) return

        this.direction = direction

        viewHolders.forEach {
            updateItemView(it)
        }
    }

    private fun updateItemView(viewHolder: RecyclerView.ViewHolder) {
        if (viewHolder.adapterPosition >= 0) {
            val motionLayout = viewHolder.itemView as MotionLayout
            when (direction) {
                Direction.TO_END -> motionLayout.transitionToEnd()
                Direction.TO_START -> motionLayout.transitionToStart()
                Direction.UNDEFINED -> motionLayout.transitionToStart()
            }
        }
    }

    override fun onBindViewHolder(holder: DataBindingViewHolder<Match>, position: Int) {
        val item = getItem(position)
        holder.bind(item, clickListener)

        val itemView = holder.itemView
        if (itemView is MotionLayout) {
            if (!viewHolders.contains(holder)) {
                viewHolders.add(holder)
            }

            updateItemView(holder)
        }
    }

    override fun onViewRecycled(holder: DataBindingViewHolder<Match>) {
        if (holder.adapterPosition >= 0 && viewHolders.contains(holder)) {
            viewHolders.remove(holder)
        }
        super.onViewRecycled(holder)
    }
}

【问题讨论】:

  • 你能分享你的 onBindViewHolderMethod 吗?
  • @Rinat 我添加了适配器的重要部分。这是我目前的实现。
  • 我花了几个小时尝试不同的方法,但都失败了。也许,您应该开始寻找 MotionLayout 的替代方案来实现您的状态。
  • 谢谢,我走了同样的路 :) 我可能很快会在 google tracker 中发布问题。
  • 我在 google tracker 上创建了一个问题:issuetracker.google.com/issues/162811653

标签: android android-recyclerview android-motionlayout


【解决方案1】:

我取得了一些进展,但这不是最终解决方案,它有一些怪癖需要完善。就像从头到尾的动画不能正常工作一样,它只是跳到了最终的位置。

https://github.com/fmatosqg/SampleRecyclerView/commit/907ec696a96bb4a817df20c78ebd5cb2156c8424

我更改的一些内容与解决方案无关,但有助于找到问题:

  • 持续时间为 1 秒
  • 回收站视图中的更多项目
  • recyclerView.setItemViewCacheSize(0) 尽量减少看不见的项目,但如果你仔细跟踪它,你就会知道它们往往会留下来
  • 消除了用于处理转换的数据绑定。因为我一般不信任视图持有者,所以如果没有不良副作用,我永远无法让它们工作
  • 使用implementation "androidx.constraintlayout:constraintlayout:2.0.0-rc1"升级约束库

详细说明是什么让它工作得更好:

所有对动画布局的调用都以post 的方式完成

    //    https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
    fun safeRunBlock(block: () -> Unit) {

        if (ViewCompat.isLaidOut(motionLayout)) {
            block()
        } else {
            motionLayout.post(block)
        }

    }

比较实际与期望的属性

    val goalProgress =
            if (currentState) 1f
            else 0f
    val desiredState =
                if (currentState) motionLayout.startState
                else motionLayout.endState

 safeRunBlock {
            startTransition(currentState)
        }
 
 if (motionLayout.progress != goalProgress) {

    if (motionLayout.currentState != desiredState) {

        safeRunBlock {
            startTransition(currentState)
        }

    }
 }

这将是部分解决方案的完整类


class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
    RecyclerView.ViewHolder(binding.root) {

    val motionLayout: MotionLayout =
        binding.root.findViewById<MotionLayout>(R.id.root_item_recycler_view)
            .also {
                it.setTransitionDuration(1_000)
                it.setDebugMode(DEBUG_SHOW_PROGRESS or DEBUG_SHOW_PATH)
            }

    var lastPosition: Int = -1


    fun bind(item: T, position: Int, layoutState: Boolean) {

        if (position != lastPosition)
            Log.i(
                "OnBind",
                "Position=$position lastPosition=$lastPosition - $layoutState "
            )



        lastPosition = position

        setMotionLayoutState(layoutState)

        binding.setVariable(BR.item, item)
        binding.executePendingBindings()
    }

    //    https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
    fun safeRunBlock(block: () -> Unit) {

        if (ViewCompat.isLaidOut(motionLayout)) {
            block()
        } else {
            motionLayout.post(block)
        }

    }

    fun setMotionLayoutState(currentState: Boolean) {
        
        val goalProgress =
            if (currentState) 1f
            else 0f

        safeRunBlock {
            startTransition(currentState)
        }

        if (motionLayout.progress != goalProgress) {

            val desiredState =
                if (currentState) motionLayout.startState
                else motionLayout.endState

            if (motionLayout.currentState != desiredState) {
                Log.i("Pprogress", "Desired doesn't match at position $lastPosition")
                safeRunBlock {
                    startTransition(currentState)
                }
            }
        }
    }

    fun startTransition(currentState: Boolean) {
        if (currentState) {
            motionLayout.transitionToStart()
        } else {
            motionLayout.transitionToEnd()
        }
    }

}

编辑:添加约束布局版本

【讨论】:

  • 感谢您的工作!我周末去看看。
  • 您好,我查看了您的解决方案,safeRunBlock 是一个不错的方法。闪烁来自notifyDataSetChanged()。我在测试中遇到了同样的问题。这个问题还没有回答,但我赞成你的回答,所以如果别人给你第二次赞成票,你应该隐含地得到界限。
猜你喜欢
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-24
  • 2020-08-31
  • 1970-01-01
  • 1970-01-01
  • 2021-04-01
相关资源
最近更新 更多