Android Kotlin中RecyclerView动画问题

4

我已经在recyclerview中添加了一个动画来展示下面的过渡效果。

当我长按某个项目时,它会显示单选按钮并将项目卡片向右移动。问题是,在初始选择后,每当我点击或选择其他项目时,item6和其以下的项目会再次进行动画处理。

有人能解释一下为什么会这样,并告诉我如何修复这个问题吗?

enter image description here

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?它们都会在更新单个项目的状态时被调用。你有没有尝试删除其中的一部分?让我知道。 - A S M Sayem
使用notifyDataSetChanged()的原因是通知所有项目它们应该显示选择复选框。然后,在整个recyclerview项目中所有复选框都可见之后,用户按下一个随机项目。我需要通知该项目已被选中以显示复选框,为此我使用notifyItemChanged。 - pepperlove
但是这两个语句强制更新所有项目,但您的视图持有者通常只持有5个项目,您应该仅更新更新其视图的项目位置。调用notifyDataSetChanged对于此解决方案而言变得非常昂贵。如果您在GitHub上拥有此项目,请分享GitHub公共链接,以便我可以正确检查它。 - A S M Sayem
4个回答

1
你可以重写getItemViewType(int position)方法,并返回视图的id,该id始终是唯一的。
@Override
public int getItemViewType(int position) {
    return position;
}

或者

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

由于Android系统将每个布局存储为“R”(资源)类中的整数静态引用,因此我们只需返回要在onCreateViewHolder()方法中使用的布局资源ID即可。


1

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

更新

如评论中所述,第6个项目被动画化的原因可能是RecyclerView保持的默认ViewPool(5个项目)。第6个视图不是其中的一部分,因此它会被重新绑定、重新显示和重新动画化。

我会这样做:

  1. 我能否摆脱notifyDataSetChanged()?为什么要调用它?
  2. 我是否可以利用 RecyclerView-Selection,因为它是Google库,他们建议我们使用它?它会有更少的“自定义”代码的好处。
除此之外,您可以尝试根据评论中Pawel的建议增加RecycledViewPool。请记住,这可能会被视为代码异味,因为不同的分辨率、密度、屏幕大小等可能会影响其在运行时的行为;这可能会很不稳定并容易失败,但根据您特定的用例,现在可能允许您摆脱它

你能解释一下为什么动画只在初始选择后的item6及其以下运行吗? - pepperlove
我不能不运行和调试,但我的猜测是适配器认为/推断那些ViewHolders在调用notifyDataSetChanged后已经处于正确的状态。记住这一切都是异步的。 - Martin Marconcini
你能提供一些可能的解决方案吗,以便我可以修复这个问题吗? - pepperlove
是的,就像^所说的那样。我的意思是,很有可能前5个视图持有者没有被改变和缓存,因此它们不会从适配器接收到任何分派的更改。 - Martin Marconcini
@MartinMarconcini,我使用notifyDataSetChanged()的原因是在选择一个项目时显示每个项目中的复选框。因此,即使我使用recyclerview-selection,我还需要再次使用notifyDataSetChanged(),以显示所有复选框,对吗? - pepperlove
显示剩余2条评论

0

这有点像是一个hack,但你考虑过使用吗?

holder.setIsRecyclable(false);

如果列表不是很大并且可以解决问题,它可以作为一个快速解决方案使用。

1
我不能使用这个,列表太大了。 - pepperlove

0

在绑定适配器的方法中,尝试使用holder.getAdapterPosition()代替position,这样对于所有点击操作都是一样的。

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

这似乎不起作用。你能分享一下你采取这种方法的想法吗? - pepperlove
请阅读此博客内容:https://proandroiddev.com/difference-between-position-getadapterposition-and-getlayoutposition-in-recyclerview-80279a2711d1 - KGeeks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接