Jetpack Compose(十七)Compose动画低级别动画API

前一节介绍的高级别动画API都是在低级别动画API的基础上构建的。本节来看看都有哪些低级别动画API,以及该如何使用它们。

一、低级别动画API

1、animate*AsState

animated*AsState是最常用的低级别API之一,它类似于传统视图中的属性动画,可以自动完成从当前值到目标值过渡的估值计算。

以animateColorAsState为例,它将Color转成一个可以在Composable中访问的State。

kotlin 复制代码
@Composable  
fun animateColorAsState(  
    targetValue: Color,  
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,  
    finishedListener: ((Color) -> Unit)? = null  
): State<Color> {  
    return animateColorAsState(  
        targetValue, animationSpec, finishedListener = finishedListener  
    )  
}

Color的变化会触发Composable重组,从而完成动画效果。下面是一个例子:

kotlin 复制代码
@Preview  
    @Composable  
    fun TestAnimateColorAsState() {  
        var isLike by remember { mutableStateOf(false) }  
        //animateDpAsState大小发生变化
        val buttonSize by animateDpAsState(  
            targetValue = if (isLike) 32.dp else 24.dp,  
            animationSpec = tween(3000),   //为了看到效果特地加长了动画时长
            label = ""  
        )  
        //animateColorAsState颜色发生变化
        val buttonColor by animateColorAsState(  
            targetValue = if (isLike) Color.Red else Color.Gray,  
            animationSpec = tween(3000),   //为了看到效果特地加长了动画时长
            label = ""  
        )  
  
        IconButton(onClick = {  
            isLike = !isLike  
        }) {  
            Icon(  
                imageVector = Icons.Rounded.Favorite,  
                contentDescription = null,  
                modifier = Modifier.size(buttonSize),   //使用buttonSize
                tint = buttonColor      //使用buttonColor
            )  
        }  
    }

UI效果

Compose为常用的数据类型都提供了animate*AsState方法,例如Float、Color、Dp、Size、Bounds、Offset、Rect、Int、IntOffset和InSize等,对于无法直接估值计算的数据类型,可以使用通用类型的animateValueAsState,并自行实现TwoWayConverter估值计算器。

2、Animatable

Animatble是一个数值包装器,它的animateTo方法可以根据数值的变化设置动画效果,animate*AsState背后就是基于Animatable实现的。在前面的例子中,通过animateColorAsState对IconButton颜色应用过渡动画,如果改为Animatable实现方案,那么代码如下:

kotlin 复制代码
@Preview  
    @Composable  
    fun TestAnimatable() {  
        var isLike by remember { mutableStateOf(false) }  
        //animateDpAsState修改尺寸  
        val buttonSize by animateDpAsState(  
            targetValue = if (isLike) 32.dp else 24.dp,  
            animationSpec = tween(3000),  
            label = ""  
        )  
        //Animatable修改颜色  
        val buttonColor = remember { Animatable(Color.Gray) }  
  
        IconButton(onClick = {  
            isLike = !isLike  
        }) {  
            Icon(  
                imageVector = Icons.Rounded.Favorite,  
                contentDescription = null,  
                modifier = Modifier.size(buttonSize),  
                tint = buttonColor.value  
            )  
        }  
  
        LaunchedEffect(isLike) {  
            //animateTo  
            buttonColor.animateTo(  
                targetValue = if (isLike) Color.Red else Color.Gray,  
                animationSpec = tween(3000)  
            )  
        }  
          
    }

UI效果同上

Animatable中包括animateTo在内的许多API都是挂起函数,需要在CoroutineScope中执行,可以使用LaunchedEffect为其提供所需的环境。

在上面的例子中,我们创建了初始值为Color. Gray的Animatable,使用remember避免重组中的重复创建。当状态isLike更新时,Animatable的颜色值将以动画形式在Color. Red和Color. Gray之间转换。如果在动画中途重新修改了状态isLike,那么播放中的动画会被立即中断并开始新的动画。

Animatable是animate*AsState的更底层api,因此相对于animate*AsState,它具有更多灵活的能力,这体现在以下几个方面:

首先,Animatable允许设置一个不同的初始值,比如可以将IconButton的初始颜色设置为一个不同于Gray和Red的任意颜色,然后通过animateTo将其改变为目标颜色。

其次,Animatable除了animateTo之外,还提供了不少其他方法,比如snapTo可以立即到达目标值,中间没有过渡值,可以在需要跳过动画的场景中使用这个方法。animateDecay可以启动的一个衰减动画,这在fling等场景中非常有用。

需要注意的是多个animateTo是依次按顺序执行的,看下面的代码:

kotlin 复制代码
import androidx.compose.animation.Animatable  
import androidx.compose.animation.core.Animatable

@Preview  
    @Composable  
    fun TestAnimatable() {  
        var isLike by remember { mutableStateOf(false) }  
        //Animatable修改尺寸  
        val buttonSize = remember { Animatable(24.dp, Dp.VectorConverter) }  
        //Animatable修改颜色  
        val buttonColor = remember { Animatable(Color.Gray) }  
  
        LaunchedEffect(isLike) {  
            //animateTo  
            buttonColor.animateTo(  
                targetValue = if (isLike) Color.Red else Color.Gray,  
                animationSpec = tween(2000)  
            )  
            //animateTo  
            buttonSize.animateTo(  
                targetValue = if (isLike) 48.dp else 24.dp,  
                animationSpec = tween(2000)  
            )  
        }  
  
        IconButton(onClick = {  
            isLike = !isLike  
        }) {  
            Icon(  
                imageVector = Icons.Rounded.Favorite,  
                contentDescription = null,  
                modifier = Modifier.size(buttonSize.value),  
                tint = buttonColor.value  
            )  
        }  
    }

UI效果

Animatable传入名为Dp.VectorConverter的参数,这是一个针对Dp类型的TwoWayConverter。Animatable可以直接传入Float或Color类型的值,当传入其他类型时,需要同时指定对应的TwoWayConverter。Compose为常用数据类型都提供了对应的TwoWayConverter实现,直接传入即可,例如代码中的Dp. VectorConverter。

二、Transition过渡动画

Transition也是低级别动画API中的一类。AnimateState以及Animatable都是针对单个目标值的动画,而Transition可以面向多个目标值应用动画并保持它们同步结束。Transition的作用更像是传统视图体系动画中的AnimationSet。

注意:虽然这里的Transition与前面介绍的EnterTransition和ExitTransition等在名字上很容易混淆,但实际是不同的东西。

1、updateTransition

我们使用updateTransition创建一个Transition动画,通过一个例子看一下具体使用方式。

首先,Transition也需要依赖状态执行,需要枚举出所有可能的状态。

kotlin 复制代码
/**  
 * 密封类,代表二种状态  
 */  
sealed class SwitchState {  
    object OPEN : SwitchState()  
    object CLOSE : SwitchState()  
}

接下来需要创建一个MutableState表示当前开关的状态,并使用updateTransition基于这个状态创建Transition实例。

kotlin 复制代码
var selectedState by remember { mutableStateOf(SwitchState.CLOSE) }  
val transition = updateTransition(targetState = selectedState, label = "switch_transition")

updateTransition接收两个参数。targetState最重要,它是动画执行所依赖的状态。label是代表此动画的标签,可以在Android Studio动画预览工具中标识动画。

获取到Transition实例后,可以创建Transitioin动画中的所有属性状态。前面说过当开关被打开时,文案会逐渐消失,与此同时,底部逐渐上升选中的标签。所以这里需要两个属性状态:selectBarPadding与textAlpha。

使用animate*来声明每个动画属性其在不同状态时的数值信息,当Transition所依赖的状态发生改变时,其中每个属性状态都会得到相应的更新。

kotlin 复制代码
val selectBarPadding by transition.animateDp(  
            transitionSpec = {  
                tween(1000)  
            }, label = ""  
        ) {  
            when (it) {  
                SwitchState.CLOSE -> 40.dp  
                SwitchState.OPEN -> 0.dp  
            }  
        }  
        val textAlpha by transition.animateFloat(  
            transitionSpec = {  
                tween(1000)  
            }, label = ""  
        ) {  
            when (it) {  
                SwitchState.CLOSE -> 1f  
                SwitchState.OPEN -> 0f  
            }  
        }

可以为animate*设置transitionSpec参数,为属性状态制定不同的AnimationSpec,关于AnimationSpec我们会在后面一节详细介绍。接下来,仅需将创建好的状态应用到组件的对应属性中,完整代码如下:

kotlin 复制代码
    @Preview  
    @Composable  
    fun SwitchBlock() {  
        var selectedState: SwitchState by remember { mutableStateOf(SwitchState.CLOSE) }  
        val transition = updateTransition(selectedState, label = "switch transition")  
        val selectBarPadding by transition.animateDp(  
            transitionSpec = {  
                tween(1000)  
            }, label = ""  
        ) {  
            when (it) {  
                SwitchState.CLOSE -> 40.dp  
                SwitchState.OPEN -> 0.dp  
            }  
        }  
        val textAlpha by transition.animateFloat(  
            transitionSpec = {  
                tween(1000)  
            }, label = ""  
        ) {  
            when (it) {  
                SwitchState.CLOSE -> 1f  
                SwitchState.OPEN -> 0f  
            }  
        }  
  
        Box(  
            modifier = Modifier  
                .size(150.dp)  
                .padding(8.dp)  
                .clip(RoundedCornerShape(10.dp))  
                .clickable {  
                    selectedState = if (selectedState == SwitchState.OPEN) SwitchState.CLOSE else  
                        SwitchState.OPEN  
                }) {  
  
            Image(  
                painter = painterResource(id = R.mipmap.rabit2),  
                contentDescription = stringResource(R.string.description),  
                contentScale = ContentScale.FillBounds  
            )  
  
            Text(  
                text = "点我",  
                fontSize = 30.sp,  
                fontWeight = FontWeight.W900,  
                color = Color.White,  
                modifier = Modifier  
                    .align(Alignment.Center)  
                    .alpha(textAlpha)  
            )  
            Box(  
                modifier = Modifier  
                    .align(Alignment.BottomCenter)  
                    .fillMaxWidth()  
                    .height(40.dp)  
                    .padding(top = selectBarPadding)  
                    .background(Color.DarkGray)  
            ) {  
                Row(  
                    modifier = Modifier  
                        .align(Alignment.Center)  
                        .alpha(1 - textAlpha)  
                ) {  
                    Icon(  
                        imageVector = Icons.Filled.CheckCircle,  
                        contentDescription = "star",  
                        tint = Color.White  
                    )  
                    Spacer(modifier = Modifier.width(2.dp))  
                    Text(  
                        text = "已选择",  
                        fontSize = 20.sp,  
                        fontWeight = FontWeight.W900,  
                        color = Color.White  
                    )  
                }  
            }  
        }  
    }

UI效果

(1)createChildTransition创建子动画

Transition可以使用createChildTransition创建子动画,比如在下面的场景中。我们希望通过Transition来同步控制DialerButton和NumberPad的显隐,但是对于DailerButton和NumberPad来说,各自只需要关心自己的状态。通过createChildTranstion将DailerState转换成Boolean类型State,能够更好地实现关注点分离。子动画的动画数值计算来自于父动画,某种程度上说,createChildTransition更像是一种map。伪代码如下所示:

kotlin 复制代码
enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    ...
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    ...
}

@OptIn(ExperimentalTransitionApi::class)
@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "")
    Box {
        NumberPad(
            transition.createChildTransition {	//createChildTransition
                it == DialerState.NumberPad
            })
        DialerButton(
            transition.createChildTransition {	//createChildTransition
                it == DialerState.DialerMinimized
            })
    }
}

(2)与AnimatedVisibility和AnimatedContent配合使用

AnimatedVisibility和AnimatedContent有针对Transition的扩展函数,将Transition的State转换成所需的TargetState。借助这两个扩展函数,可以将AnimatedVisibility和AnimatedContent的动画状态通过Transition对外暴露,以供使用。

看下面的示例代码:

kotlin 复制代码
    @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
    @Composable
    fun TestAnimateCombine() {
        var selected by remember { mutableStateOf(false) }
        //当selected被更改时触发过渡动画
        val transition = updateTransition(selected, label = "")
        //定义动画
        //改变边框颜色
        val borderColor by transition.animateColor(label = "") { isSelected ->
            if (isSelected) Color.Magenta else Color.White
        }
        //改变维度
        val elevation by transition.animateDp(label = "") { isSelected ->
            if (isSelected) 10.dp else 2.dp
        }
        Surface(
            onClick = { selected = !selected },
            modifier = Modifier.size(200.dp),
            shape = RoundedCornerShape(8.dp),
            border = BorderStroke(2.dp, borderColor),
            tonalElevation = elevation,
            shadowElevation = elevation
        ) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            ) {
                //AnimatedVisibility作为过渡动画的一部分
                //扩展函数
                transition.AnimatedVisibility(
                    visible = { targetSelected -> targetSelected },
                    enter = expandVertically(),
                    exit = shrinkVertically()
                ) {
                    Text(text = "It is fine today.")
                }
                //AnimatedContent作为过渡动画的一部分
                //扩展函数
                transition.AnimatedContent { targetState ->
                    if (targetState) {
                        Text(text = "Selected")
                    } else {
                        Icon(
                            imageVector = Icons.Default.Phone, contentDescription = stringResource(
                                R.string.description
                            )
                        )
                    }
                }
            }
        }
    }

UI效果

对于AnimatedContent扩展函数来说,transition所包含的状态数值会被转换成targetValue参数传入。而对于AnimatedVisibility扩展函数来说,则需要通过转换器将包含的状态数值转换成其所需的Boolean类型visible参数。

(3)封装并复用Transition动画

在简单的场景下,在用户界面中使用UpdateTransition创建Transition并直接操作它完成动画是没有问题的。然而,如果需要处理一个具有许多动画属性的复杂场景,可能希望把Transition动画的实现与用户界面分开。

可以通过创建一个持有所有动画值的类和一个返回该类实例的"更新"函数来做到这一点。Transition动画的实现被提取到单独的函数中,便于后续进行复用。

下面是一种方式和思路:

kotlin 复制代码
    //通过枚举定义状态
    enum class BoxState { Collapsed, Expanded }

    //通过BoxState枚举定义状态
    //使用示例
    @Composable
    fun AnimatingBox(boxState: BoxState) {
        //updateTransitionData通过传入的枚举类型获取动画数据
        val transitionData = updateTransitionData(boxState)
        //将动画数据赋值给UI树
        Box(
            modifier = Modifier
                .background(color = transitionData.color)
                .size(transitionData.size)
        )
    }


    //自定义保存动画数值的类
    //import androidx.compose.runtime.State
    private class TransitionData(
        color: State<Color>,
        size: State<Dp>
    ) {
        val color by color
        val size by size
    }

    //创建一个Transition并返回其动画值
    //通过BoxState枚举定义状态
    @Composable
    private fun updateTransitionData(boxState: BoxState): TransitionData {
        val transition = updateTransition(boxState, label = "")
        val color = transition.animateColor(label = "") { state ->
            when (state) {
                BoxState.Collapsed -> Color.Gray
                BoxState.Expanded -> Color.Red
            }
        }
        val size = transition.animateDp(label = "") { state ->
            when (state) {
                BoxState.Collapsed -> 64.dp
                BoxState.Expanded -> 128.dp
            }
        }
        return remember(transition) { TransitionData(color, size) }
    }

2、rememberInfiniteTransition

InfinitTransition从名字上便可以知道其就是一个无限循环版的Transition。一旦动画开始执行,便会不断循环下去,直至Composable生命周期结束。

子动画可以用animateColor、animatedFloat或animatedValue等进行添加,另外还需要指定infiniteRepeatableSpec来设置动画循环播放方式。

看下面的代码示例:

kotlin 复制代码
    @Composable
    fun TestInfiniteTransition() {
        val infiniteTransition = rememberInfiniteTransition(label = "")
        val color by infiniteTransition.animateColor(
            initialValue = Color.Red,//初始值
            targetValue = Color.Green, //最终值
            animationSpec = infiniteRepeatable(   //创建一个可无限次重复的InfiniteRepeatableSpec
                //一个动画值的转换持续1秒,缓和方式为LinearEasing
                animation = tween(1000, easing = LinearEasing),
                repeatMode = RepeatMode.Reverse
                //指定动画重复运行的方式
                // Reverse为initialValue->targetValue,targetValue->initialValue
                // Repeat为initialValue->targetValue,initialValue->targetValue
            ), label = ""
        )
        Box(
            Modifier
                .fillMaxSize()
                .background(color)
        )
    }

UI效果

在上面的例子中,使用infiniteRepeatable创建了infiniteRepeatableSpec,其中使用tween创建一个单次动画的AnimationSpec,这是一个持续时长1000 ms的线性衰减动画。通过repeatMode参数指定动画循环播放方式为Reverse,此外还有一种方式是Repeat。从名字上可以直观地看出两者的区别。Reverse会在达到结束状态后,原路返回起始状态重新开始,而Repeat则会从立即回到起始状态重新开始。

三、AnimationSpec动画规格

在前面出现的代码中多次见到animationSpec参数。大部分的Compose动画API都支持通过animationSpec参数定义动画效果:

kotlin 复制代码
val alpha: Float by animateFloatAsState(
 targetValue = if (enabled) 1f else 0.5f,
    //设置一个时长300ms的补间动画
 animationSpec = tween(durationMillis =300, easing = FastOutSlowInEasing)
 )

在上面的代码中使用tween创建了一个AnimationSpec实例。

看一下AnimationSpec源码:

kotlin 复制代码
interface AnimationSpec<T> {  
 
    fun <V : AnimationVector> vectorize(  
        converter: TwoWayConverter<T, V>  
    ): VectorizedAnimationSpec<V>  
}

AnimatioinSpec是一个单方法接口,泛型T是当前动画数值类型,vectorize用来创建一个VectorizedAnimationSpec,即一个矢量动画的配置。矢量动画是通过函数运算生成的,而AnimationVector就是用来参与计算的动画矢量。TwoWayConverter将T类型的状态值转换成参与动画计算的矢量数据。

Compose提供了多种AnimationSpec的子类,分别基于不同的VectorizedAnimationSpec实现不同动画效果的计算。例如TweenSpec用来实现两点间的补间动画,SpringSpec实现基于物理效果的动画,SnapSpec是一个即时生效的动画。

接下来看看各种AnimationSpec的构建,以及它们所提供的实际动画效果。

1、spring弹跳动画

使用spring会创建一个SpringSpec实例,可用来创建一个基于物理特性的弹跳动画,它的动画估值将在当前值和目标值之间按照弹簧物理运动轨迹进行变化。看下面一段代码:

kotlin 复制代码
val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,  //dampingRatio
        stiffness = Spring.StiffnessMedium    //stiffness
    ), label = ""
)

spring有三个参数:dampingRatio、stiffness和visibilityThreshold,前两个参数主要用来控制弹跳动画的动画效果。源码:

kotlin 复制代码
@Stable
fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
): SpringSpec<T> =
    SpringSpec(dampingRatio, stiffness, visibilityThreshold)

(1)dampingRation

dampingRation表示弹簧的阻尼比。阻尼比用于描述弹簧振动逐渐衰减的状况。阻尼比可以定义振动从一次弹跳到下一次弹跳所衰减的速度有多快。以下是不同阻尼比下的弹力衰减情况:

  • 当dampingRation>1时会出现过阻尼现象,这会使对象快速地返回到静止位置。
  • 当dampingRation=1时会出现临界阻尼现象,这会使对象在最短时间内返回到静止位置。
  • 当1>dampingRation>0时会出现欠阻尼现象,这会使对象围绕最终静止位置进行多次反复震动。
  • 当dampingRation=0时会出现无阻尼现象,这会使对象永远振动下去。

注意阻尼比不能小于零。

Compose为spring提供了一组常用的阻尼比常量。

kotlin 复制代码
object Spring {  
const val StiffnessHigh = 10_000f  
const val StiffnessMedium = 1500f    
const val StiffnessMediumLow = 400f  
const val StiffnessLow = 200f  
const val StiffnessVeryLow = 50f  
  
const val DampingRatioHighBouncy = 0.2f  
const val DampingRatioMediumBouncy = 0.5f   
const val DampingRatioLowBouncy = 0.75f   
const val DampingRatioNoBouncy = 1f 

const val DefaultDisplacementThreshold = 0.01f  
}

如果不额外指定,默认会采用DampingRatioNoBouncy。此时会出现临界阻尼现象,对象会在很短的时间内恢复静止而不发生振动。

(2)stiffness

stiffness定义弹簧的刚度值。刚度值越大,弹簧到静止状态的速度越快。Compose为stiffness定义的常量如上段代码所示。

stiffness的默认值为StiffnessMedium,表示到静止过程的速度适中。很多动画API内部对AnimationSpec使用的默认值均为spring,例如animate*AsState以及updateTransition等。因为spring的动画效果基于物理原理,使动画更加真实自然。

注意:刚度值必须大于0。

(3)visibilityThreshold

spring的最后一个参数visibilityThreshold是一个泛型,此泛型与targetValue类型保持一致。由开发者指定一个阈值,当动画到达这个阈值时,动画会立即停止。

2、tween补间动画

使用tween可以创建一个TweenSpec实例,TweenSpec是DurationBasedAnimationSpec的子类。从基类名字可以感受到,TweenSpec的动画必须在规定时间内完成,所以它不能像SpringSpec那样完全基于物理规律进行动画,它的动画效果是基于时间参数计算的,可以使用Easing来指定不同的时间曲线动画效果。

tween有三个参数,如下所示:

kotlin 复制代码
  val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
        durationMillis = 300,//动画执行时间(ms)
        delayMillis = 50,//可以指定动画的延迟执行
        easing = LinearOutSlowInEasing//衰减曲线动画效果
    ), label = ""
)

3、keyframes关键帧动画

相对于tween动画只能在开始和结束两点之间应用动画效果,keyframes可以更精细地控制动画,它允许在开始和结束之间插入关键帧节点,节点与节点之间的动画过渡可以应用不同效果。

kotlin 复制代码
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
    }, label = ""
)

keyframes会返回一个KeyFramesSpec实例,由于它也是DurationBasedAnimationSpec的子类,所以它也是一种需要在规定时间内完成的动画。关键帧节点的定义就是由时间戳、动画数值,以及动画效果等组成。比如0.2f at 15 with FastOutLinearInEasing表示在15ms时刻value应该达到0.2f,并且使用FastOutLinearInEasing动画效果。keyframes内部通过采用中缀运算符的配置方式使整个配置过程更加友好。

4、repeatable循环动画

使用repeatable可以创建一个RepeatableSpec实例。前面所介绍的动画都是单次动画,而这里的repeatable是一个可循环播放的动画,可以指定TweenSpec或者KeyFramesSpec以及循环播放的方式。

kotlin 复制代码
val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = repeatable(
        iterations = 3,
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    ), label = ""
)

上面的代码表示,让一个tween动画循环播放三次,循环模式是往返执行。我们一共有两种循环模式可选:

  • RepeatMode. Reverse:往返执行,状态值达到目标值后,再原路返回到初始值。
  • RepeatMode. Restart:从头执行,状态值达到目标值后,立即从初始值重新开始执行。

注意:repeatable函数的参数animation必须是一个DurationBasedAnimationSpec子类,spring不支持循环播放。这是可以理解的,因为一个永动的弹簧确实违背物理定律。

5、infiniteRepeatable无限循环动画

infiniteRepeatable顾名思义,就是无限执行的RepeatableSpec,因此没有iterations参数。它将创建并返回一个InfiniteRepeatableSpec实例。

kotlin 复制代码
val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    ),
    label = ""
)

前面介绍过rememberInfiniteTransition,这是一种无限循环的Transition动画,因此它只能对无限循环的动画进行组合,它的animationSpec必须使用infiniteRepeatable来创建。下面是一个示例代码:

kotlin 复制代码
@Preview
@Composable
fun InfinitRepetableDemo() {
    val infiniteTransition = rememberInfiniteTransition(label = "")
    val degrees by infiniteTransition.animateFloat(   //定义旋转角度
        initialValue = 0f,
        targetValue = 359f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1500
                0F at 0
                359F at 1500
            }), label = ""
    )

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "Hello Compose",
            modifier = Modifier.rotate(degrees = degrees)   //使用动画的旋转角度
        )
    }
}

UI效果

如上所述,infiniteTransition通过animateFloat添加了一个Float类型的动画,此处animationSpec必须指定一个InfiniteRepeatableSpec类型实例,这里创建了一个无限循环的关键帧动画。

6、snap快闪动画

snap会创建一个SnapSpec实例,这是一种特殊动画,它的targetValue发生变化时,当前值会立即更新为targetValue。由于没有中间过渡,动画会瞬间完成,常用于跳过过场动画的场景。我们也可以设置delayMillis参数来延迟动画的启动时间。

kotlin 复制代码
val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = snap(delayMillis = 50), label = ""
)

下面是一个示例,通过snap立刻修改值

kotlin 复制代码
@Composable
private fun SnapAnimationExample() {
    var targetValue by remember { mutableFloatStateOf(0f) }

    //创建动画使用SnapSpec
    val animatedValue by animateValueAsState(
        targetValue = targetValue,
        typeConverter = Float.VectorConverter,
        animationSpec = snap(delayMillis = 1000), label = ""   //延迟1秒执行
    )

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        //创建一个按钮修改targetValue的值
        Button(
            onClick = {
                targetValue = if (targetValue == 0f) 1f else 0f   //点击后修改targetValue的值
            },
            modifier = Modifier.padding(8.dp)
        ) {
            Text("Toggle Animation")
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Display the animated value
        //修改文本
        Text("Animated Value: $animatedValue", style = MaterialTheme.typography.bodyMedium)
    }
}

UI效果

7、使用Easing控制动画节奏

在介绍tween动画时提到过Easing。我们知道Tween与Keyframes都是基于时间计算的动画,Easing本质上就是一个基于时间参数的函数(实际是一个单方法接口),它的输入和输出都是0f~1f的浮点数值。

kotlin 复制代码
package androidx.compose.animation.core

@Stable  
fun interface Easing {  
    fun transform(fraction: Float): Float  
}

输入值表示当前动画在时间上的进度,返回值是则是当前value的进度,1.0表示已经达到targetValue。不同的Easing算法可以实现不同的动画加速、减速效果,因此也可以将Easing理解为动画的瞬时速度。Compose内部提供了多种内置的Easing曲线,可满足大多数的使用场景,如下图所示。

另外还可以使用CubicBezierEasing三阶贝塞尔曲线自定义任意Easing,上述几种预设的曲线也都是使用CubicBezierEasing实现的。

kotlin 复制代码
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)  
 
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)  
  
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)  
  
val LinearEasing: Easing = Easing { fraction -> fraction }

四、AnimationVector动画矢量值

矢量动画是基于动画矢量值AnimationVector计算的。前面的章节中我们了解到,animae*AsState基于Animatable将Color、Float、Dp等数据类型的数值转换成可动画类型,其本质就是将这些数据类型转换成AnimationVector参与动画计算。

Animatable的构造函数有三个参数:

kotlin 复制代码
@Suppress("NotCloseable")  
class Animatable<T, V : AnimationVector>(  
    initialValue: T,     //T类型的动画初始值
    val typeConverter: TwoWayConverter<T, V>,   //将T类型的数值与V类型的AnimationVector进行转换
    private val visibilityThreshold: T? = null,   //动画消失的阈值,默认为null
    val label: String = "Animatable"  
) {..}

1、TwoWayConverter

源码如下:

kotlin 复制代码
interface TwoWayConverter<T, V : AnimationVector> {  
    val convertToVector: (T) -> V   
    val convertFromVector: (V) -> T  
}

从TwoWayConverter接口定义可以看出,它可以将任意T类型的数值转换为标准的AnimationVector,反之亦然。这样,任何数值类型都可以随着动画改变数值。

不同类型的数值可以根据需求与不同的AnimationVectorXD进行转换,这里的X代表了信息的维度。例如一个Int可以与AnimationVector1D相互转换,AnimationVector1D只包含一个浮点数信息。

kotlin 复制代码
private val IntToVector: TwoWayConverter<Int, AnimationVector1D> =  
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })

同样,Size中包含width和height两个维度的信息,可以与AnimationVector2D进行转换,Color中包含red、green、blue和alpha 4个数值,可以与AnimatIonVector4D进行转换。当然Compose已经为常用类型提供了TwoWayConverter的拓展实现,可以在这些类型的伴生对象中找到它们,并且可以在animate*AsState中直接使用。

kotlin 复制代码
package androidx.compose.animation.core
 
val Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>  
get() = FloatToVector  
 
val Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>  
get() = IntToVector

val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>  
get() = RectToVector  
  
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>  
get() = DpToVector  

val DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>  
get() = DpOffsetToVector  

val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>  
get() = SizeToVector  
  
val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>  
get() = OffsetToVector  
  
val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>  
get() = IntOffsetToVector  
   
val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>  
get() = IntSizeToVector

2、自定义实现TwoWayConverter

对于没有提供默认支持的数据类型,可以为其自定义对应的TwoWayConverter。例如针对MySize这个自定义类型来自定义实现TwoWayConverter,然后使用animateValueAsState为MySize添加动画效果。

kotlin 复制代码
data class MySize(val width: Dp, val height: Dp)

@Composable
fun MyAnimation(targetSize: MySize) {
    val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
        targetSize,
        TwoWayConverter(
            convertToVector = { size: MySize ->
                //Extract a float value from each of the 'Dp  fields
                AnimationVector2D(size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector2D ->
                MySize(vector.v1.dp, vector.v2.dp)
            }), label = ""
    )
}

参考资料

本文为学习博客,内容来自书籍《Jetpack Compose 从入门到实战》,代码为具体实践。致谢!

相关推荐
Mr_Xuhhh9 分钟前
程序地址空间
android·java·开发语言·数据库
呆呆小雅1 小时前
C# 结构体
android·java·c#
ᥬ 小月亮4 小时前
Layui表格的分页下拉框新增“全部”选项
android·javascript·layui
sunly_13 小时前
Flutter:启动屏逻辑处理02:启动页
android·javascript·flutter
Sgq丶14 小时前
Android Studio 配置 proto
android·ide·android studio
_小马快跑_17 小时前
ConstraintLayout 中的ImageFilterView探索:处理图片圆角、亮度、饱和度、图片重叠等
android
IT-sec18 小时前
jquery-picture-cut 任意文件上传(CVE-2018-9208)
android·前端·javascript·安全·web安全·网络安全·jquery
xiaoduyyy19 小时前
【Android】RecyclerView回收复用机制
android
林北芒大果19 小时前
【Flutter】搭建Flutter开发环境,安卓开发
android·flutter
m0_7482302121 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql