首页 > 解决方案 > 如何在 RecyclerView 中很好地为 CardView 扩展和收缩设置动画(在性能和视觉方面)?

问题描述

我设法创建了一个自动布局更新。我有一个列表CardView显示在RecyclerView. 单击 后CardView,表示CardView将展开并导致其他可能展开CardView的内容缩回。我设法做到了这一点:

执行上述操作的代码如下所示:

// Inside RecyclerView.Adapter
private var expandedViewHolder = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val cardView = LayoutInflater.from(parent.context)
        .inflate(R.layout.recyclerview_cardview, parent, false) as CardView
    val holder = ViewHolder(cardView)

    cardView.setOnClickListener {
        notifyItemChanged(expandedPosition)
        expandedPosition = if (expandedPosition == holder.adapterPosition) -1 else holder.adapterPosition
        notifyItemChanged(expandedPosition)
    }
    return holder
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.headline.text = dataset[position].name
    holder.desc.text = dataset[position].desc
    if (position == expandedPosition) {
        holder.desc.visibility = View.VISIBLE
    }
    else {
        holder.desc.visibility = View.GONE
    }
}

可以看出,我正在强制RecyclerView重新触发onBindViewHolder使用notifyItemChanged(). 在内部onBindViewHolder, aCardView的描述的可见性将相应地改变。onBindViewHolder使用自动过渡/动画自动重绘新布局。

但是,当 a 被展开而另一个被收回时,收回会有一个小问题,CardView如下所示(请注意,展开和收回一个CardView不会产生问题):

示例 GIF

如果您仔细观察,在缩回时,当CardView' 的高度完全缩回时,文本部分可见。理想情况下,我希望文本不可见(alpha 设置为 0)更快(可能是 10 毫秒)。因此,我的问题是:

这是我所说的文本可见性部分可见的更清晰的图片:

更清晰的画面

标签: androidandroid-layoutandroid-animationandroid-recyclerview

解决方案


好吧,可见性动画有一些棘手的情况。例如,您可以查看这篇文章。 布局动画在第一次运行时不起作用

android:animateLayoutChanges="true"

大多数时候 animateLayoutChanges 就足够了。但是在 recyclerView 中,开发人员应该恢复 viewHolder 的先前状态,因为 recyclerView 重用了视图持有者,所以如果你不根据位置检查视图持有者的真实状态,它可能是错误的(例如,两个不同的位置使用同一个持有者,其中一个展开另一个倒塌)。现在有一个新问题,如果您使用动画布局更改,没有动画就没有简单的方法来展开或折叠描述文本。您可以尝试禁用、启用父布局转换,但它对我不起作用。

另一种方法是将 textViews 高度 0 更改为 TextView.getHeight() 或相反,但在这种情况下,您不知道 textviews 高度,因为它的高度为 0,除非用户单击它,否则您不想扩展它。但是你可以找到 TextView 想要的高度。这是我的工作代码。请让我知道它是否对您有帮助。

class Adapter : RecyclerView.Adapter<Adapter.Holder>() {

    private var expandedHolderPosition = -1

    private var itemList = emptyList<Item>()

    private var getViewHolder: (position: Int) -> Holder? = { null }

    override fun getItemCount() = itemList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val inflatedView = parent.inflate(R.layout.item, false)
        return Holder(inflatedView)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val currentItem = itemList[position]
        holder.bindItem(currentItem)
    }

    fun updateList(items: List<Item>) {
        this.expandedHolderPosition = -1
        this.itemList = items
        this.notifyDataSetChanged()
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        getViewHolder = { position ->
            recyclerView.findViewHolderForAdapterPosition(position) as? Holder
        }
    }

    inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val textTitle = itemView.textTitle
        private val textDescription = itemView.textDescription
        private var desiredTextViewHeight = 0
        private val isExpanded get() = adapterPosition == expandedHolderPosition


        fun bindItem(item: Item) {

            textTitle.text = item.title.toString()

            textDescription.setText(item.description)

            textDescription.post {

                desiredTextViewHeight = with(textDescription) {
                    lineHeight * lineCount + layout.bottomPadding - layout.topPadding
                }

                if (isExpanded) {
                    showDescription(false)
                } else {
                    hideDescription(false)
                }
            }

            itemView.setOnClickListener {

                val position = adapterPosition

                //Its in open state close it.
                if (isExpanded) {
                    expandedHolderPosition = -1
                    hideDescription(true)
                } else {
                    getViewHolder(expandedHolderPosition)?.hideDescription(true)
                    showDescription(true)
                    expandedHolderPosition = position
                }
            }
        }

        private fun hideDescription(animate: Boolean) {
            logDebug("hideDescription")
            if (animate) {
                changeHeightWithAnimation(desiredTextViewHeight, 0)
            } else {
                updateDescriptionHeight(0)
            }
        }

        private fun showDescription(animate: Boolean) {
            logDebug("showDescription")
            if (animate) {
                changeHeightWithAnimation(0, desiredTextViewHeight)
            } else {
                updateDescriptionHeight(desiredTextViewHeight)
            }
        }

        private fun changeHeightWithAnimation(from: Int, to: Int) {
            val animator = ValueAnimator.ofInt(from, to)
            animator.duration = 300
            animator.addUpdateListener { animation: ValueAnimator ->
                updateDescriptionHeight(animation.animatedValue as Int)
            }
            animator.start()
        }

        private fun updateDescriptionHeight(newHeight: Int) {
            textDescription.updateLayoutParams<ViewGroup.LayoutParams> {
                height = newHeight
            }
        }

    }
}

data class Item(val title: Int, @StringRes val description: Int = R.string.lorem)

用户项目的布局文件。

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textTitle"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:gravity="center_vertical"
            tools:text="Title" />

        <TextView
            android:id="@+id/textDescription"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:gravity="center_vertical"
            tools:layout_height="wrap_content"
            tools:text="Description" />

    </LinearLayout>

</com.google.android.material.card.MaterialCardView>

推荐阅读