Compose-动画实战篇

基础介绍

作为前端架构,动画在整个UI体系中扮演着重要的角色,不仅会增强页面观看效果,还会在用户体验及交互上呈现关键的作用,如果没动画,就像一个机器人,没有情感系统一样。

Android 系统中的动画经历了多个版本的演变和进化

  1. 补间动画(Tween Animation):
    • 早期版本(Android 1.0 - 2.x): 最早的 Android 版本主要使用补间动画,这种动画只能对 View 上的一些基本属性进行动画,例如平移、缩放、旋转和透明度。
    • XML 配置: 补间动画通常通过 XML 文件进行配置,放置在 res/anim 目录下。
  2. 属性动画(Property Animation):
    • 引入属性动画(Android 3.0+): 随着 Android 3.0 的引入,属性动画成为主流。属性动画不仅支持对 View 的基本属性进行动画,还可以对任何对象的任何属性进行动画处理,提供了更高度灵活的动画系统。
    • ValueAnimator 和 ObjectAnimator: Android 引入了 ValueAnimatorObjectAnimator 等类来支持属性动画的实现。
    • XML 和代码配置: 可以通过 XML 文件或者代码来配置属性动画。
    • AnimatorSet: 引入了 AnimatorSet 类,使得可以组合多个动画一起执行。
    • Interpolator: 提供了更多的插值器(Interpolator)选项,用于改变动画的变化速度。
    • 监听器: 添加了动画监听器,允许开发者监听动画的状态变化。
  3. Transition API:
    • Android 4.4+: 引入了 Transition API,用于处理场景之间的变化和过渡。这为实现更复杂的界面过渡和动画提供了更直观的方式。

Jetpack Compose 作为全新UI框架,也提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。
上面图片是官方文档中,从使用角度对动画API的一个分类。

动画分类

我们按照官方图片的分类开始,从底层API开始。

  • 基础动画 : 本身不带有任何状态,无法和缓存住当前值,配合compose 使用时,需要remeber{} 住状态。
  • 基于动画状态 : 提供一系状态的变化,无需手动维护状态,但需要将状态和布局绑定。
  • 基于内容 : 提供一系状态组件,无需手动维护状态,不需要手动控制状态和布局绑定。
(基础)Animatable 和 AnimationState

现有一个需求,需要控制背景高度,我们可以通过AnimationState(),并且可以animationSpec控制动画时间与动画规范

kotlin 复制代码
var checked by remember { mutableStateOf(true) }
val animationState = remember { AnimationState(20f) }
val animatableColor = remember {
    Animatable(Color.Red)
}
LaunchedEffect(checked) {
    launch {
        animationState.animateTo(
            100F, animationSpec = tween(durationMillis = 3000)
        )
    }
    launch {
        animatableColor.animateTo(
            Color.Green,
            animationSpec = tween(durationMillis = 3000)
        )
        animatableColor.animateTo(
            Color.Blue,
            animationSpec = tween(durationMillis = 3000)
        )
    }
}
Switch(checked = checked, onCheckedChange = {
    checked = it
})
Text(
    modifier = Modifier
        .height(animationState.value.dp)
        .background(Color.Gray),
    text = "Hello Compose",
    color = animatableColor.value
)

除了AnimationState ,还有Animatable,使用方式基本相同,但是后者默认值是Color 类型的值,前者是Float。

不论上面哪种,其实都可以通过实现TwoWayConverter 进行类型转换,这允许动画在任何类型的对象上运行,例如位置、矩形、颜色等。

官网介绍 他们俩的使用的区别为:

动画是唯一可信来源时,使用Animatable ,否则使用AnimationState

动画是唯一可信来源时 通俗理解就是 动画作为用户界面状态的可信数据源,保证动画的一致性和连续性。 但是在实际使用和测试中,我并没有发现他们俩的实际区别。

(基础)Animation

最底层的,许多动画都是基于 Animation 构建的,本身没有任何状态,也没有任何生命周期概念,。有两个子类,主要用于计算动画时间速率。TargetBasedAnimationDecayAnimation,具体未实战使用过,后续补充。

(基于状态)animate*AsState

上面的提供的几种方式,都不会维护动画的的状态,在Compose中如果不把维护好数据的状态,会导致值被重置等情况,所以系统直接提供API,维护这些类型的动画状态。

kotlin 复制代码
    val alpha: Float by animateFloatAsState(if (checked) 1f else 0.5f)
    val height: Dp by animateDpAsState(if (checked) 59.dp else 100.dp)
    Box(
        Modifier
            .fillMaxSize()
            .height(height)
            .graphicsLayer(alpha = alpha)
            .background(Color.Red)
    )
(基于状态)updateTransition

上面的状态用于维护单个状态,假设我们需要维护一个状态,用于展开/关闭,在展开的时候,修改控件Size,Color等多种状态,那我们需要使用到 updateTransition

kotlin 复制代码
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
val size by transition.animateSize(label = "") { state ->
    when (state) {
        BoxState.Collapsed -> Size(50f, 50f)
        BoxState.Expanded -> Size(100f, 100f)
    }
}
val borderWidth by transition.animateDp(label = "") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}
val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)
            else ->
                tween(durationMillis = 500)
        }
    }
) { state ->
    when (state) {
        BoxState.Collapsed -> colorPrimary()
        BoxState.Expanded -> colorSecondary()
    }
}
Canvas(modifier = Modifier
    .fillMaxSize()
    .border(borderWidth, Color.Black)
    .clickable {
        when (currentState) {
            BoxState.Collapsed -> {
                currentState = BoxState.Expanded
            }
            BoxState.Expanded -> {
                currentState = BoxState.Collapsed
            }
        }
    }, onDraw = {
    drawRect(color = color, size = size)
})
(基于状态)InfiniteTransition

可以像 Transition 一样保存一个或多个子动画,但是,这些动画一进入组合阶段就开始运行,除非被移除,否则不会停止。

kotlin 复制代码
//一个带状态的循环播放的背景
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(
    Modifier
        .fillMaxSize()
) {
    Text(
        text = "Hello Compose~",
        modifier = Modifier.background(color)
    )
}
(基于内容)Modifier.animateContentSize

上面讲到的,不论原始动画API,还是基于状态的API,在变化时,并不是基于内容,而是基于状态。现在有一个需求,有一个文案,当文字长度发生变化时,背景会发生变化(),这时我们就可以使用修饰符animateContentSize(),该方法接收两个参数,动画规范AnimationSpec和一个回调,后面会讲AnimationSpec具体的分类和细节。

koltin 复制代码
Box(
    modifier = Modifier
        .background(Color.Blue)
        .animateContentSize()
) {
    Text(text = "数字值翻倍 $count")
}
(基于内容)AnimatedContent

对于animateContentSize(),只是在内容上增加了动画的规范,效果过于单一,现在需求变更,有一个数字,当点击增加按钮的时候,数字向上移动消失,点击删除的时候,数字向下移动删除,这时候animateContentSize()明显不够用了,此时就需要组件AnimatedContent

kotlin 复制代码
AnimatedContent(
    targetState = count,
    transitionSpec = {
        if (targetState > initialState) {
            slideInVertically { height -> height } + fadeIn() with
                    slideOutVertically { height -> -height } + fadeOut()
        } else {
            slideInVertically { height -> -height } + fadeIn() with
                    slideOutVertically { height -> height } + fadeOut()
        }.using(
            SizeTransform(clip = false)
        )
    }
) { targetCount ->
    Text(text = "$targetCount")
}
(基于内容)Crossfade

对比AnimatedContent()Crossfade()更具体,在具有交叉淡入淡出动画的两种布局之间切换。两者本质都是通过updateTransition(),实现了 维护状态,从而控制内容。

kotlin 复制代码
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}
(基于内容)AnimatedVisibility

内容的出现和消失添加动画效果。

相比上面的的两种组件,AnimatedVisibility()会更加具体,主要负责子组件的 进入与退出,分为很多种效果(淡入淡出,垂直,水平,滑入,展开,缩放,)

ini 复制代码
AnimatedVisibility(
    visible = checked,
    enter = slideInHorizontally(),
    exit = slideOutHorizontally(),
) {
    CircleCat()
}

总结 : 基于内容的API,给我们带来的更多是便捷,而基于动画的API更多的是细节效果,具体使用,我们需要去根据场景进行选择。

可自定义项

AnimationSpec

在 Jetpack Compose 中,AnimationSpec 接口主要用于创建动画的规范(时间,速度)。

基本所有动画效果:淡入淡出,平移,,包括animate* AsState,都需要animationSpec作为参数。

系统提供了一系列的方法用来控制动画效果。

  1. tween: 创建一个线性插值(匀速)的动画效果,可以指定持续时间、延迟时间和缓动函数。

    css 复制代码
    kotlinCopy code
    val tweenSpec = tween<Float>(durationMillis = 1000, easing = LinearEasing)
  2. spring: 创建一个弹性动画效果,可以指定弹性的刚度(stiffness)和阻尼(damping)。

    css 复制代码
    kotlinCopy code
    val springSpec = spring<Float>(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium)
  3. keyframes: 创建一个关键帧动画效果,允许在不同时间点指定不同的值。

    ini 复制代码
    kotlinCopy code
    val keyframesSpec = keyframes {
        durationMillis = 1000
        0f at 0 with LinearEasing
        100f at 500 with FastOutSlowInEasing
        200f at 1000
    }
  4. repeatable: 创建一个可重复的动画规范,允许定义动画的重复次数。

    ini 复制代码
    kotlinCopy code
    val repeatableSpec = repeatable<Float>(
        iterations = Infinite,
        animation = tween(durationMillis = 1000)
    )

这些实现类分别用于创建不同类型的动画规范,以满足不同的动画需求。

Easing

对于tween 来说,如果想调整速度为加速或者减速,使用 Easing 来调整动画的小数值,小数是介于 0(起始值)和 1.0(结束值)之间的值,表示动画中的当前点。

ini 复制代码
val snapSpec = snap(durationMillis = 500)
val animatedValue by animateFloatAsState(
    targetValue = 100f,
    animationSpec = snapSpec
)
AnimationVector

上面我们了解过,Compose支持多种数据类型的状态,靠的就是TwoWayConverter和AnimationVector,TwoWayConverter会将数据类型(Int,Float) 和AnimationVector(1D)进行双向转换,在动画播放期间,任何动画值都表示为 AnimationVector,这样一来,核心动画系统就可以统一对其进行处理。例如,Int 表示为包含单个浮点值的 AnimationVector1D。用于 Int 的 TwoWayConverter 如下所示:

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

Color 在变化时,实际上是 red、green、blue 和 alpha 这 4 个值的集合,因此,Color 可转换为包含 4 个浮点值的 AnimationVector4D。通过这种方式,动画中使用的每种数据类型都可以根据其维度转换为 AnimationVector1D、AnimationVector2D、AnimationVector3D 或 AnimationVector4D。

自定义矩阵:

JAVA 复制代码
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)
            }
        )
    )
}

参考资料

相关推荐
WeiShuai12 分钟前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
ice___Cpu18 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill20 分钟前
nestjs使用ESM模块化
前端
加油吧x青年39 分钟前
Web端开启直播技术方案分享
前端·webrtc·直播
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白1 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧2 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog2 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川2 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶2 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron