10、Jetpack Compose 入门 --- 动画(一)

326 阅读5分钟

一、高级别动画

1. 内容变化

动画作用其他说明
AnimatedVisibility显示/隐藏状态切换
Crossfade布局切换
AnimatedContent单个值变化可组合项会在内容根据目标状态发生变化时,为内容添加动画效果。
animateContentSize为大小变化添加动画效果为了确保流畅的动画,请务必将其放置在任何大小修饰符(如 size 或 defaultMinSize)前面,以确保 animateContentSize 会将带动画效果的值的变化报告给布局。

1.1 AnimatedVisibility

@Composable
fun ColumnScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    label: String = "AnimatedVisibility",
    content: @Composable AnimatedVisibilityScope.() -> Unit
)
  • visible:内容是否可见
  • modifier:修饰符
  • enter:进入动画
  • exit:退出动画

例子:

// 显示/隐藏状态
val inState = remember { mutableStateOf(true) }
AnimatedVisibility(
    visible = inState.value,
    // 设置淡入动画
    enter = fadeIn() + expandVertically(),
    // 设置淡出动画
    exit = fadeOut() + shrinkVertically()
) {
    Image(
        painter = painterResource(id = R.mipmap.image_cat3),
        contentDescription = null
    )
}

以下是google为我们提供的几组自带的进入/退出动画效果,多个动画可以用+进行组合使用。

EnterTransition(进入动画)ExitTransition(退出动画)
fadeIn
fadeOut
slideIn
slideOut
slideInHorizontally
slideOutHorizontally
slideInVertically
slideOutVertically
scaleIn
scaleOut
expandIn
shrinkOut
expandHorizontally
shrinkHorizontally
expandVertically
shrinkVertically

1.2 Crossfade

由于此动画是用于在布局切换中执行,因此首先需要定义一组tag用于区别不同的布局,并将之传递给Crossfade。先看看源码:

@Composable
fun <T> Crossfade(
    targetState: T,
    modifier: Modifier = Modifier,
    animationSpec: FiniteAnimationSpec<Float> = tween(),
    content: @Composable (T) -> Unit
)
  • targetState:动画的触发器,表示动画执行的目标值,每次改变的时候都会触发动画。
  • modifier:修饰符。
  • animationSpec:配置动画。默认为tween()

例子:

var currentPage by remember { mutableStateOf("A") }
Button(onClick = {
  currentPage = if (currentPage == "A") "B" else "A"
}) {
  Text(text = if (currentPage == "A") "切换到B" else "切换到A")
}
Crossfade(
  targetState = currentPage,
  // 设置切换时候的动画
  animationSpec = tween(durationMillis = 1500)
) { screen ->
  when (screen) {
    "A" -> Box(
      modifier = Modifier
        .size(100.dp)
        .background(color = Color.Cyan)
    ) { Text("Page A") }
    "B" -> Box(
      modifier = Modifier.size(100.dp)
    ) { Text("Page B") }
  }
}

GIF 2022-9-9 17-56-36.gif

1.3 AnimatedContent

@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = { ...... },
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)
  • targetState:动画的触发器,表示动画执行的目标值,每次改变的时候都会触发动画。
  • modifier:修饰符。
  • transitionSpec:动画的执行规范。
  • contentAlignment:动画的触发位置。默认是从布局的左上角进入/退出。

例子:

var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
  Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
  // Make sure to use `targetCount`, not `count`.
  Text(text = "Count: $targetCount")
}

GIF 2022-9-9 20-26-34.gif

1.4 animateContentSize

fun Modifier.animateContentSize(
    animationSpec: FiniteAnimationSpec<IntSize> = spring(),
    finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
)
  • animationSpec:配置动画。默认为spring()
  • finishedListener:动画完成的监听。

例子:

var large by remember { mutableStateOf(true) }
Button(onClick = { large = !large }) {
  Text(text = if (large) "变短" else "变长")
}
Box(
  modifier = Modifier
    .padding(top = 4.dp)
    .background(Color.Blue)
    // 开启动画
    .animateContentSize()
) {
  Text(
    text = "Hello",
    modifier = Modifier.width(if (large) 100.dp else 50.dp)
  )
}

2. 值变化

动画作用其他说明
animateXxxAsState为单个值变化添加动画
updateTransition多个值随着状态一起变化通过自定义的一组状态来对一组值进行统一管理。
rememberInfiniteTransation多个值随着状态一起变化效果与updateTransition类似。
区别在于,此动画在一进入组合阶段就会开始不断重复地运行,除非被移除,否则不会停止。

2.1 animateXxxAsState

  • animateXxxAsState非常简单,只需要提供一个最终的目标值,就会从当前值开始执行动画。
  • animateXxxAsState支持的数据类型包含:FloatDpSizeOffsetRectIntIntOffsetIntSize

接下来,我们以Dp为例:

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
)
  • targetValue:动画的触发器,表示动画执行的目标值,每次改变的时候都会触发动画。
  • animationSpec:配置动画。
  • finishedListener:动画完成的监听。

看个例子:

GIF 2022-9-15 19-34-53.gif

@Composable
fun CustomAnimateContent() {
  Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Top
  ) {
    val size = remember { mutableStateOf(10) }
    // 为尺寸添加动画
    val sizeState by animateDpAsState(size.value.dp)
    ......
    Box(
      modifier = Modifier
        .padding(top = 8.dp)
        // 直接使用添加动画后的尺寸值
        .size(size = sizeState)
        .background(color = Color.Blue)
    )
  }
}

2.2 updateTransition

使用步骤:

  1. 创建一组状态:推荐使用枚举类型来保证类型的安全。
  2. 利用updateTransition记住Transtion实例,并更新状态。
  3. 使用animateXxx函数来创建对应类型的子动画。
  4. 使用到组件属性上。
// 1. 定义一组状态
enum class UiState {
  BIG_BLUE,
  MIDDLE_RED,
  SMALL_GREEN
}

@Composable
fun UpdateTransitionUi() {
  // 2. 创建Transition对象
  val state = remember { mutableStateOf(UiState.BIG_BLUE) }
  val transition = updateTransition(targetState = state, label = "")
  // 3. 创建动画
  val size = transition.animateDp(label = "") {
    when (it.value) {
      UiState.BIG_BLUE    -> 100.dp
      UiState.MIDDLE_RED  -> 70.dp
      UiState.SMALL_GREEN -> 40.dp
    }
  }
  val color = transition.animateColor(label = "") {
    when (it.value) {
      UiState.BIG_BLUE    -> Color.Blue
      UiState.MIDDLE_RED  -> Color.Red
      UiState.SMALL_GREEN -> Color.Green
    }
  }
  ......
  Box(
    modifier = Modifier
      .padding(top = 4.dp)
      // 4. 使用动画值
      .size(size = size.value)
      .background(color = color.value)
  )
}

GIF 2022-9-15 20-47-43.gif

2.3 rememberInfiniteTransation

使用步骤:

  1. 使用rememberInfiniteTransition方法构造一个InfiniteTransition实例。
  2. 使用InfiniteTransitionanimateXxx方法创建子动画。
  3. 使用到组件属性上。
@Composable
fun InfiniteTransitionUi() {
  // 1. 构造InfiniteTransition实例
  val infiniteTransition = rememberInfiniteTransition()
  // 2. 创建动画
  val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
      animation = tween(1000, easing = LinearEasing),
      repeatMode = RepeatMode.Reverse
    )
  )
  Box(
    Modifier
      .width(200.dp)
      .height(200.dp)
      // 3. 使用动画值
      .background(color)
  )
}

GIF 2022-10-13 21-43-22.gif

二、低级别动画

1 Animatable

从命名就可以看出来,Animatable是一个可执行动画的属性容器,里面可以放各种各样的数据。只要调用animatable.animateTo(newValue)方法,即可使使用了该容器数据作为属性的组件执行相应的动画。

@Composable
private fun AnimatablePage() {
  val size = remember { Animatable(100f) }
  val sizeState = remember { mutableStateOf(size.value) }
  LaunchedEffect(key1 = sizeState.value) {
    size.animateTo(sizeState.value)
  }
  Column(modifier = Modifier.padding(16.dp)) {
    FloatSetting(title = "圆心X轴偏移量", modifier = Modifier.height(40.dp), minNum = 100f, maxNum = 200f, interval = 20f, defNum = sizeState)
    Canvas(
      modifier = Modifier
        .padding(top = 8.dp)
        .size(size.value.dp)
    ) {
      drawCircle(color = Color.Red)
    }
    Canvas(
      modifier = Modifier
        .padding(top = 8.dp)
        .size(size.value.dp)
    ) {
      drawRect(color = Color.Green)
    }
  }
}

动画.gif

2 Animation

Animation是一个用来描述动画的接口,通过其源码我们可以看到它定义了一个动画的信息以及该动画在在执行过程中任一时刻的状态。具体的后面再说。

注意看下面代码的注释:

  • 1 定义了一个数据类型与动画在某一时刻的值和速度的转换规则
  • 2 定义了动画执行的时长
  • 3 定义了动画执行的目标值
  • 4 定义了动画是否是一个无限动画
  • 5、6、7 共同定义了动画的过程
interface Animation<T, V : AnimationVector> {
    /**
     * 1. 将任意数据类型的值/速度转换为动画矢量的双向转换器
     */
    val typeConverter: TwoWayConverter<T, V>
    
    /**
     * 2. 动画执行的时长(以纳秒为单位)
     */
    @get:Suppress("MethodNameUnits")
    val durationNanos: Long

    /**
     * 3. 动画执行的目标值
     */
    val targetValue: T

    /**
     * 4. 是否是一个无限动画。
     * 无限动画也不会自行完成,需要依赖于外部的行动才能停止。
     * 例如,不确定的进度条,只有在从合成中删除时才会停止。
     */
    val isInfinite: Boolean

    /**
     * 5. 返回给定播放时刻动画的值。
     */
    fun getValueFromNanos(playTimeNanos: Long): T

    /**
     * 6. 返回动画在给定播放时刻的速度(以动画矢量形式)。
     */
    fun getVelocityVectorFromNanos(playTimeNanos: Long): V

    /**
     * 7. 返回动画是否在给定播放时刻完成。
     */
    fun isFinishedFromNanos(playTimeNanos: Long): Boolean
}