android - 如何在 RecyclerView 中很好地为 CardView 扩展和收缩设置动画(在性能和视觉方面)?
问题描述
我设法创建了一个自动布局更新。我有一个列表CardView
显示在RecyclerView
. 单击 后CardView
,表示CardView
将展开并导致其他可能展开CardView
的内容缩回。我设法做到了这一点:
- 维护一个变量,该变量保存要在 my的适配器
CardView
内扩展的位置RecyclerView
- 当点击发生在任何
CardView
- 如果另一个
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
不会产生问题):
如果您仔细观察,在缩回时,当CardView
' 的高度完全缩回时,文本部分可见。理想情况下,我希望文本不可见(alpha 设置为 0)更快(可能是 10 毫秒)。因此,我的问题是:
- 如何
CardView
在扩展另一个CardView
并调整 alpha 值的持续时间的同时平滑缩回?(从 gif 中可以看出,问题在于动画在视觉上没有吸引力。) notifyItemChanged
除了使用重新触发之外,还有更好的方法来实现我想要的简单动画onBindViewHolder
吗?
这是我所说的文本可见性部分可见的更清晰的图片:
解决方案
好吧,可见性动画有一些棘手的情况。例如,您可以查看这篇文章。 布局动画在第一次运行时不起作用
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>
推荐阅读
- angular - 使用编码为 multipart/form-data 的数据处理 POST 请求
- swift - 如何使用 UIButton 刷新静态表中的 UITextField 和 UILabel
- heroku - 我的 vue.js + Sails.js 应用程序无法在 Heroku 上运行
- mysql - 在单个分区中查询记录非常慢
- chef-infra - 使用打包程序覆盖厨师客户端中的角色属性
- c# - 我可以使用 log4net 或 Nlog 来捕获 sql 查询错误吗
- python - 如何在 Azure Redhat Linux VM 上更新 python?
- git - '-text' 或 'text eol=lf' 是否更适合 .gitattributes 中的 *.sh 文件?
- c++ - CRTP 共享指针的 C++ 向量
- python - 如何更改 read_csv 对空值的处理