Jetpack Compose 动画

2022-12-06/2022-12-01

在compose中,没有属性动画,而是使用了一套全新的动画API

状态转移型动画 animateXxxAsState

以需要给box的size从46dp平滑扩大到96dp为例

fun AnimateAsState() {

    var isSmall by remember { mutableStateOf(true) }
    val size = animateDpAsState(if (isSmall) 46.dp else 96.dp)

    Box(Modifier.size(size.value).clickable {
        isSmall = !isSmall
    }.background(Color.Green)) {

    }
}

在使用animateAsState动画时不需要使用remember因为在内部实现,同样不需要使用mutableStateOf包裹,同样是因为内部已经实现

不可以使用by 因为by是说把左边的读写交给右边代理

这样写会报错

var size by animateDpAsState(if (isSmall) 46.dp else 96.dp)
    
    Type 'State<Dp>' has no method 'setValue(Nothing?, KProperty<*>, Dp)' and thus it cannot serve as a delegate for var (read-write property)

因为animateDpAsState只可以读,不可以写

我们无法手动更改animateDpAsState里面的值,但可以通过recompose时里面的值发生了变化从而由animateDpAsState自动改变

那我们怎么定义动画呢?需要在最开始就定义好,然后通过在recompose时改变 animateDpAsState里面的值

可以使用的animateAsState动画

image-20221122082919884

再看一个多个状态的例子

private object SizeState {
    val SMALL_SIZE = 46.dp
    val MIDDLE_SIZE = 96.dp
    val LARGE_SIZE = 142.dp
}

@Composable
fun AnimateAsState() {
    
    var size by remember { mutableStateOf(SizeState.SMALL_SIZE) }
    val sizeAnim = animateDpAsState(size)

    Box(Modifier.size(sizeAnim.value).clickable {
        if (size == SizeState.SMALL_SIZE) {
            size = SizeState.MIDDLE_SIZE
        } else if (size == SizeState.MIDDLE_SIZE) {
            size = SizeState.LARGE_SIZE
        } else if (size == SizeState.LARGE_SIZE) {
            size = SizeState.SMALL_SIZE
        }
    }.background(Color.Green)) {

    }
}

Animatable

animAsState是对Animatable使用便捷性的扩展,无法指定初值,功能受到了限制,如果需要可以使用Animatable

Animatable需要传Float类型的参数,如果传其它类型的参数需要定义TwoWayConverter指定怎么将其它类型的参数转化为Float以及将Float转为其它类型的参数

以Dp为例

val anim = remember { Animatable(8.dp, Dp.VectorConverter) }
    
/**
 * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
 */
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
    get() = DpToVector

ps:其实float类型也有一个TwoWayConverter Float.VectorConverter

val anim = remember { Animatable(8f) }
    
    fun Animatable(
    initialValue: Float,
    visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) = Animatable(
    initialValue,
    Float.VectorConverter,
    visibilityThreshold
)

private val FloatToVector: TwoWayConverter<Float, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it) }, { it.value })

但其实AnimationVector1D只是对float简单的包装,在Float.VectorConverter里的转换规则可以看出这一点

一个使用的简单例子

@Composable
fun AnimatableLearn() {
    var big by remember { mutableStateOf(false) }
    var size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    //不会重复执行
    LaunchedEffect(Unit) {
        anim.animateTo(size)
    }
    
    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

Animatable动画的启动要在协程中,并且我们不能直接使用lifecycleScope.launch

会报错并且提示Calls to launch should happen inside a LaunchedEffect and not composition

这是因为这样开启的协程并没有做compose重组的支持从而导致协程的重复执行

我们可以使用 LaunchedEffect,跟随一个key和要执行的代码块,如果只需要执行一次就填入Unit的key

如何设置初始值呢

使用snapTo函数

@Composable
fun AnimatableLearn() {
    var big by remember { mutableStateOf(false) }
    var size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    //不会重复执行
    LaunchedEffect(Unit) {
        //here
        anim.snapTo(if(big) 192.dp else 0.dp)
        anim.animateTo(size)
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

AnimationSpec

AnimationSpec定义了动画的具体配置

AnimationSpec的继承树:

image-20221122180303327

TweenSpec

AnimationSpec可以用来设置动画的变化曲线以及持续时间,延迟开始事件

首先介绍TweenSpec

使用示例:

@Composable
fun TweenSpecLearn() {
    var big by remember { mutableStateOf(false) }
    val size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    LaunchedEffect(big) {
        //这俩种方式都可以 第二种写法方便些
//        anim.animateTo(size, TweenSpec(1000, 0, LinearEasing))
        anim.animateTo(size, tween(1000, 0, LinearEasing))
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

TweenSpec参数解释

/**
 * Creates a TweenSpec configured with the given duration, delay, and easing curve.
 *
 * @param durationMillis duration of the [VectorizedTweenSpec] animation.
 * @param delay the number of milliseconds the animation waits before starting, 0 by default.
 * @param easing the easing curve used by the animation. [FastOutSlowInEasing] by default.
 */
@Immutable
class TweenSpec<T>(
    val durationMillis: Int = DefaultDurationMillis,
    val delay: Int = 0,
    val easing: Easing = FastOutSlowInEasing
) : DurationBasedAnimationSpec<T>

前俩个很好理解,最后一个easing可以设置动画的变化曲线

有四种默认的

/**
 * Elements that begin and end at rest use this standard easing. They speed up quickly
 * and slow down gradually, in order to emphasize the end of the transition.
 *
 * Standard easing puts subtle attention at the end of an animation, by giving more
 * time to deceleration than acceleration. It is the most common form of easing.
 *
 * This is equivalent to the Android `FastOutSlowInInterpolator`
 */
//先加速再减速 适合从一个状态到另一个状态
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)

/**
 * Incoming elements are animated using deceleration easing, which starts a transition
 * at peak velocity (the fastest point of an element’s movement) and ends at rest.
 *
 * This is equivalent to the Android `LinearOutSlowInInterpolator`
 */
//减速进入 适合元素消失
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)

/**
 * Elements exiting a screen use acceleration easing, where they start at rest and
 * end at peak velocity.
 *
 * This is equivalent to the Android `FastOutLinearInInterpolator`
 */
//加速进入 适合元素进入
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)

/**
 * It returns fraction unmodified. This is useful as a default value for
 * cases where a [Easing] is required but no actual easing is desired.
 */
//线性变化 应用场景较少
val LinearEasing: Easing = Easing { fraction -> fraction }

除此之外,还可以自定义CubicBezierEasing曲线

https://cubic-bezier.com/

SnapSpec

类似于snapTo 可以让动画突变,区别是SnapSpec可以设置延时

@Composable
fun TweenSpecLearn() {
    var big by remember { mutableStateOf(false) }
    val size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    LaunchedEffect(big) {
//        anim.animateTo(size, TweenSpec(1000, 0, LinearEasing))
//        anim.animateTo(size, tween(1000, 0, LinearEasing))
        anim.animateTo(size, SnapSpec(1000))
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

KeyframeSpec

可以用来定义关键帧

@Composable
fun KeyframeSpecLearn() {
    var big by remember { mutableStateOf(false) }
    val size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    LaunchedEffect(big) {
        anim.animateTo(size, keyframes {
            durationMillis = 450
            delayMillis = 100
            //表示在150ms时变到144dp 同时使用with中缀函数指定速度曲线 若不填则默认是线性变化
            144.dp at 150 with FastOutSlowInEasing //一定要注意这里是150到300ms的速度曲线 而不是 0-150ms的
            //表示在300ms时变到20dp
            20.dp at 300
        })
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

SpringSpec

SpringSpec基于弹簧物理模型,无法定义精确的运动时间,可以通过阻尼比dampingRatio和刚度stiffness来定义动画的形式

代码示例

@Composable
fun SpringSpecLearn() {
    var big by remember { mutableStateOf(false) }
    val size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    LaunchedEffect(big) {
        anim.animateTo(size, spring(Spring.DampingRatioHighBouncy, StiffnessLow))

        //炸弹效果
        anim.animateTo(48.dp, spring(Spring.DampingRatioHighBouncy, StiffnessMedium), 2000.dp)
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

spring接收三个参数

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

dampingRatio为阻尼比,可以理解成弹簧的回弹次数,这个值越小回弹次数越多

stiffness为刚度,这个值越大回弹速度越快

visibilityThreshold 定义弹簧回弹幅度到哪个界限时停止弹动 这个值越小越精确,过大会导致弹簧突然跃变到原位

RepeatableSpec

RepeatableSpec 可以让动画重复执行 需要和DurationBasedAnimationSpec配合使用(即tween snap keyframe)

示例代码

@Composable
fun RepeatableSpecLearn() {
    var big by remember { mutableStateOf(false) }
    val size = remember(big) { if (big) 96.dp else 48.dp }

    val anim = remember { Animatable(size, Dp.VectorConverter) }

    LaunchedEffect(big) {
        anim.animateTo(
            size, RepeatableSpec(
                3, tween(), RepeatMode.Reverse,
                StartOffset(500, StartOffsetType.FastForward)
            )
        )
    }

    Box(
        Modifier.size(anim.value)
            .background(Color.Green).clickable {
                big = !big
            }
    )
}

参数说明:

class RepeatableSpec<T>(
	//重复次数
    val iterations: Int,
    //真正执行的Spec
    val animation: DurationBasedAnimationSpec<T>,
    //有俩种模式Restart和Reverse
    //[Restart] will restart the animation and animate from the start value to the end value.
    //[Reverse] will reverse the last iteration as the animation repeats.
    val repeatMode: RepeatMode = RepeatMode.Restart,
    //默认动画启动的延时 StartOffset接收俩个参数 一个是时间,还有一个是StartOffsetType
    //Delay是延迟动画的启动时间 FastForward是快进动画的时间
    val initialStartOffset: StartOffset = StartOffset(0)
)

animateDecay

消散型动画,常用来滑动减速,需要指定初始速度,不指定目标值

使用示例:

@Composable
fun AnimateDecayLearn() {

    val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
    val decay = remember { exponentialDecay<Dp>() }
//    val decay = rememberSplineBasedDecay<Dp>()
    LaunchedEffect(Unit) {
        delay(1000)
        anim.animateDecay(1000.dp, decay)
    }

    Box(
        Modifier
            .padding(0.dp, anim.value, 0.dp, 0.dp)
            .size(100.dp)
            .background(Color.Green)
    )
}

DecayAnimationSpec

DecayAnimationSpec和AnimationSpec是独立的俩个接口,我们可以使用rememberSplineBasedDecay和exponentialDecay

前者是安卓原生recyclerview和listview以及scrollview中的滑动算法,专门争对px设计,会根据像素密度对滑动进行修正

后者没有专门争对px设计,适用于比如角度的旋转或者需要使用dp的场景

如果需要争对px做动画,则用SplineBasedDecay,其他情况可以用exponentialDecay

使用block参数监听每一帧

block高阶函数会在动画的每一帧都被执行一次

示例:让俩个小方块齐头并进

@Composable
fun BlockParaLearn() {
    val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
    var padding = 0.dp
    val decay = rememberSplineBasedDecay<Dp>()
    LaunchedEffect(Unit) {
        delay(1000)
        anim.animateDecay(1000.dp, decay) {
            padding = value
        }
    }

    Row {
        Box(
            Modifier
                .padding(0.dp, anim.value, 0.dp, 0.dp)
                .size(100.dp)
                .background(Color.Green)
        )
        Box(
            Modifier
                .padding(0.dp, padding, 0.dp, 0.dp)
                .size(100.dp)
                .background(Color.Red)
        )
    }
}

动画的取消

一个动画开始取消另外一个动画

@Composable
fun AnimationCancellation() {
    val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
    val anim1 = remember { Animatable(0.dp, Dp.VectorConverter) }
    val padding = remember { mutableStateOf(0.dp) }
    val decay = rememberSplineBasedDecay<Dp>()
    LaunchedEffect(Unit) {
        delay(1000)
        anim.animateDecay(1000.dp, decay) {
            padding.value = value
        }
    }

    LaunchedEffect(Unit) {
        delay(1500)
        anim1.animateDecay(1500.dp, decay) {
            padding.value = value
        }
    }

    Box(
        Modifier
            .padding(0.dp, padding.value, 0.dp, 0.dp)
            .size(100.dp)
            .background(Color.Green)
    )
}

stop取消,注意不能在开启动画的协程里

可以看到方块的运动会突停

@Composable
fun AnimationCancellation() {
    val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
    val padding = remember { mutableStateOf(0.dp) }
    val decay = rememberSplineBasedDecay<Dp>()
    LaunchedEffect(Unit) {
        delay(1000)
        anim.animateDecay(2000.dp, decay) {
            padding.value = value
        }
    }

    LaunchedEffect(Unit) {
        delay(1100)
        anim.stop()
    }

    Box(
        Modifier
            .padding(0.dp, padding.value, 0.dp, 0.dp)
            .size(100.dp)
            .background(Color.Green)
    )
}

上下界 anim.updateBounds

@Composable
fun AnimationCancellation() {
    val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
    anim.updateBounds(0.dp, 1000.dp)
    val padding = remember { mutableStateOf(0.dp) }
    val decay = rememberSplineBasedDecay<Dp>()
    LaunchedEffect(Unit) {
        anim.animateDecay(2000.dp, decay) {
            padding.value = value
        }
    }

    Box(
        Modifier
            .padding(0.dp, padding.value, 0.dp, 0.dp)
            .size(100.dp)
            .background(Color.Green)
    )
}

animateDecay的返回值是一个AnimationResult

class AnimationResult<T, V : AnimationVector>(
    /**
     * The state of the animation in its last frame before it's canceled or reset. This captures
     * the animation value/velocity/frame time, etc at the point of interruption, or before the
     * velocity is reset when the animation finishes successfully.
     */
    val endState: AnimationState<T, V>,
    /**
     * The reason why the animation has ended. Could be either of the following:
     * -  [Finished], when the animation finishes successfully without any interruption
     * -  [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
     *    [upperBound][Animatable.upperBound] in any dimension, the animation will end with
     *    [BoundReached] being the end reason.
     */
    val endReason: AnimationEndReason
) {
    override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
}

enum class AnimationEndReason {
    /**
     * Animation will be forced to end when its value reaches upper/lower bound (if they have
     * been defined, e.g. via [Animatable.updateBounds])
     *
     * Unlike [Finished], when an animation ends due to [BoundReached], it often falls short
     * from its initial target, and the remaining velocity is often non-zero. Both the end value
     * and the remaining velocity can be obtained via [AnimationResult].
     */
    BoundReached,
    /**
     * Animation has finished successfully without any interruption.
     */
    Finished
}

里面可以返回动画停止的原因和停止时的状态比如速度

Transition

相比较于原来的Animatable针对于单个属性做动画,Transition更加适合于给多个属性做动画

Animatable创建了多个对象 多个协程

Transition一个协程

同时 transition可以使用preview功能方便的调试动画(需要加上label属性)

@Composable
fun TransitionSquare() {
    var big by remember { mutableStateOf(false) }
    val transition = updateTransition(big, label = "big")
    val size by transition.animateDp(label = "size") { if (it) 96.dp else 48.dp }
    val corner by transition.animateDp(label = "corner") { if (it) 0.dp else 18.dp }
    //原来我们的写法。也可以实现同样的
//    val size by animateDpAsState(if (big) 96.dp else 58.dp)
//    val corner by animateDpAsState(if (big) 0.dp else 18.dp)
    
    Box(
        Modifier
            .size(size)
            .clip(RoundedCornerShape(corner))
            .background(Color.Green)
            .clickable {
                big = !big
            }
    )
}

image-20221129144735980

spec参数

可以根据不同的状态设置不同的spec

@Preview
@Composable
fun TransitionSquare() {
    var big by remember { mutableStateOf(false) }
    val transition = updateTransition(big, label = "big")
    val size by transition.animateDp({
        when {
            //根据状态选择不同的AnimationSpec
            false isTransitioningTo true -> spring()
            else -> tween()
        }
    }, label = "size") { if (it) 96.dp else 48.dp }
    val corner by transition.animateDp(label = "corner") { if (it) 0.dp else 18.dp }

    Box(
        Modifier
            .size(size)
            .clip(RoundedCornerShape(corner))
            .background(Color.Green)
            .clickable {
                big = !big
            }
    )
}

AnimatedVisibility

AnimatedVisibility是对Transition动画的封装,可以用来控制组件出现和消失的动画

它只可以用于一个组件的布局,多个组件需要使用多个AnimatedVisibility

基本使用

@Composable
fun AnimatedVisibilityColumn() {
    Card(Modifier.fillMaxSize()) {
        var expanded by remember { mutableStateOf(false) }
        Column(
            Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            AnimatedVisibility(expanded) {
                TransitionSquare()
            }
            Button(onClick = { expanded = !expanded }) {
                Text("expand")
            }
        }
    }
}

AnimatedVisibility参数

fun ColumnScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    label: String = "AnimatedVisibility",
    content: @Composable AnimatedVisibilityScope.() -> Unit
)

重点介绍enter参数

可以配置

fadeIn()淡入淡出动画

slideIn()滑入动画

expandIn()裁切动画 expandin expandFrom设置裁切方向 clip设置是否裁切,默认为true,如果为false,则动画元素不裁切直接出现,其它元素发生缓慢位移,若为true则其它元素会直接到动画后的位置

scaleIn()伸缩动画

这些xxxIn()的参数可以填 animationSpec和初始值

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityColumn() {
    Card(Modifier.fillMaxSize()) {
        var expanded by remember { mutableStateOf(false) }
        Column(
            Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            AnimatedVisibility(expanded, enter = fadeIn(tween(durationMillis = 5000), 0.3f)) {
                TransitionSquare()
            }
            AnimatedVisibility(
                expanded,
                enter = slideIn(
                    tween(durationMillis = 5000),
                    initialOffset = { fullSize: IntSize ->
                        //fullSize是动画元素的宽高
                        IntOffset(
                            -fullSize.width,
                            -fullSize.height
                        )
                    })
            ) {
                TransitionSquare()
            }
            AnimatedVisibility(
                expanded,
                enter = scaleIn(
                    tween(durationMillis = 5000),
                    //从x = 0 y = 0 开始伸缩
                    transformOrigin = TransformOrigin(0f, 0f)
                )
            ) {
                TransitionSquare()
            }
            AnimatedVisibility(
                expanded,
                // expandFrom可以控制从哪里开始裁切
                enter = expandIn(tween(durationMillis = 5000), expandFrom = Alignment.TopCenter)
            ) {
                TransitionSquare()
            }
            Button(onClick = { expanded = !expanded }) {
                Text("start")
            }
        }
    }
}

enter参数的默认参数右边的expandVertically就是由上面所说的expandIn配制而成

@Stable
fun expandVertically(
    animationSpec: FiniteAnimationSpec<IntSize> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntSize.VisibilityThreshold
        ),
    expandFrom: Alignment.Vertical = Alignment.Bottom,
    clip: Boolean = true,
    initialHeight: (fullHeight: Int) -> Int = { 0 },
): EnterTransition {
    return expandIn(animationSpec, expandFrom.toAlignment(), clip) {
        IntSize(it.width, initialHeight(it.height))
    }
}

+号重载符做的是+号左边如果对于上面所提到的四个属性自己没有则取+号右边的,自己有则取自己的

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
            )
        )
    }

CrossFade

与AnimatedVisibility控制组件的消失与出现,而CrossFade可以用来控制俩个组件之间的切换,并且俩个组件之间的切换动画为fade动画

@Composable
fun CrossFadeLearn() {
    Column {
        //除了使用boolean外也可以使用interesting,然后使用when语句切换
        var expanded by remember { mutableStateOf(true) }
        Crossfade(expanded) {
            //必须要使用it 而不能使用expanded
            if (it) {
                TransitionSquare()
            } else {
                Box(Modifier.size(24.dp).background(Color.Red))
            }
        }
        Button(onClick = { expanded = !expanded }) {
            Text("start")
        }
    }
}

AnimatedContent

CrossFade控制俩个组件间的切换使用简单但是效果也很简单

AnimatedContent可以进行更加复杂的控制


标题:Jetpack Compose 动画
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2022/12/06/1669883580403.html

评论
发表评论
       
       
取消