RecyclerView 动画问题 Android Kotlin

Posted

技术标签:

【中文标题】RecyclerView 动画问题 Android Kotlin【英文标题】:RecyclerView animation issue Android Kotlin 【发布时间】:2021-10-13 00:35:16 【问题描述】:

我在 recyclerview 中添加了一个动画来显示下面的过渡。

当我长按一个项目时,它会显示单选按钮并且项目卡会向右移动。问题是,在初始选择后,当我单击或选择其他项目时,item6 和下面的项目再次动画。

谁能解释为什么会发生这种情况以及我该如何解决这个问题。

ListAdapter.kt:

class ListItemAdapter(private val values: List<PlaceholderContent.PlaceholderItem>
) : RecyclerView.Adapter<ListItemAdapter.ItemViewHolder>() 

    private lateinit var itemClick: OnItemClick
    private var selectedIndex: Int = -1
    private var selectedItems: SparseBooleanArray = SparseBooleanArray()
    private var isActive: Boolean = false
    private var activateAnimation: Boolean = false

    fun setItemClick(itemClick: OnItemClick) 
        this.itemClick = itemClick
    

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ListItemAdapter.ItemViewHolder 
        val view = LayoutInflater.from(parent.context).inflate(
            R.layout.fragment_item,
            parent,
            false
        )
        return ItemViewHolder(view)
    

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) 
        holder.itemView.apply 
            findViewById<TextView>(R.id.item_number).text = values[position].id
            findViewById<TextView>(R.id.content).text = values[position].content
        

        holder.itemView.findViewById<CardView>(R.id.list_item).setOnClickListener 
            itemClick.onItemClick(values[position], position)
        

        holder.itemView.findViewById<CardView>(R.id.list_item).setOnLongClickListener 
            itemClick.onLongPress(values[position], position)
            true
        

        toggleIcon(holder, position)
    

    override fun getItemCount(): Int 
        return values.size
    
    
    private fun itemTransition(holder: ItemViewHolder)
        val animator = ObjectAnimator.ofFloat(holder.itemView.findViewById(R.id.list_item), View.TRANSLATION_X, 150f)
        animator.start()
    

    private fun itemTransitionBack(holder: ItemViewHolder)
        val animator = ObjectAnimator.ofFloat(holder.itemView.findViewById(R.id.list_item), View.TRANSLATION_X, 0f)
        animator.start()
    


    fun toggleIcon(holder: ItemViewHolder, position: Int)
        val checkBox = holder.itemView.findViewById<RadioButton>(R.id.is_selected)
        if(selectedItems.get(position, false))
            checkBox.isGone = false
            checkBox.isChecked = true
        
        else
            checkBox.isGone = true
            checkBox.isChecked = false
        
        if(isActive) checkBox.isGone = false

        if(activateAnimation)
            itemTransition(holder)
        
        else
            itemTransitionBack(holder)

        if(selectedIndex == position) selectedIndex = - 1
    

    fun selectedItemCount() = selectedItems.size()

    fun toggleSelection(position: Int)

        selectedIndex = position
        if (selectedItems.get(position, false))
            selectedItems.delete(position)
        else 
            selectedItems.put(position, true)
        
        notifyItemChanged(position)

        isActive = selectedItems.isNotEmpty()
        activateAnimation = selectedItems.isNotEmpty()
        notifyDataSetChanged()
    

    fun clearSelection()
        selectedItems.clear()
        notifyDataSetChanged()
    

    interface OnItemClick 
        fun onItemClick(item: PlaceholderContent.PlaceholderItem, position: Int)
        fun onLongPress(item: PlaceholderContent.PlaceholderItem, position: Int)
    

    inner class ItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
    


ItemFragment.kt

adapter = ListItemAdapter(PlaceholderContent.ITEMS)
val recyclerViewList = view.findViewById<RecyclerView>(R.id.list)
recyclerViewList.adapter = adapter
recyclerViewList.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
val myHelper = ItemTouchHelper(myCallback)
myHelper.attachToRecyclerView(recyclerViewList)

adapter.setItemClick(object : ListItemAdapter.OnItemClick
    override fun onItemClick(
        item: PlaceholderContent.PlaceholderItem,
        position: Int
    ) 
        if(adapter.selectedItemCount() > 0)
            toggleSelection(position)
    

    override fun onLongPress(
        item: PlaceholderContent.PlaceholderItem,
        position: Int
    ) 
        toggleSelection(position)
    

)

private fun toggleSelection(position: Int)
    adapter.toggleSelection(position)

【问题讨论】:

您能解释一下为什么在toggleSelection(position: Int) 方法中同时使用notifyItemChangednotifyDataSetChanged 吗?他们每次都在调用更新单个项目的状态。你检查过删除那部分吗?告诉我 使用 notifyDataSetChanged() 的原因是通知所有项目它们应该显示复选框以供选择。然后在整个 recyclerview 项目中所有复选框都可见并且用户按下随机项目之后。我需要通知该项目以显示复选框已选中,为此我使用 notifyItemChanged。 但这两个语句强制每个项目都更新,但您的视图持有者当前正常持有 5 个项目,您应该只更新正在更新其视图的项目位置。调用 notifyDataSetChanged 成为此解决方案的昂贵语句。如果你在 GitHub 上有这个项目,请分享 GitHub 公开链接,以便我正确查看。 【参考方案1】:

您可以覆盖getItemViewType(int position),这将返回始终唯一的视图ID。

@Override
public int getItemViewType(int position) 
    return position;

@Override
public int getItemViewType(final int position) 
    return R.layout.fragment_item;

由于 android 系统将每个布局的静态引用存储为“R”(资源)类中的 Integer,我们可以简单地返回布局资源 id 以在 onCreateViewHolder() 方法中使用。

【讨论】:

【参考方案2】:

您在adapter.toggleSelection(position) 中调用notifyDataSetChanged(),无论此位置是否已更新,这都会重新绑定所有可见视图(并再次运行动画)。

更新

如 cmets 中所述,第 6 项动画的原因很可能是由于 RecyclerView 保留的默认 ViewPool(5 项)。第 6 个视图不是其中的一部分,因此它会重新绑定、重新显示,并且...重新设置动画。

我会做的是:

    我可以摆脱notifyDataSetChanged() 吗?你为什么这么叫? 我可以利用RecyclerView-Selection,因为它是一个谷歌图书馆,他们建议我们使用什么?这样做的好处是更少的“自定义”代码。

除此之外,您可以尝试按照 cmets 中 Pawel 的建议增加 RecycledViewPool。请记住,这可能会被视为代码异味,因为不同的分辨率、密度、屏幕尺寸等可能会影响它在运行时的行为;这会很不稳定并且容易失败,但根据您的特定用例,可能会让您暂时摆脱它

【讨论】:

你能解释一下为什么动画在最初选择后只在item6及以下运行吗? 我不能不运行它和调试,但我的猜测是适配器认为/认为那些 ViewHolders 在调用 notifyDataSetChanged 后已经处于正确状态。请记住,这在某种程度上都是异步的。 您能否提出任何可能的解决方案来解决这个问题? 这是在没有项目在前面/运行/尝试 20000 件事情的情况下很难弄清楚的问题之一。我的第一个建议是:你能摆脱notifyDataSetChanged 吗?来自 Google 的 the RecyclerView-Selection 课程能否帮助您使用“较少定制”的解决方案? (只是折腾想法)。 RecyclerView.RecycledViewPool 默认大小为 5,您可以通过增加该数量来进行粗略的修复。您还可以尝试设置适配器的 setHasStableIds 并覆盖 getItemId。【参考方案3】:

这是一种 hack,但你考虑过使用吗?

holder.setIsRecyclable(false);

如果列表不会很大并且如果它解决了问题,则可以用作快速解决方案。

【讨论】:

这个用不了,列表真的很大。【参考方案4】:

代替 position 尝试使用 holder.getAdapterPosition() 与您在适配器的绑定方法中执行的所有点击操作相同,

holder.itemView.findViewById<CardView>(R.id.list_item).setOnClickListener 
            itemClick.onItemClick(values[position], holder.getAdapterPosition())
        

        holder.itemView.findViewById<CardView>(R.id.list_item).setOnLongClickListener 
            itemClick.onLongPress(values[position], holder.getAdapterPosition())
            true
        

        toggleIcon(holder, holder.getAdapterPosition())

【讨论】:

这似乎不起作用。您能分享一下您对这种方法的看法吗? 请浏览本博客内容proandroiddev.com/…

以上是关于RecyclerView 动画问题 Android Kotlin的主要内容,如果未能解决你的问题,请参考以下文章

Android RecyclerView - 触摸时动画项目提升

Android Kotlin:如何从 recyclerview 中成功删除项目、调用 notifyItemRangeChanged() 和动画

Android番外篇 RecyclerView 移除飞行效果动画

Android RecyclerView的notify方法和动画的刷新详解

Android-->RecyclerView 显示底部,滚动底部(无动画)

Android L 中的 RecyclerView ItemAnimator 故障