基础介绍
作为前端架构,动画在整个UI体系中扮演着重要的角色,不仅会增强页面观看效果,还会在用户体验及交互上呈现关键的作用,如果没动画,就像一个机器人,没有情感系统一样。
Android 系统中的动画经历了多个版本的演变和进化
- 补间动画(Tween Animation):
- 早期版本(Android 1.0 - 2.x): 最早的 Android 版本主要使用补间动画,这种动画只能对 View 上的一些基本属性进行动画,例如平移、缩放、旋转和透明度。
- XML 配置: 补间动画通常通过 XML 文件进行配置,放置在
res/anim
目录下。
- 属性动画(Property Animation):
- 引入属性动画(Android 3.0+): 随着 Android 3.0 的引入,属性动画成为主流。属性动画不仅支持对 View 的基本属性进行动画,还可以对任何对象的任何属性进行动画处理,提供了更高度灵活的动画系统。
- ValueAnimator 和 ObjectAnimator: Android 引入了
ValueAnimator
和ObjectAnimator
等类来支持属性动画的实现。 - XML 和代码配置: 可以通过 XML 文件或者代码来配置属性动画。
- AnimatorSet: 引入了
AnimatorSet
类,使得可以组合多个动画一起执行。 - Interpolator: 提供了更多的插值器(Interpolator)选项,用于改变动画的变化速度。
- 监听器: 添加了动画监听器,允许开发者监听动画的状态变化。
- 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 构建的,本身没有任何状态,也没有任何生命周期概念,。有两个子类,主要用于计算动画时间速率。TargetBasedAnimation
和 DecayAnimation
,具体未实战使用过,后续补充。
(基于状态)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
作为参数。
系统提供了一系列的方法用来控制动画效果。
-
tween: 创建一个线性插值(匀速)的动画效果,可以指定持续时间、延迟时间和缓动函数。
csskotlinCopy code val tweenSpec = tween<Float>(durationMillis = 1000, easing = LinearEasing)
-
spring: 创建一个弹性动画效果,可以指定弹性的刚度(stiffness)和阻尼(damping)。
csskotlinCopy code val springSpec = spring<Float>(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium)
-
keyframes: 创建一个关键帧动画效果,允许在不同时间点指定不同的值。
inikotlinCopy code val keyframesSpec = keyframes { durationMillis = 1000 0f at 0 with LinearEasing 100f at 500 with FastOutSlowInEasing 200f at 1000 }
-
repeatable: 创建一个可重复的动画规范,允许定义动画的重复次数。
inikotlinCopy 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)
}
)
)
}