Ограничение Установить анимацию внутри Recycler View неправильно анимации
Я использую макет ограничения для своих элементов представления переработчика. Чтобы анимировать (развернуть / свернуть) их, я использую анимацию Constraint Set. Анимация открытия отлично работает на всех предметах. Закрывающая анимация также работает нормально, но когда закрывающая анимация запускается для элемента, который не является последним, все элементы всплывают при запуске анимации, а не в конце анимации.
Анимация выполняется по клику элемента:
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_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>
И макет 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>
В чем здесь проблема и как я могу это исправить? Спасибо.
Пример анимации:
0 ответов
Прежде чем мы перейдем к тому, как мне удалось заставить все работать, давайте посмотрим, что вызывает такое поведение в вашем гифке.
Причина, по которой просмотры других элементов подскакивают, заключается в том, что анимация является чисто визуальной. То есть анимация сворачивания на самом деле не анимирует высоту вашего элемента с точки зрения макета, а только анимирует то, как элемент отрисовывается. Это сделано из соображений производительности (представьте, что вам нужно менять макет всех представлений 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()
вызывается для применения нашего свернутого макета к представлению и для анимации изменений.
Дополнительные забавные факты
Свернуть объект на самом деле намного сложнее, чем кажется на первый взгляд. ВTransitionManager.beginDelayedTransition()
позвони перед notifyItemChanged(h.adapterPosition, Unit)
является решающим. Это потому, что владелец представления перешел кonBindViewHolder
всегда не привязан из-за того, как реализованы recyclerviews.
Почему это проблема? Что ж, это означает, что если бы мы позвонилиTransitionManager.beginDelayedTransition()
в onBindViewHolder
вместо этого он сохранит состояние, в котором представление не привязано. когдаConstraintSet.applyTo()
вызывается, он будет анимировать между несвязанным представлением и привязанным представлением, а анимация по умолчанию для этого - постепенное исчезновение представления. Это не то, что мы хотим, и анимация выглядит очень уродливо.