Recycler View 中的约束设置动画未正确设置动画
Posted
技术标签:
【中文标题】Recycler View 中的约束设置动画未正确设置动画【英文标题】:Constraint Set animation inside Recycler View not animating properly 【发布时间】:2019-07-28 18:04:44 【问题描述】:我正在为我的回收站视图项使用约束布局。要为它们设置动画(展开/折叠),我使用约束集动画。开场动画在所有项目上运行良好。结束动画也可以正常运行,但是当结束动画在不是最后一个的项目上开始时,所有项目都会在动画开始时跳起来,而不是在动画结束时。
在项目点击时执行动画:
itemView.setOnClickListener
val smallItemConstraint = ConstraintSet()
smallItemConstraint.clone(itemView.context, R.layout.day_of_week_small)
val largeItemConstraint = ConstraintSet()
largeItemConstraint.clone(itemView.context, R.layout.day_of_week)
val constraintToApply = if (isViewExpanded) smallItemConstraint else
largeItemConstraint
animateItemView(constraintToApply, itemView.dayOfWeekConstraintLayout)
if (!isViewExpanded)
itemView.dayOfWeekWeatherIcon.visibility = View.VISIBLE
else
itemView.dayOfWeekWeatherIcon.visibility = View.GONE
isViewExpanded = !isViewExpanded
animateItemView 在哪里:
private fun animateItemView(constraintToApply: ConstraintSet,
constraintLayout: ConstraintLayout)
TransitionManager.beginDelayedTransition(constraintLayout)
constraintToApply.applyTo(constraintLayout)
day_of_week.xml(扩展)布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dayOfWeekConstraintLayout"
android:layout_
android:layout_>
<ImageView
android:id="@+id/dayOfWeekWeatherIcon"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:contentDescription="@string/weather_image"
app:layout_constraintBottom_toBottomOf="@+id/dayOfWeekHumidityLabel"
app:layout_constraintEnd_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/dayOfWeekText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/today"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dayOfWeekItemVerticalGuideline"
android:layout_
android:layout_
android:orientation="vertical"
app:layout_constraintGuide_begin="192dp" />
<TextView
android:id="@+id/dayOfWeekCurrentTemperatureText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAllCaps="true"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekText" />
<TextView
android:id="@+id/dayOfWeekDegreeCelsiusSign"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/degree_celsius"
android:textAllCaps="true"
android:textSize="24sp"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekCurrentTemperatureText"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekText" />
<TextView
android:id="@+id/dayOfWeekWeatherStateText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/weather_state_text"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekDegreeCelsiusSign" />
<TextView
android:id="@+id/dayOfWeekWindLabel"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/wind_label"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekHumidityLabel"
android:layout_
android:layout_
android:layout_margin="8dp"
android:text="@string/humidityLabel"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWindLabel" />
<TextView
android:id="@+id/dayOfWeekWindDirection"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindLabel"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekWindSpeed"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindDirection"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekWindSpeedLabel"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/wind_speed"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindSpeed"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekHumidityText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekHumidityLabel"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWindSpeedLabel" />
<TextView
android:id="@+id/dayOfWeekHumidityPercentageLabel"
android:layout_
android:layout_
android:layout_margin="8dp"
android:text="@string/percentage_sign"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekHumidityText"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWindSpeedLabel" />
</androidx.constraintlayout.widget.ConstraintLayout>
还有day_of_week_small.xml(折叠)布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dayOfWeekConstraintLayout"
android:layout_
android:layout_>
<ImageView
android:id="@+id/dayOfWeekWeatherIcon"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:contentDescription="@string/weather_image"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/dayOfWeekHumidityLabel"
app:layout_constraintEnd_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/dayOfWeekText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/today"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dayOfWeekItemVerticalGuideline"
android:layout_
android:layout_
android:orientation="vertical"
app:layout_constraintGuide_begin="192dp" />
<TextView
android:id="@+id/dayOfWeekCurrentTemperatureText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAllCaps="true"
android:textSize="40sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekText" />
<TextView
android:id="@+id/dayOfWeekDegreeCelsiusSign"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/degree_celsius"
android:textAllCaps="true"
android:textSize="40sp"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekCurrentTemperatureText"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekText" />
<TextView
android:id="@+id/dayOfWeekWeatherStateText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/weather_state_text"
android:textSize="24sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekDegreeCelsiusSign" />
<TextView
android:id="@+id/dayOfWeekWindLabel"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/wind_label"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekHumidityLabel"
android:layout_
android:layout_
android:layout_margin="8dp"
android:text="@string/humidityLabel"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/dayOfWeekItemVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWindLabel" />
<TextView
android:id="@+id/dayOfWeekWindDirection"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindLabel"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekWindSpeed"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindDirection"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekWindSpeedLabel"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/wind_speed"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekWindSpeed"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWeatherStateText" />
<TextView
android:id="@+id/dayOfWeekHumidityText"
android:layout_
android:layout_
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekHumidityLabel"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekWindSpeedLabel" />
<TextView
android:id="@+id/dayOfWeekHumidityPercentageLabel"
android:layout_
android:layout_
android:layout_margin="8dp"
android:text="@string/percentage_sign"
android:textAllCaps="true"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/dayOfWeekCurrentTemperatureText"
app:layout_constraintTop_toBottomOf="@+id/dayOfWeekCurrentTemperatureText" />
</androidx.constraintlayout.widget.ConstraintLayout>
这里有什么问题,我该如何解决? 谢谢。
动画示例:
【问题讨论】:
请发布一些示例代码,以便我们了解您的动画效果。 我编辑了这篇文章。谢谢。 【参考方案1】:在我们了解如何让一切正常工作之前,让我们先看看是什么导致了 gif 中的行为。
其他项目视图跳起来的原因是因为动画是纯视觉的。也就是说,折叠动画实际上并没有从布局的角度为您的项目的高度设置动画,它只是为项目的绘制方式设置动画。这样做是出于性能原因(假设必须每秒重新布局所有视图 60 次)。这就是为什么当您的项目折叠时,所有其他视图都会跳转到最终布局位置。
RecyclerViews 非常擅长为孩子的身高设置动画,这就是我们将用来解决整个动画问题的方法。我在下面概述了完整的解决方案。
预览 GIF:https://giphy.com/gifs/SVlBnpeW3wIwNIVpVU
经过一些实验,我能够让 ConstraintLayout + ConstrainSet + RecyclerViews 工作。我将分享我是如何工作的。
代码
这里是代码的快速预览。
private inner class MatchInfoAdapter (
private val context: Context
) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
private var items = listOf<MatchItem>()
private val inflater: LayoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder =
FullViewHolder(inflater.inflate(viewType, parent, false))
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
)
if (payloads.isEmpty())
super.onBindViewHolder(holder, position, payloads)
else
val item = items[position]
val h = holder as FullViewHolder
if (!item.isExpanded)
h.collapsedConstraintSet.applyTo(h.rootView)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
val item = items[position]
val h = holder as FullViewHolder
val isExpanded = item.isExpanded
val constraint = if (isExpanded)
h.expandedConstraintSet
else
h.collapsedConstraintSet
constraint.applyTo(h.rootView)
bindGeneralViews(h, item, isExpanded)
if (isExpanded)
bindExpandedExtraViews(h, item)
h.clickView.setOnClickListener
toggleExpanded(h)
private fun bindGeneralViews(
h: FullViewHolder,
item: MatchItem,
isExpanded: Boolean
)
// bind views that are visible when expanded and collapsed
private funbindExpandedExtraViews(
h: FullViewHolder,
item: MatchItem
)
// bind views that are only shown when the item is expanded
private fun toggleExpanded(
h: FullViewHolder
)
if (h.adapterPosition< 0) return // touch event can technically fire after a view is unbound
val autoTransition = AutoTransition()
val item = items[position]
item.isExpanded = !item.isExpanded
bindGeneralViews(h, item, newIsExpanded)
if (item.isExpanded)
bindExpandedExtraViews(h, item)
autoTransition.ordering = AutoTransition.ORDERING_TOGETHER
autoTransition.duration = ANIMATION_DURATION_MS
TransitionManager.beginDelayedTransition(h.rootView, autoTransition)
h.expandedConstraintSet.applyTo(h.rootView)
notifyItemChanged(h.adapterPosition, Unit)
else
autoTransition.ordering = AutoTransition.ORDERING_TOGETHER
autoTransition.duration = ANIMATION_DURATION_MS
TransitionManager.beginDelayedTransition((h.rootView.parent as ViewGroup), autoTransition)
notifyItemChanged(h.adapterPosition, Unit)
data class MatchItem(
...
)
// Exclude this field from equals/hachcode by declaring it in class body
var isExpanded: Boolean = false
private class FullViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView)
...
val collapsedConstraintSet: ConstraintSet = ConstraintSet()
val expandedConstraintSet: ConstraintSet = ConstraintSet()
init
collapsedConstraintSet.clone(rootView)
expandedConstraintSet.clone(rootView.context, R.layout.build_full_item)
它是如何工作的?
代码严重依赖notifyItemChanged(Int, Payload)
和TransitionManager.beginDelayedTransition()
。我们先来看看这些是如何工作的。
首先,notifyItemChanged(Int, Payload)
将确保传递给onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>)
的视图持有者与当前绑定的视图持有者是同一个视图持有者。例如。假设A
是当前绑定到项目0的视图持有者。如果我们调用notifyItemChanged(0, Unit)
,那么我们可以保证A
将被传递给onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>)
。除此之外,RecyclerViews 非常擅长对项目视图高度的变化进行动画处理,因此notifyItemChanged()
将通知 RecyclerView 检查高度是否发生了变化,以及它是否必须播放一个很好的动画来让其他项目向上或向下动画。
其次,TransitionManager.beginDelayedTransition()
对传入的视图当前状态进行快照。然后在调用ConstraintSet.applyTo()
时,计算保存状态与当前状态之间的差异,并应用动画在两者之间进行转换, 自动。
现在基础知识已经不存在了。以下是展开和折叠项目的工作原理。
对于扩展项目:
-
用户点击项目。
toggleExpanded()
被调用。
项目的视图状态更新为展开。
我们将所有视图预绑定到视图持有者,以便在动画期间不会发生闪烁,并且所有视图都已完全绑定。
调用TransitionManager.beginDelayedTransition() 来获取项目视图状态的快照。
调用 ConstraintSet.applyTo()
以将我们展开的布局应用到视图并为更改设置动画。
notifyItemChanged(h.``adapterPosition``, Unit)
被调用。这保证了当 onBindViewHolder 被调用时,我们将把完全绑定的视图持有者传递给我们。此外,它会通知 recyclerview 项目的高度发生了变化,这将让 recyclerview 处理高度变化的动画。
折叠项目:
-
用户点击项目。
toggleExpanded()
被调用。
项目的视图状态更新为折叠。
调用TransitionManager.beginDelayedTransition() 来获取项目视图状态的快照。
notifyItemChanged(h.``adapterPosition``, Unit)
被调用。这保证了当 onBindViewHolder 被调用时,我们将把完全绑定的视图持有者传递给我们。此外,它会通知 recyclerview 项目的高度发生了变化,这将让 recyclerview 处理高度变化的动画。
调用 ConstraintSet.applyTo()
以将折叠的布局应用到视图并为更改设置动画。
其他有趣的事实
折叠一个项目实际上比看起来要复杂得多。在notifyItemChanged(h.adapterPosition, Unit)
之前调用TransitionManager.beginDelayedTransition()
至关重要。这是因为由于 recyclerview 的实现方式,传递给 onBindViewHolder
的视图持有者始终未绑定。
为什么这是一个问题?好吧,这意味着如果我们改为在onBindViewHolder
中调用TransitionManager.beginDelayedTransition()
,它将保存的状态是视图未绑定。当ConstraintSet.applyTo()
被调用时,它会在未绑定视图和绑定视图之间进行动画处理,默认动画是淡入视图。这不是我们想要的,动画看起来很丑。
【讨论】:
您好,首先,感谢您抽出宝贵时间写出这份详细的解释并回答问题。我不得不承认我有点忘记我发布了这个问题(已经有一段时间了)。不确定我什么时候有时间审查并试用它,所以如果其他人可以验证它是否有效,我会将其标记为已接受。以上是关于Recycler View 中的约束设置动画未正确设置动画的主要内容,如果未能解决你的问题,请参考以下文章
Android - 为啥 Recycler 视图未显示在设备上? .我认为我的代码是正确的,但视图没有显示
在 Android 的 Recycler View 中生成和设置文本视图背景的随机颜色
如何使用 Recycler View 的长按位置获取 SQLite 列值?