android - Recycler View 中的约束集动画动画不正确

标签 android animation android-constraintlayout constraintset

我正在为我的回收站 View 项目使用约束布局。为了动画(展开/折叠)它们,我使用约束集动画。开场动画在所有项目上运行良好。关闭动画也运行良好,但是当关闭动画在不是最后一个的项目上开始时,所有项目在动画开始时都会跳起来,而不是在动画结束时。

动画在项目点击时执行:

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>

这里有什么问题,我该如何解决?
谢谢你。

动画示例:

https://gph.is/g/aj8GdB4

最佳答案

在我们了解我如何让一切正常工作之前,让我们先看看是什么导致了您的 gif 中的行为。
其他项目 View 跳起来的原因是因为动画是纯视觉的。也就是说,从布局的角度来看,折叠动画实际上并没有为您的项目的高度设置动画,它只是为项目的绘制方式设置了动画。这样做是出于性能原因(想象一下必须每秒重新布局所有 View 60 次)。这就是为什么当您的项目折叠时,所有其他 View 都会跳转到最终布局位置。
RecyclerViews 非常擅长为 child 的高度设置动画,这就是我们将用来解决整个动画问题的方法。我在下面概述了完整的解决方案。

预览 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)将确保 View 持有者传递给 onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>)与当前绑定(bind)的 View 持有者是同一个 View 持有者。例如。比方说 A是当前绑定(bind)到项目 0 的 View 持有者。如果我们调用 notifyItemChanged(0, Unit)那么我们可以保证A将传递给 onBindViewHolder(RecyclerView.ViewHolder, Int, MutableList<Any>) .除此之外,RecyclerViews 非常擅长动画项目 View 高度的变化,所以 notifyItemChanged()将通知 RecyclerView 检查高度是否改变,以及它是否必须播放一个很好的动画来向上或向下设置其他项目的动画。
二、TransitionManager.beginDelayedTransition()获取传入 View 当前状态的快照。然后当 ConstraintSet.applyTo()被调用时,计算保存状态和当前状态之间的差异,并自动将动画应用于两者之间的转换。
现在,基础知识已经解决了。以下是展开和折叠项目的工作方式。
用于展开项目:
  • 用户点击项目。
  • toggleExpanded()叫做。
  • 项目的 View 状态更新为展开。
  • 我们将所有 View 预先绑定(bind)到 View 持有者,以便在动画过程中不会发生闪烁并且所有 View 都被完全绑定(bind)。
  • 调用 TransitionManager.beginDelayedTransition() 以获取项目 View 状态的快照。
  • ConstraintSet.applyTo()被调用以将我们的扩展布局应用于 View 并为更改设置动画。
  • notifyItemChanged(h.``adapterPosition``, Unit)叫做。这保证了当 onBindViewHolder 被调用时,我们将获得完全绑定(bind)的 View 持有者传递给我们。此外,它会通知 recyclerview 项目的高度发生了变化,让 recyclerview 处理高度变化的动画。

  • 用于折叠项目:
  • 用户点击项目。
  • toggleExpanded()叫做。
  • 项目的 View 状态更新为折叠状态。
  • 调用 TransitionManager.beginDelayedTransition() 以获取项目 View 状态的快照。
  • notifyItemChanged(h.``adapterPosition``, Unit)叫做。这保证了当 onBindViewHolder 被调用时,我们将获得完全绑定(bind)的 View 持有者传递给我们。此外,它会通知 recyclerview 项目的高度发生了变化,让 recyclerview 处理高度变化的动画。
  • ConstraintSet.applyTo()被调用以将我们的折叠布局应用于 View 并为更改设置动画。

  • 其他有趣的事实
    折叠一个项目实际上比眼睛看到的要复杂得多。 TransitionManager.beginDelayedTransition()之前打电话notifyItemChanged(h.adapterPosition, Unit)是至关重要的。这是因为 View 持有者传递给 onBindViewHolder由于 recyclerviews 是如何实现的,它总是不受约束的。
    为什么这是一个问题?那么这意味着如果我们要拨打 TransitionManager.beginDelayedTransition()onBindViewHolder相反,它将保存的状态是 View 未绑定(bind)。当ConstraintSet.applyTo()被调用,它将在未绑定(bind) View 到绑定(bind) View 之间进行动画处理,并且默认动画是淡入 View 。这不是我们想要的,动画看起来很丑陋。

    关于android - Recycler View 中的约束集动画动画不正确,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55033858/

    相关文章:

    java - Android Manifest 文件定制

    android - 如何从断点处找到调用线程堆栈跟踪?

    ios - 理解图层上的暂停和恢复动画

    android - 在 ConstraintLayout 中缩放矢量图形

    android - ConstraintLayout 1.1.0(测试版)中链中的边距如何工作

    android - 多部分实体 POST android

    Android 无法使用 Picasso 从 URI 加载图像

    objective-c - 在 UITableView 滚动时为单元格设置动画

    html - 如何围绕圆圈旋转顶部边界?

    android - 如何在 ConstraintLayout 中不使用 margin 属性的情况下设置两个 View 之间相对于屏幕宽度的空间