Compose 动画艺术探索之可见性动画示例详解

  目录

  正文

  本篇文章是此专栏的第二篇文章,上一篇文章简单写了下 的动画,让大家先看了下 开箱即用的动画效果,效果还是挺好的,感兴趣的可以去看下:Compose 动画艺术探索之瞅下 Compose 的动画

  从可见性动画看起

  可见性动画在上一篇文章中介绍过,不过只是简单使用,没看过上一篇文章的也不用担心,给大家看下可见性动画的实际效果。

  实现代码也很简单,来回顾下:

  val visible = remember { mutableStateOf(true) }

  AnimatedVisibility(visible = visible.value,) {

  Text(text = "天青色等烟雨,而我在等你,炊烟袅袅升起,隔江千万里")

  }

  上一篇文章主要介绍了 动画的便携之处,例如上面代码,确实非常简单就能实现之前原生安卓中比较难实现的动画效果,今天咱们就来稍微深入一点看看,从小节标题也能知道,就从可见性动画来看!

  怎么看呢?直接点进去源码来看!先来看看 的函数定义吧!

  @Composable

  fun AnimatedVisibility(

  visible: Boolean,

  modifier: Modifier = Modifier,

  enter: EnterTransition = fadeIn() + expandIn(),

  exit: ExitTransition = shrinkOut() + fadeOut(),

  label: String = "AnimatedVisibility",

  content: @Composable() AnimatedVisibilityScope.() -> Unit

  ) {

  val transition = updateTransition(visible, label)

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  }

  其实可组合项 不是只有这一个,目前 版本中有六个,这个咱们待会再说,先看这一个,也是第一个,可以看到函数中一共可以接收六个参数,下面先来看看这六个参数分别有什么作用吧:

  上面这些参数除了 和 外都比较好理解,这里就不做过多解释,重点来看下 和 ,可以看到 的类型为 , 的类型为 ,那么接下来咱们来分别看看 和 吧!

  这里其实有一个小问题,可以看到上面 Gif 图中并不是淡入并扩展和缩小并消失,这是为什么呢?继续往下看就能找到答案!

  进入过渡——EnterTransition

  顾名思义,这个类主要是为了做进入过渡的,来简单看下它的源码吧:

  @Immutable

  sealed class EnterTransition {

  internal abstract val data: TransitionData

  // 组合不同的进入转换。组合的顺序并不重要,因为这些将同时启动

  @Stable

  operator fun plus(enter: EnterTransition): EnterTransition {

  return EnterTransitionImpl(

  TransitionData(

  fade = data.fade ?: enter.data.fade,

  slide = data.slide ?: enter.data.slide,

  changeSize = data.changeSize ?: enter.data.changeSize,

  scale = data.scale ?: enter.data.scale

  )

  )

  }

  companion object {

  // 当不需要输入转换时,可以使用此函数。

  val None: EnterTransition = EnterTransitionImpl(TransitionData())

  }

  }

  可以看到 是一个密封类, 类中有一个抽象的不可变值 ,类型为 ,这个放到下面来说;类中还有一个函数,而且该函数有 前缀, 这表示运算符重载,重载了“+”号,所以就可以使用“+”来组合不同的输入动画了,函数接收的参数也是 ,然后直接返回 ,又没见过,怎么办?继续看看 是个啥!

  @Immutable

  private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()

  可以看到 类很简单,是一个私有类,继承自 ,注意类上有 注解, 注解可用于将类标记为生成不可变实例,但类的不变性没有得到验证,它是类型的一种承诺,即在构造实例之后,所有公开可访问的属性和字段都不会更改。 还需要实现父类的抽象值,所有有 类型的参数 ,上面咱们简单提到了 ,这里来看下吧!

  @Immutable

  internal data class TransitionData(

  val fade: Fade? = null,

  val slide: Slide? = null,

  val changeSize: ChangeSize? = null,

  val scale: Scale? = null

  )

  可以看到 类也有 注解,这里就不做过多介绍,这是一个包内可见的数据类,里面有四个不可变值,分别是 、 、 、 ,其实从名称就能看出这几个参数分别代表的意思,不过还是来看下它们的源码吧!

  @Immutable

  internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec)

  @Immutable

  internal data class Slide(

  val slideOffset: (fullSize: IntSize) -> IntOffset,

  val animationSpec: FiniteAnimationSpec

  )

  @Immutable

  internal data class ChangeSize(

  val alignment: Alignment,

  val size: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },

  val animationSpec: FiniteAnimationSpec,

  val clip: Boolean = true

  )

  @Immutable

  internal data class Scale(

  val scale: Float,

  val transformOrigin: TransformOrigin,

  val animationSpec: FiniteAnimationSpec

  )

  可以看到这四个类都是不可变的数据类,分别表示颜色转变、滑动、大小变化及缩放。这几个类有一个共同点,都有一个共同的参数 ,参数类型为 ,来看看 是个啥?

  interface FiniteAnimationSpec : AnimationSpec {

  override fun vectorize(

  converter: TwoWayConverter

  ): VectorizedFiniteAnimationSpec

  }

  可以看到 是一个接口,继承自 ,简单理解就是有限动画规格,定义了动画的时长及动画效果等,类似于原生安卓中的什么呢?嗯。。。差值器吧! 是所有非无限动画实现的接口,包括: , , , , 等等,上一篇文章中说到的无限循环动画 没有继承这个接口,其实从名字看就知道了, 也继承自 。。。。

  不行不行,扯太远了,其实看源码就是这样,看着看着一直往下看就会迷失了最初的目标,越看越多,越看越多,这里就先不接着往下看了,再看就没完没了了,这里咱们知道这四个类大概存储了什么数据就行了。动画规格咱们放到之后的文章中慢慢看,今天主要来过一遍可见性动画!

  简单总结下, 类中有一个函数,进行了运算符重载,可以有多个动画同时进行。

  关闭过渡——ExitTransition

  其实看完刚才的 类再来看 就会觉得很简单了,不信的话咱们来看下 的源码:

  @Immutable

  sealed class ExitTransition {

  internal abstract val data: TransitionData

  // 结合不同的退出转换,组合顺序并不重要

  @Stable

  operator fun plus(exit: ExitTransition): ExitTransition {

  return ExitTransitionImpl(

  TransitionData(

  fade = data.fade ?: exit.data.fade,

  slide = data.slide ?: exit.data.slide,

  changeSize = data.changeSize ?: exit.data.changeSize,

  scale = data.scale ?: exit.data.scale

  )

  )

  }

  companion object {

  // 当不需要内置动画时使用

  val None: ExitTransition = ExitTransitionImpl(TransitionData())

  }

  }

  是不是基本一致,连抽象不可变值都一摸一样,甚至值的名称都没变!唯一不同的是 的实现类为 ,而 的子类为 ,那就再看看 !

  @Immutable

  private class ExitTransitionImpl(override val data: TransitionData) : ExitTransition()

  和 不能说相似,只能说一摸一样。。。所以就不做过多介绍。。。

  过渡——Transition

  文章开头的时候看 函数中只有下面的两行代码,

  val transition = updateTransition(visible, label)

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  这一小节咱们先来看第一行,可以看到调用了 函数, 传入了当前显示状态和标签,来具体看下这个函数吧!

  @Composable

  fun updateTransition(

  targetState: T,

  label: String? = null

  ): Transition {

  val transition = remember { Transition(targetState, label = label) }

  transition.animateTo(targetState)

  DisposableEffect(transition) {

  onDispose {

  // 出去的时候清理干净,确保观察者不会被困在中间状态

  transition.onTransitionEnd()

  }

  }

  return transition

  }

  可以看到这个函数也是一个可组合项,返回值为 ,函数中先记住并构建了一个 ,然后调用了 的 函数来执行过渡动画,然后调用了需要清理的效应 ,之后在 中调用了 函数,以防卡在这里,最后返回刚才构建的 。

  函数中的内容不难理解,现在唯一困惑的是 是个什么东西!那就来看看!

  @Stable

  class Transition @PublishedApi internal constructor(

  private val transitionState: MutableTransitionState,

  val label: String? = null

  )

  这就是 的类声明, 在状态级别上管理所有子动画。子动画可以使用 以声明的方式创建。、、 等。当 改变时, 将自动启动或调整其所有子动画的路线,使其动画到为每个动画定义的新目标值。

  可以看到 构造函数中接收一个 类型的参数和一个 ,但咱们看到上面传入的并不是 类型的参数,肯定还有别的构造函数!

  internal constructor(

  initialState: S,

  label: String?

  ) : this(MutableTransitionState(initialState), label)

  没错,确实还有一个构造函数,接下来看下 吧!

  class MutableTransitionState(initialState: S) {

  // 当前的状态

  var currentState: S by mutableStateOf(initialState)

  internal set

  // 过渡的目标状态

  var targetState: S by mutableStateOf(initialState)

  // 是否空闲

  val isIdle: Boolean

  get() = (currentState == targetState) && !isRunning

  // 是否运行

  internal var isRunning: Boolean by mutableStateOf(false)

  }

  可以看到 中构建了几个需要的状态,具体表示什么在上面代码中添加了注释。

  过渡动画实现——AnimatedEnterExitImpl

  刚才看了 函数中的第一行代码,下面咱们来看下第二行代码:

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  这块调用了一个实现函数,将刚构建好的 和之前的 、 统统传了进去,下面就来看下 !

  @Composable

  private fun AnimatedEnterExitImpl(

  transition: Transition,visible: (T) -> Boolean,

  modifier: Modifier,enter: EnterTransition,

  exit: ExitTransition,content: @Composable() AnimatedVisibilityScope.() -> Unit

  ) {

  val isAnimationVisible = remember(transition) {

  mutableStateOf(visible(transition.currentState))

  }

  if (visible(transition.targetState) || isAnimationVisible.value || transition.isSeeking) {

  val childTransition = transition.createChildTransition(label = "EnterExitTransition") {

  transition.targetEnterExit(visible, it)

  }

  LaunchedEffect(childTransition) {

  snapshotFlow {

  childTransition.currentState == EnterExitState.Visible ||

  childTransition.targetState == EnterExitState.Visible

  }.collect {

  isAnimationVisible.value = it

  }

  }

  AnimatedEnterExitImpl(childTransition,modifier,

  enter = enter,exit = exit,content = content

  )

  }

  }

  这块代码有点多啊,但不要害怕,可以看到基本上这里使用到的类在刚才咱们都看过了,接下来需要的就是一行一行往下看,看看哪个没见过咱们再看!

  函数中先记录了当前的状态,然后判断当前的状态值是否为 true,如果为 true 的话则创建子过渡,老规矩,来看看 !

  @Composable

  inline fun Transition.createChildTransition(

  label: String = "ChildTransition",

  transformToChildState: @Composable (parentState: S) -> T,

  ): Transition {

  val initialParentState = remember(this) { this.currentState }

  val initialState = transformToChildState(if (isSeeking) currentState else initialParentState)

  val targetState = transformToChildState(this.targetState)

  return createChildTransitionInternal(initialState, targetState, label)

  }

  可以看到 是 的一个扩展函数,还是一个高阶函数,函数内获取了高阶函数中的返回值,然后直接返回调用了 ,并传入获取的值,这个值目前还不知道是什么,只知道是 返回的数据,这个一会再看,先接着看 函数:

  @Composable

  internal fun Transition.createChildTransitionInternal(

  initialState: T,

  targetState: T,

  childLabel: String,

  ): Transition {

  val transition = remember(this) {

  Transition(MutableTransitionState(initialState), "${this.label} > $childLabel")

  }

  DisposableEffect(transition) {

  addTransition(transition)

  onDispose {

  removeTransition(transition)

  }

  }

  if (isSeeking) {

  transition.setPlaytimeAfterInitialAndTargetStateEstablished(

  initialState,

  targetState,

  this.lastSeekedTimeNanos

  )

  } else {

  transition.updateTarget(targetState)

  transition.isSeeking = false

  }

  return transition

  }

  可以看到 也是 的一个扩展函数,然后函数中也使用了需要清理的效应,之后判断 的值是, 的值在 函数中会被置为 true,如果 值为 true 则调用 函数,用来设置初始状态和目标状态建立后的时间,如果为 false,则更新状态值。

  到这里 函数咱们大概看了下,但刚才还有一个扣,刚才说了不知道目标值是什么,因为那是 的返回值,现在咱们来看看!

  @Composable

  private fun Transition.targetEnterExit(

  visible: (T) -> Boolean,

  targetState: T

  ): EnterExitState = key(this) {

  if (this.isSeeking) {

  if (visible(targetState)) {

  Visible

  } else {

  if (visible(this.currentState)) {

  PostExit

  } else {

  PreEnter

  }

  }

  } else {

  val hasBeenVisible = remember { mutableStateOf(false) }

  if (visible(currentState)) {

  hasBeenVisible.value = true

  }

  if (visible(targetState)) {

  EnterExitState.Visible

  } else {

  // If never been visible, visible = false means PreEnter, otherwise PostExit

  if (hasBeenVisible.value) {

  EnterExitState.PostExit

  } else {

  EnterExitState.PreEnter

  }

  }

  }

  }

  同样的, 也是一个扩展函数,返回值为 。这里也使用了 来进行判断,然后根据当前的值来设置不同的 。 又是个啥呢???接着来看!

  enum class EnterExitState {

  // 自定义进入动画的初始状态。

  PreEnter,

  // 自定义进入动画的目标状态,也是动画过程中自定义退出动画的初始状态。

  Visible,

  // 自定义退出动画的目标状态。

  PostExit

  }

  奥, 只是一个枚举类,定义了三种状态:初始状态、进入动画的状态和退出动画的状态。

  下面接着来看 :

  LaunchedEffect(childTransition) {

  snapshotFlow {

  childTransition.currentState == EnterExitState.Visible ||

  childTransition.targetState == EnterExitState.Visible

  }.collect {

  isAnimationVisible.value = it

  }

  }

  AnimatedEnterExitImpl(childTransition,modifier,

  enter = enter,exit = exit,content = content

  )

  这里使用了 效应,并使用 将 转为了 ,然后将值设置到 。

  后面又调用了相同名字的一个函数 :

  @Composable

  private inline fun AnimatedEnterExitImpl(

  transition: Transition,

  modifier: Modifier,

  enter: EnterTransition,

  exit: ExitTransition,

  content: @Composable AnimatedVisibilityScope.() -> Unit

  ) {

  if (transition.currentState == EnterExitState.Visible ||

  transition.targetState == EnterExitState.Visible

  ) {

  val scope = remember(transition) { AnimatedVisibilityScopeImpl(transition) }

  Layout(

  content = { scope.content() },

  modifier = modifier.then(transition.createModifier(enter, exit, "Built-in")),

  measurePolicy = remember { AnimatedEnterExitMeasurePolicy(scope) }

  )

  }

  }

  函数名字一样,但参数不同,这里将刚才构建好的 传了进来,然后函数内先对状态进行了过滤,然后构建了 。

  中设置了 , 调用了 函数, 函数用于将此修饰符与另一个修饰符连接,然后连接了 ,大家发现点什么没有,这里使用到了上面咱们构建好了所有东西。。。那么。。。。哈哈哈哈!

  柳暗花明

  下面咱们就来看看 这个函数,先来看下函数体:

  @Composable

  internal fun Transition.createModifier(

  enter: EnterTransition,

  exit: ExitTransition,

  label: String

  ): Modifier

  嗯,没错,是 的一个扩展函数,也是一个可组合项!接着往下看几行代码:

  var modifier: Modifier = Modifier

  modifier = modifier.slideInOut(

  this,

  rememberUpdatedState(enter.data.slide),

  rememberUpdatedState(exit.data.slide),

  label

  ).shrinkExpand(

  this,

  rememberUpdatedState(enter.data.changeSize),

  rememberUpdatedState(exit.data.changeSize),

  label

  )

  构建了一个 ,然后调用了 和 ,没错,这是滑动和缩小放大!接着来!

  var shouldAnimateAlpha by remember(this) { mutableStateOf(false) }

  var shouldAnimateScale by remember(this) { mutableStateOf(false) }

  if (currentState == targetState && !isSeeking) {

  shouldAnimateAlpha = false

  shouldAnimateScale = false

  } else {

  if (enter.data.fade != null || exit.data.fade != null) {

  shouldAnimateAlpha = true

  }

  if (enter.data.scale != null || exit.data.scale != null) {

  shouldAnimateScale = true

  }

  }

  创建两个值来记录是否需要透明度的转换和缩放!下面来看下执行的代码:

  if (shouldAnimateScale) {

  ......

  modifier = modifier.graphicsLayer {

  this.alpha = alpha

  this.scaleX = scale

  this.scaleY = scale

  this.transformOrigin = transformOrigin

  }

  } else if (shouldAnimateAlpha) {

  modifier = modifier.graphicsLayer {

  this.alpha = alpha

  }

  }

  嗯呢,是不是豁然开朗!但是 和 函数也是可见性动画这里定义的 的扩展函数,里面还有一些自定义的东西,但这不是本文的重点了。

  别的可见性动画

  文章开头说了,可见性动画目前一共有六个,听着很吓人,其实大同小异,来简单看下不同吧!

  @Composable

  fun RowScope.AnimatedVisibility(

  visible: Boolean,

  modifier: Modifier = Modifier,

  enter: EnterTransition = fadeIn() + expandHorizontally(),

  exit: ExitTransition = fadeOut() + shrinkHorizontally(),

  label: String = "AnimatedVisibility",

  content: @Composable() AnimatedVisibilityScope.() -> Unit

  ) {

  val transition = updateTransition(visible, label)

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  }

  @Composable

  fun ColumnScope.AnimatedVisibility(

  visible: Boolean,

  modifier: Modifier = Modifier,

  enter: EnterTransition = fadeIn() + expandVertically(),

  exit: ExitTransition = fadeOut() + shrinkVertically(),

  label: String = "AnimatedVisibility",

  content: @Composable AnimatedVisibilityScope.() -> Unit

  ) {

  val transition = updateTransition(visible, label)

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  }

  上面这两个和咱们上面说的基本一样,只不过一个是 的扩展函数,另一个是 的扩展函数,并且在默认动画上还有些区别, 默认是横向的扩展和收缩, 是纵向的扩展和收缩,更加方便咱们日常调用!这也就解释了文章开头提出的小问题!

  接着再来看别的!

  @Composable

  fun AnimatedVisibility(

  visibleState: MutableTransitionState,

  modifier: Modifier = Modifier,

  enter: EnterTransition = fadeIn() + expandIn(),

  exit: ExitTransition = fadeOut() + shrinkOut(),

  label: String = "AnimatedVisibility",

  content: @Composable() AnimatedVisibilityScope.() -> Unit

  ) {

  val transition = updateTransition(visibleState, label)

  AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

  }

  这个和之前的就有点区别了,第一个参数就不同了,参数类型为 ,其实是一样的,咱们上面也都说到了, 的使用方法在上一篇文章中也介绍过,感兴趣的可以去看一下。

  剩下的两个还是 和 的扩展函数,也是参数类型改为了 ,这里也就不做过多介绍。

  结尾

  本篇文章带大家看了可见性动画的一些源码,很多其实都是点到为止,并没有一致不断深入,一直深入就会陷入其中,忘了看源码的本意,本文所有源码基于 。

  本文至此结束,有用的地方大家可以参考,当然如果能帮助到大家,更多关于Compose 可见性动画的资料请关注脚本之家其它相关文章!

  您可能感兴趣的文章: