前面讲到布局基础和图像绘制,本篇来讲下Jetpack Compose动画。
介绍动画主要从下图中几点进行讲解
一、内容动画
与布局内容变化相关的几种动画,官方称之为高级别动画API。
- AnimatedVisibility,实验性功能,可组合项可为内容的出现和消失添加动画效果;
- AnimatedContent,实验性功能,可组合项在内容根据目标状态发生变化时,添加内容的动画效果;
- AnimateContentSize,可组合项内容大小发生变化动画;
- Crossfade,可组合项的淡入淡出;
AnimatedVisibility
使用如下:
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
AnimatedContent
示例:
AnimatedContent(targetState = member,
transitionSpec ={
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically({ height -> height }) + fadeIn() with
slideOutVertically({ height -> -height }) + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically({ height -> -height })+ fadeIn() with
slideOutVertically({ height -> height }) + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) {targetCount->
Text(text = "$targetCount")
}
AnimatedContentSize
示例:
Text(text = if (!isExpanded) "点我展开" else "点我收起\n收起",modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.clickable { isExpanded = !isExpanded })
Crossfade
示例:
//UI切换 带淡入淡出动画
Crossfade(targetState = crossFadeState) {
Box(modifier = Modifier.background(color = if (it) Color.Green else Color.Gray)) {
if (it) Text(text = "Page A",)
else Text(text = "Page B")
}
}
上述示例展示动画:
二、值动画
通知单个或多个值发生变化来设置动画,分为:多值动画、单值动画、重复动画。
多值动画
定义:当状态发生改变时,多个值要一起发生改变。
修饰符:updateTransition
下面以颜色、大小、边框为例设置多值动画
@Composable
private fun MultiValueAnimation(){
val targetState = remember {
mutableStateOf(BoxState.Collapsed)
}
val transition = updateTransition(
targetState = targetState,
label = "hahah"
)
val rect by transition.animateRect { state ->
when (state.value) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { state ->
when (state.value) {
BoxState.Collapsed -> 1.dp
BoxState.Expanded -> 0.dp
}
}
val color by transition.animateColor {state ->
when(state.value){
BoxState.Expanded->Color.LightGray
BoxState.Collapsed->Color.Gray
}
}
Surface(shape = RectangleShape,
border = BorderStroke(width = borderWidth,color = Color.Blue),
modifier = Modifier
.size(rect.width.dp, rect.height.dp)
.clickable {
if (targetState.value == BoxState.Expanded) targetState.value =
BoxState.Collapsed else targetState.value = BoxState.Expanded
},
color = color
) {
Text(text = "多值动画")
}
}
运行效果:单值动画
定义:为单个值添加动画效果。Compose 为 Float、Color、Dp、Size、Offset、Rect、Int、IntOffset 和 IntSize 提供开箱即用的 animate*AsState 函数。通过为接受通用类型的 animateValueAsState 提供 TwoWayConverter,您可以轻松添加对其他数据类型的支持
修饰符:animateXXAsState
以animateFloatAsState
和animateOffsetAsState
为示例:
@Composable
private fun SingleValueAnimation(){
var enabled by remember {
mutableStateOf(false)
}
//使用animateFloatAsState变化透明度
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
val offset by animateOffsetAsState(targetValue = if (enabled) Offset.Zero else Offset(10f,10f))
Image(
painter = painterResource(id = R.mipmap.ic_girl),
contentDescription = "avatar",
modifier = Modifier
.offset(offset.x.dp, offset.y.dp)
.alpha(alpha)
.clickable { enabled = !enabled }
)
}
运行结果:针对单值动画,除了上述使用方式外,也可采用Animatable
来实现。
如下:
var enabled by remember {
mutableStateOf(false)
}
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(enabled) {
color.animateTo(if (enabled) Color.Green else Color.Red)
}
Box(
Modifier
.size(60.dp)
.background(color.value)
.clickable { enabled = !enabled }
)
运行效果:重复动画
定义:InfiniteTransition
可以像 Transition 一样保存一个或多个子动画,但是,这些动画一进入组合阶段就开始运行,除非被移除,否则不会停止。使用 rememberInfiniteTransition 创建 InfiniteTransition 实例,并使用 animateColor、animatedFloat 或 animatedValue 添加子动画。
@Composable
private fun InfiniteTransitionAnimation(){
val infiniteTransition = rememberInfiniteTransition()
val state by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Cyan,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2000,easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
))
Box(
Modifier
.size(60.dp)
.background(state)
)
}
运行结果:三、自定义动画
有时候系统提供的默认动画无法满足我们的需求,这样我们就需要进行自定义了。那么怎么自定义呢,先看下代码:
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
看到有个参数为:animationSpec
,其类型为AnimationSpec
,该参数为可选有默认值,是定义动画的类型。因此想自定义动画,必须给该参数传值了。
先看下有哪些类型的动画:
spring(弹性动画)
定义:可在起始值和结束值之间创建基于物理特性的动画。它接受 2 个参数:dampingRatio 和 stiffness
-
dampingRatio
,定义弹簧的弹性。默认值为 Spring.DampingRatioNoBouncy。 -
stiffness
,定义弹簧应向结束值移动的速度。默认值为 Spring.StiffnessMedium。
示例:
/**
* 弹性动画
*/
@Composable
private fun springAnimation(){
var enable by remember{ mutableStateOf(true)}
val value: Int by animateIntAsState(
targetValue = if (enable) 200 else 50,
// Configure the animation duration and easing.
animationSpec = spring(
//定义弹簧的弹性
dampingRatio = Spring.DampingRatioHighBouncy,
//定义弹簧应向结束值移动的速度
stiffness = Spring.StiffnessHigh
)
)
Box(
Modifier
.offset(50.dp)
.width(value.dp)
.height(50.dp)
.background(Color.Blue)
.clickable { enable = !enable }
){
Text(text = "spring")
}
}
运行结果tween
在指定的 durationMillis
内使用缓和曲线在起始值和结束值之间添加动画效果。还可以指定 delayMillis
来推迟动画播放的开始时间。
示例:
@Composable
private fun tweenAnimation(){
var enable by remember{ mutableStateOf(false)}
val value by animateIntAsState(
targetValue = if (enable) 150 else 50,
animationSpec = tween(
durationMillis = 1000,
delayMillis = 500,
easing = LinearOutSlowInEasing
)
)
Box(
Modifier.width(value.dp)
.height(50.dp)
.background(Color.Gray)
.clickable { enable = !enable }
) {
Text(text = "tween")
}
}
运行结果:
keyframes
定义:会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,您都可以指定 Easing 来确定插值曲线。
需要设置的配置为:
- durationMillis 动画执行时长,单位毫秒;
- delayMillis 延迟时间,单位毫秒;
- keyframes 关键帧,internal类型不能直接配置,需通过
KeyframesSpecConfig
的扩展函数at
和with
结合来设置关键帧信息
示例如下:
@Composable
private fun keyframesAnimation(){
var enable by remember{ mutableStateOf(false)}
val value by animateIntAsState(
targetValue = if (enable) 200 else 50,
animationSpec = keyframes {
durationMillis = 2000 //动画执行时长
delayMillis = 500 //动画延迟多久后执行
50 at 0 with LinearOutSlowInEasing //0 - 200ms执行的帧
100 at 200 with FastOutLinearInEasing // 200 - 1200ms执行的帧
150 at 1200 with LinearEasing
}
)
Box(
Modifier.height(50.dp)
.width(value.dp)
.background(Color.Yellow)
.clickable { enable = !enable }
) {
Text(text = "keyframes")
}
}
运行结果:repeatable(按次数重复动画)
定义:反复运行基于时长的动画,直至达到指定的迭代计数。可以传递 repeatMode
参数来指定动画是从头开始 (RepeatMode.Restart
) 还是从结尾开始 (RepeatMode.Reverse
) 重复播放。
可设置参数为:
- iterations 重复次数
- animation 有时长的动画
- repeatMode 重复模式,有:
RepeatMode.Restart
和RepeatMode.Reverse
示例如下:
/**
* 重复动画
*/
@Composable
private fun repeatableAnimation(){
var enable by remember{ mutableStateOf(false)}
val value by animateIntAsState(
targetValue = if (enable) 200 else 50,
animationSpec = repeatable(
iterations = 2, //重复执行次数
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Reverse //重复执行模式,从最后开始
)
)
Box(
Modifier.height(50.dp)
.width(value.dp)
.background(Color.Red)
.clickable { enable = !enable }
) {
Text(text = "repeatable")
}
}
运行效果:
infiniteRepeatable(无限重复动画)
该动画类似
repeatable
,都是重复的迭代。但infiniteRepeatable
为无限次的重复执行。示例如下:
/**
* 无限次重复动画
*/
@Composable
private fun InfiniteRepeatableAnimation(){
var enable by remember{ mutableStateOf(false)}
val value by animateIntAsState(
targetValue = if (enable) 200 else 50,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Reverse //重复执行模式,从最后开始
)
)
Box(
Modifier.height(50.dp)
.width(value.dp)
.background(Color.Green)
.clickable { enable = !enable }
) {
Text(text = "infiniteRepeatable")
}
}
运行结果就不展示了。
snap
定义:是一种特殊的 AnimationSpec
类型,它会立即将值切换到结束值
。您可以指定 delayMillis
来延迟动画播放的开始时间。
以延迟1000ms为例,如下:
/**
* 无限次重复动画
*/
@Composable
private fun SnapAnimation(){
var enable by remember{ mutableStateOf(false)}
val value by animateIntAsState(
targetValue = if (enable) 200 else 50,
animationSpec = snap(
delayMillis = 1000 //延迟1000ms执行
)
)
Box(
Modifier
.height(50.dp)
.width(value.dp)
.background(Color.Green)
.clickable { enable = !enable }
) {
Text(text = "snap")
}
}
运行结果:四、手势动画
我们使用 Animatable 表示图片组件的偏移位置为例,触摸以修饰符pointerInput
。当检测到新的点按事件时,我们将调用 animateTo 以将偏移值通过动画过渡到点按位置。
/**
* 通过手势点击,设置偏移量动画
*/
@Composable
private fun Gesture(){
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Image(
painter = painterResource(id = R.mipmap.ic_girl),
contentDescription = "avatar",
modifier = Modifier.offset { offset.value.toIntOffset() }
)
}
}
运行结果:总结
欢迎留言,一起学习,共同进步!