一个小时入门 Android Compose 动画

0. 前言

前段时间对于Android中的Compose动画做了系统性的学习,相关文章发布在 Compose 动画
专栏里。系统性学完Compose动画后,又对此做了系统性的回顾,抽取其比较重要的部分,希望能帮助大家快速入门Compose动画,所以有了本篇文章 : 一个小时入门 Android Compose 动画

1. Compose中的动画API

我们先来看下官网上的图,看上去很复杂,东西很多
在这里插入图片描述

对此,我进行了重新的整理。其实需要重点关注的Compose动画API,有这些
在这里插入图片描述
接下来我们一个个的来看

2. AnimateXxxAsState

AnimateXxxAsState用来实现单个值变化(大小、位置、颜色等)的动画
Xxx代指很多单位,比如DpColorFloatIntRect等。

我们这里以DpColor为例

  • animateDpAsState : 大小变化
  • animateColorAsState : 颜色变化

2.1 代码示例

下面这段代码点击后,Box的尺寸和颜色会发生动画过渡的变化

var big by remember {
    
    
    mutableStateOf(false)
}
//var size = if (big) 300.dp else 50.dp
val size by animateDpAsState(targetValue = if (big) 300.dp else 50.dp)
val color by animateColorAsState(targetValue = if (big) Color.Red else Color.Blue)
Box(
    Modifier
        .size(size)
        .background(color)
        .clickable {
    
    
            big = !big
        }
)

效果如下所示

在这里插入图片描述

3. AnimatedVisibility

控制UI组件的显示/隐藏,并可以自定义入场和出场时候的动画效果

3.1 入场和出场动画

AnimatedVisibility通过enter/exit可以配置入场和出场动画效果,比如以下几个

  • 淡入 : fadeIn / fadeout
  • 缩放 : scaleIn / scaleOut
  • 滑动 : slideIn / slideOut
  • 展开 : expandIn / shrinkOut

3.1 代码示例

这段代码点击按钮后,会 显示/隐藏 图片

Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
    var visible by remember {
    
    
        mutableStateOf(true)
    }
    AnimatedVisibility(visible = visible, enter = expandIn(), exit = shrinkOut()) {
    
    
        Image(painter = painterResource(id = R.drawable.photot1), contentDescription = null)
    }
    Button(onClick = {
    
    
        visible = !visible
    }) {
    
    
        Text(text = "显示/隐藏")
    }
}

效果如下所示
在这里插入图片描述

4. Transition

用来管理多个动画,通过updateTransition()来创建一个Transition,再通过Transition创建具体的动画

4.1 代码示例

这段使用Transition实现的代码,效果和使用animateXxxAsState实现的效果一样

var big by remember {
    
    
    mutableStateOf(false)
}
val transition = updateTransition(targetState = big, label = "myTransition")
val size by transition.animateDp(label = "mySize") {
    
    
    if (it) 300.dp else 50.dp
}
val color by transition.animateColor(label = "myColor") {
    
    
    if (it) Color.Red else Color.Blue
}
//val size by animateDpAsState(targetValue = if (big) 300.dp else 50.dp)
//val color by animateColorAsState(targetValue = if (big) Color.Red else Color.Blue)
Box(
    Modifier
    .size(size)
    .background(color)
    .clickable {
    
    
        big = !big
    }
)

效果如下所示

在这里插入图片描述

4.2 为什么要有Transition?

animateXxAsState可以实现一样的效果,那为什么还要有 Transition 这个API呢 ?

  • 使用Transition可以对动画做很方便的管理
    • animateXxAsState是面向值的,在多个动画多个状态的情况下存在不便于管理的问题
    • Transition是面向状态的,多个动画可以共用一个状态,能够做到统一的管理
  • Transition支持Compose动画预览
    • Transition支持Compose动画预览功能
    • animateXxAsState当前不支持Compose动画预览

4.3 如何进入Compose动画预览界面 ?

点击Start Animation Preview按钮即可进入

在这里插入图片描述

4.4 封装并复用Transition

在简单的场景下,在同一个页面中使用updateTransition创建Transition并直接操作它完成动画是没问题的。然而,如果需要处理一个具有许多动画属性的复杂场景,可以把Transition动画的实现与用户界面分开,从而提升代码复用率和可维护性。

class TransitionBean(size: State<Dp>, color: State<Color>) {
    
    
    val size by size
    val color by color
}

@Composable
private fun updateMyTransition(big: Boolean): TransitionBean {
    
    
    val transition = updateTransition(targetState = big, label = "myTransition")
    val size = transition.animateDp(label = "mySize") {
    
    
        if (it) 300.dp else 50.dp
    }
    val color = transition.animateColor(label = "myColor") {
    
    
        if (it) Color.Red else Color.Blue
    }
    return TransitionBean(size, color)
}

@Composable
private fun TransitionTest() {
    
    
    var big by remember {
    
    
        mutableStateOf(false)
    }
    val transitionBean = updateMyTransition(big)
    Box(
        Modifier
            .size(transitionBean.size)
            .background(transitionBean.color)
            .clickable {
    
    
                big = !big
            }
    )
}

5. AnimationSpec

自定义动画规格 AnimationSpec,类似于传统View体系中的差值器Interpolator,但是比起差值器,又提供了更多的功能。
在这里插入图片描述

5.1 SpringSpec

基于弹簧的物理动画效果,通过spring()进行调用。

  • 很多动画内部AnimationSpec使用的默认值都是spring,比如animateXXXAsState以及updateTransition等
  • 基于物理规律,使动画更真实自然
  • 因为是基于物理规律的,所以无法指定动画执行时长,而是会基于物理规律来确定动画执行时长

5.1.1 SpringSpec的参数

  • dampingRatio :弹簧的阻尼比,这个值越大,阻尼越大
  • stiffness :弹簧的刚度值,弹簧有多想回弹回去,这个值越大,回弹的越快
  • visibilityThreshold :当动画到达这个阈值会立即停止

5.1.2 代码示例

var big by remember {
    
    
    mutableStateOf(false)
}
val size by animateDpAsState(
    targetValue = if (big) 300.dp else 50.dp,
    animationSpec = spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessHigh, 0.1.dp)
)
Box(
    Modifier
        .size(size)
        .background(Color.Blue)
        .clickable {
    
    
            big = !big
        }
)

效果如下所示

在这里插入图片描述

5.2 TweenSpec

可指定规定时间完成动画,通过tween进行调用。

可以使用Easing来可以控制动画的节奏。

5.2.1 TweenSpec的参数

  • durationMillis : 动画执行时长

  • delayMillis : 动画延迟多久执行

  • easing : 用来控制动画的节奏

5.2.2 Easing

在这里插入图片描述

5.2.3 代码示例

这段代码指定了在2秒时间匀速线性地完成动画

var big by remember {
    
    
    mutableStateOf(false)
}
val size by animateDpAsState(
    targetValue = if (big) 300.dp else 50.dp,
    animationSpec = tween(2000, easing = LinearEasing)
)
Box(
    Modifier
    .size(size)
    .background(Color.Blue)
    .clickable {
    
    
        big = !big
    }
)

效果如下所示
在这里插入图片描述

5.3 RepeatableSpec / InfiniteRepeatableSpec

  • RepeatableSpec : 可循环播放的动画,需要包裹其他AnimateSpec
  • InfiniteRepeatableSpec: 无限循环动画,需要包裹其他AnimateSpec

repeatable/infiniteRepeatable不支持spring,因为一个循环运动的弹簧是违背物理规律的

5.3.1 代码示例

这段代码使用RepeatableSpec,循环播放动画5

val size by animateDpAsState(
    targetValue = if (big) 300.dp else 50.dp,
    animationSpec = repeatable(
        iterations = 5,
        tween(2000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

这段代码使用InfiniteRepeatableSpec,无限循环播放动画

val size by animateDpAsState(
    targetValue = if (big) 300.dp else 50.dp,
    animationSpec = infiniteRepeatable(
        tween(2000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

6. Animatable

AnimatableAndroid Compose动画的底层API

  • animateDpAsState内部使用Animatable实现
  • Animatable具有更高的可定制性
  • 如果animateDpAsState能够满足需求,就用animateDpAsState就行了,否则才去使用Animatable

6.1 Animatable的参数

  • initialValue : 初始值
  • typeConverter : 转换到什么单位 (比如Dp)
  • visibilityThreshold : 当动画到达这个阈值会立即停止

6.2 代码示例

通过调用animatable.animateTo实现动画的过渡,animateTo必须要在LaunchedEffect内部执行
这段代码的动画效果,和使用AnimateDpAsState的动画效果一致。

@Composable
private fun AnimatableTest() {
    
    
    var big by remember {
    
    
        mutableStateOf(false)
    }
    var size = if (big) 300.dp else 50.dp
    val animatable = remember {
    
    
        Animatable(size, Dp.VectorConverter)
    }
    LaunchedEffect(key1 = big) {
    
    
        animatable.animateTo(size)
    }
    Box(
        Modifier
            .size(animatable.value)
            .background(Color.Blue)
            .clickable {
    
    
                big = !big
            }
    )
}

效果如下所示
在这里插入图片描述

6.3 Compose里的协程 : LaunchedEffect

LaunchedEffectCompose里的协程
为什么要专门为Compose出一个专门的协程呢 ?
因为Compose中每次状态改变,Compose进行重组更改UI的时候,就会去重新执行相应的代码块

  • 如果使用原本的协程,每次Compose的重组都会执行该代码,这肯定是不行的
  • LaunchedEffect专门针对Compose重组的这个特定,做了特定的处理,只有其传入的key值发生变化的时候,才会去执行
LaunchedEffect(key1 = big) {
    
    
    animatable.animateTo(size)
}

7. 为什么使用animateDpAsState,就可以实现动画渐变的效果 ?

其实我们点击AnimateDpAsState内部的代码,可以发现其也是调用Animatable来实现的。

val animatable = remember {
    
    
    Animatable(size, Dp.VectorConverter)
}
LaunchedEffect(key1 = targetValue) {
    
    
    animatable.animateTo(size)
}

也就是说,animateDpAsState内部会去启动一个协程,通过animatable.animateTo会在某一个时间段内,自动完成从当前值到目标值的计算,并且实时将该值通知给Compose重组,从而实现页面界面的更新,达到动画渐变的效果。

而什么时候触发协程呢 ? 就是当协程传入的这个keytargetValue发生变化的时候。

8. 小结

到这里,我们就把Compose的重点API都讲完了,但是要想真正入门Compose动画,还需要上手亲自上手去敲一下代码,真正实践下才能真的掌握。在此基础上,如果想要看更详细的Compose动画,欢迎看我的Compose动画 专栏

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/131038145