Compose中的动画

1,578 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

我们在传统的Android开发中编写UI动画需要在drawable文件中定义一大堆动画xml文件,在Acitivity设置动画属性并开启,但是在Compose中定义动画就没有这么复杂,接下来就介绍一下Compose中的各种动画Api

Animatable

Animatable是一个基于携程的API,用于设置单个值的动画。我们可以使用此API设置颜色或浮点值的动画。它不同于所有其他动画API,因为您可以在可组合函数之外使用此API。

@Composable
private fun AnimatableSample() {
    var isAnimated by remember { mutableStateOf(false) }

    val color = remember { Animatable(Color.DarkGray) }

    LaunchedEffect(isAnimated) {
        color.animateTo(if (isAnimated) Color.Green else Color.Red, animationSpec = tween(2000))
    }

    Box(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(0.8f)
            .background(color.value)
    )
    Button(
        onClick = { isAnimated = !isAnimated },
        modifier = Modifier.padding(top = 10.dp)
    ) {
        Text(text = "Animate Color")
    }
}

在之前我们不是学习过Compose的函数副作用吗,在 LaunchedEffect中,我们给出了一个基于 isAnimated值更改颜色的条件。当您按下按钮时,LaunchedEffect中的block就会改变color的颜色值,从红色切换到绿色,为了让大家看到动画效果我们给颜色切换加了间隔持续时间,使用到了tween(),我们还可以使用以下Api来实现自定义动画

  • tween

    @Stable
    fun <T> tween(
        durationMillis: Int = DefaultDurationMillis, //动画持续时间
        delayMillis: Int = 0, //动画延时开始时间
        easing: Easing = FastOutSlowInEasing  //使用缓和曲线在起始值和结束值之间添加动画
    ): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
    
  • keyframes

    keframes可以针对不同时间范围的动画帧添加动画效果

    @Composable
    fun KeyframesScreen(){
    val value by animateFloatAsState(
            targetValue = 1f,
            animationSpec = keyframes {
                durationMillis = 375
                0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
                0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
                0.4f at 75 // ms
                0.4f at 225 // ms
            }
        )
    }
    
  • spring

    @Stable
    fun <T> spring(
        dampingRatio: Float = Spring.DampingRatioNoBouncy, //弹簧的阻尼,也就是弹性 阻尼比越低,弹簧越有弹性
        stiffness: Float = Spring.StiffnessMedium,  //弹簧像结束值步进的速度
        visibilityThreshold: T? = null
    ): SpringSpec<T> =
        SpringSpec(dampingRatio, stiffness, visibilityThreshold)
    

    spring的动画效果和弹簧的感觉非常像

  • repeatable

    //执行一个重复性动画
    @Stable
    fun <T> repeatable(
        iterations: Int, //重复次数
        animation: DurationBasedAnimationSpec<T>, //重复动画
        repeatMode: RepeatMode = RepeatMode.Restart, //重复模式
        initialStartOffset: StartOffset = StartOffset(0) //动画的启示偏移时长
    ): RepeatableSpec<T> =
        RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)
    
  • infiniteRepeatable

    infiniteRepeatable其实和repeatable非常相似,差别就在于一个是有重复次数,一个是无限重复的动画

  • snap

    snap主要用于需要提前结束动画

    @Stable
    fun <T> snap(
        delayMillis: Int = 0 //延时毫秒数
    ) = SnapSpec<T>(delayMillis)
    

animate*AsState

Animate*AsState主要用于为单个值提供动画,这些值可以是DpColorFloatIntegerOffsetRectSize

image.png

AnimateDpAsState主要用于widget的长度或者高度变化的动画添加

@Composable 
fun animateDpAsState ( 
    targetValue : Dp ,
     animationSpec : AnimationSpec < Dp >  =  dpDefaultSpring ,
     finishedListener :  ( ( Dp )  - > Unit ) ?  =  null
 )

同样也可以为其提供动画效果animationSpec

fun SearchBoxForState(
    focusState: MutableState<Boolean>,
    modifier: Modifier,
    onSearchEvent: () -> Unit,
    onClick: () -> Unit
) {
    val searchBoxWidth by animateDpAsState(targetValue = if (focusState.value) 679.wdp else 340.wdp, animationSpec = tween())
    SearchBox(
        searchBoxWidth = searchBoxWidth,
        isShowClean = focusState,
        modifier = modifier,
        onSearchEvent = onSearchEvent,
        onClick = onClick
    )
}

AnimateColorAsState 为颜色的变化添加过渡动画

@Composable
fun HomeScreen(){
val bgColor by animateColorAsState(if (index % 2 == 0) whiteFFFFFFFF else grayF9F9F9)
    ConstraintLayout(
        modifier = modifier
            .fillMaxWidth()
            .height(71.hdp)
            .background(bgColor)
    ){
    	.....
    }
}

AnimateFloatAsState animateDpAsState() 相同。但唯一的区别是我们将浮点值作为目标值。

例如为颜色透明度添加过渡动画等等

@Composable
fun HomeScreen(){
	val alpha by animateFloatAsState(targetValue = if (isSelect) 1.0f else 0.7f)
	Text(
                text = tabName,
                style = TextStyle(
                    color = whiteFFFFFFFF.copy(alpha),
                    fontSize = 19.spi,
                    fontWeight = if (isSelect) FontWeight.Bold else FontWeight.Medium
                )
     )
}

updateTransition 同时制作多个动画

@Composable
fun AnimateScreen(){
    //动画启停的状态
    var isAnimate by remember { mutableStateOf(false) }
    //updateTransitionde 定义
    val transition = updateTransition(targetState = isAnimate, label = "transition")
    //定义关于Box大小变化的过渡动画
    val animateSize by transition.animateDp(transitionSpec ={
        tween(1000)
    }, label = ""){
        if(it) 200.wdp else 50.wdp
    }
	// //定义关于Box旋转角度变化的过渡动画
    val animateRotate by transition.animateFloat(transitionSpec ={
        tween(1000)
    }, label = ""){
        if(it) 360f else 0f
    }
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
        Column() {
            Box(modifier = Modifier
                .rotate(animateRotate)
                .size(animateSize)
                .background(green1ACFB7)

            )
            TextButton(onClick = {
                isAnimate = !isAnimate
            }) {
                Text(text = "开始")
            }
        }

    }
}

InfiniteTransition

上面的动画效果都是执行一次就不会运行了,那我们怎么创建一个Box无限旋转的动画呢,接下来学习无限过渡的Api-----InfiniteTransition

通过InfiniteTransition创建的动画在进入可组合项时就会开始运行,直到他们从和组合项中移除才会停止


@Composable
fun InfiniteTransitionScreen(){
    val infiniteTransition = rememberInfiniteTransition()
    val infiniteRotate by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(200),
            repeatMode = RepeatMode.Restart //重复模式Restart是重启,Reverses是反转
        )
    )
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
        Column() {
            Box(modifier = Modifier
                .rotate(infiniteRotate)
                .size(100.wdp)
                .background(green1ACFB7)

            )
        }
    }
}

我们不仅能使用infiniteTransition.animateFloat创建子动画,还可以使用InfiniteTransition.animateColorInfiniteTransition.animateValue这些方式来创建一个无限运行的子动画

更多的详情可以参考官方文档