Var*_* I. 7 animation android android-constraintlayout constraintset
我正在为我的回收站视图项目使用约束布局。为了动画(展开/折叠)它们,我使用约束集动画。开场动画在所有项目上运行良好。关闭动画也运行良好,但是当关闭动画在不是最后一个的项目上开始时,所有项目在动画开始时都会跳起来,而不是在动画结束时。
动画在项目点击时执行:
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
}
Run Code Online (Sandbox Code Playgroud)
animateItemView 在哪里:
private fun animateItemView(constraintToApply: ConstraintSet,
constraintLayout: ConstraintLayout) {
TransitionManager.beginDelayedTransition(constraintLayout)
constraintToApply.applyTo(constraintLayout)
}
Run Code Online (Sandbox Code Playgroud)
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_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/dayOfWeekWeatherIcon"
android:layout_width="90dp"
android:layout_height="90dp"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="192dp" />
<TextView
android:id="@+id/dayOfWeekCurrentTemperatureText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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>
Run Code Online (Sandbox Code Playgroud)
和 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_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/dayOfWeekWeatherIcon"
android:layout_width="90dp"
android:layout_height="90dp"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="192dp" />
<TextView
android:id="@+id/dayOfWeekCurrentTemperatureText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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_width="wrap_content"
android:layout_height="wrap_content"
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>
Run Code Online (Sandbox Code Playgroud)
这里有什么问题,我该如何解决?谢谢你。
动画示例:
在我们了解如何让一切正常工作之前,让我们先看看是什么导致了 gif 中的行为。
\n其他项目视图跳跃的原因是因为动画纯粹是视觉的。也就是说,折叠动画实际上并不从布局角度对项目的高度进行动画处理,它仅对项目的绘制方式进行动画处理。这样做是出于性能原因(想象一下必须每秒重新布局所有视图 60 次)。这就是为什么当您的项目折叠时,所有其他视图都会跳转到最终布局位置。
\nRecyclerView 非常擅长为其子级的高度设置动画,这就是我们将用来解决整个动画问题的方法。我在下面概述了完整的解决方案。
\n预览 GIF:https://giphy.com/gifs/SVlBnpeW3wIwNIVpVU
\n经过一些实验,我能够让 ConstraintLayout + ConstrainSet + RecyclerViews 工作。我将分享我是如何让它发挥作用的。
\n这是代码的快速预览。
\nprivate inner class MatchInfoAdapter (\n private val context: Context\n) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {\n\n private var items = listOf<MatchItem>()\n private val inflater: LayoutInflater = LayoutInflater.from(context)\n\n override fun onCreateViewHolder(\n parent: ViewGroup, \n viewType: Int\n ): RecyclerView.ViewHolder = \n FullViewHolder(inflater.inflate(viewType, parent, false))\n\n override fun onBindViewHolder(\n holder: RecyclerView.ViewHolder,\n position: Int,\n payloads: MutableList<Any>\n ) {\n if (payloads.isEmpty()) {\n super.onBindViewHolder(holder, position, payloads)\n } else {\n val item = items[position]\n val h = holder as FullViewHolder\n \n if (!item.isExpanded) {\n h.collapsedConstraintSet.applyTo(h.rootView)\n }\n }\n }\n\n override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {\n val item = items[position]\n val h = holder as FullViewHolder\n val isExpanded = item.isExpanded\n \n val constraint = if (isExpanded) \n h.expandedConstraintSet \n else \n h.collapsedConstraintSet\n constraint.applyTo(h.rootView)\n\n bindGeneralViews(h, item, isExpanded)\n if (isExpanded) {\n bindExpandedExtraViews(h, item)\n }\n\n h.clickView.setOnClickListener {\n toggleExpanded(h)\n }\n }\n\n\n private fun bindGeneralViews(\n h: FullViewHolder,\n item: MatchItem,\n isExpanded: Boolean\n ) {\n // bind views that are visible when expanded and collapsed\n }\n \n private funbindExpandedExtraViews(\n h: FullViewHolder,\n item: MatchItem\n ) {\n // bind views that are only shown when the item is expanded\n }\n\n \n\n private fun toggleExpanded(\n h: FullViewHolder\n ) {\n if (h.adapterPosition< 0) return // touch event can technically fire after a view is unbound\n\n val autoTransition = AutoTransition()\n \n val item = items[position]\n item.isExpanded = !item.isExpanded\n \n bindGeneralViews(h, item, newIsExpanded)\n if (item.isExpanded) {\n bindExpandedExtraViews(h, item)\n \n autoTransition.ordering = AutoTransition.ORDERING_TOGETHER\n autoTransition.duration = ANIMATION_DURATION_MS\n TransitionManager.beginDelayedTransition(h.rootView, autoTransition)\n h.expandedConstraintSet.applyTo(h.rootView)\n \n notifyItemChanged(h.adapterPosition, Unit)\n } else {\n autoTransition.ordering = AutoTransition.ORDERING_TOGETHER\n autoTransition.duration = ANIMATION_DURATION_MS\n TransitionManager.beginDelayedTransition((h.rootView.parent as ViewGroup), autoTransition)\n notifyItemChanged(h.adapterPosition, Unit)\n }\n }\n}\n\ndata class MatchItem(\n...\n) {\n // Exclude this field from equals/hachcode by declaring it in class body\n var isExpanded: Boolean = false\n}\n\nprivate class FullViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView) {\n ...\n\n val collapsedConstraintSet: ConstraintSet = ConstraintSet()\n val expandedConstraintSet: ConstraintSet = ConstraintSet()\n \n init {\n collapsedConstraintSet.clone(rootView)\n expandedConstraintSet.clone(rootView.context, R.layout.build_full_item)\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n该代码严重依赖于notifyItemChanged(Int, Payload)和TransitionManager.beginDelayedTransition()。首先让\xe2\x80\x99s 回顾一下它们是如何工作的。
首先,notifyItemChanged(Int, Payload)将确保传递给的视图持有者onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>)与当前绑定的视图持有者是同一视图持有者。例如。let\xe2\x80\x99s 表示A当前绑定到项目 0 的视图持有者。如果我们调用,notifyItemChanged(0, Unit)那么我们可以保证它将A被传递到onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>)。除此之外,RecyclerView 非常擅长对项目视图高度的变化进行动画处理,因此notifyItemChanged()会通知 RecyclerView 检查高度是否发生变化,以及是否必须播放一个漂亮的动画来使其他项目向上或向下移动。
其次,TransitionManager.beginDelayedTransition()对传入的视图的当前状态进行快照。然后ConstraintSet.applyTo(),当调用时,将计算保存的状态和当前状态之间的差异,并自动应用动画来在两者之间进行转换。
现在基础知识已经不存在了。以下是展开和折叠项目的工作原理。
\n对于扩展项目:
\ntoggleExpanded()叫做。ConstraintSet.applyTo()被调用以将我们的扩展布局应用到视图并以动画方式进行更改。notifyItemChanged(h.``adapterPosition``, Unit)叫做。这保证了当调用 onBindViewHolder 时,我们会将完全绑定的视图持有者传递给我们。另外,它通知recyclerview项目的高度已经改变,让recyclerview处理高度变化的动画。对于折叠项目:
\ntoggleExpanded()叫做。notifyItemChanged(h.``adapterPosition``, Unit)叫做。这保证了当调用 onBindViewHolder 时,我们会将完全绑定的视图持有者传递给我们。另外,它通知recyclerview项目的高度已经改变,让recyclerview处理高度变化的动画。ConstraintSet.applyTo()被调用以将折叠布局应用于视图并以动画方式进行更改。折叠一个物品实际上比看上去要复杂得多。TransitionManager.beginDelayedTransition()之前的通话至关重要notifyItemChanged(h.adapterPosition, Unit)。onBindViewHolder这是因为由于 recyclerview 的实现方式,传递给的视图持有者始终未绑定。
为什么这是一个问题?好吧,这意味着如果我们改为调用TransitionManager.beginDelayedTransition(),onBindViewHolder它将保存的状态是视图未绑定。当ConstraintSet.applyTo()调用时,它将在未绑定视图和绑定视图之间进行动画处理,默认动画是使视图淡入。这不是我们想要的,而且动画看起来非常难看。
| 归档时间: |
|
| 查看次数: |
692 次 |
| 最近记录: |