本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/17617292.html
前言
此篇博客讲解Jetpack Compose的动画实现。Compose的动画分两种:
- 一种是可以简单快捷使用的Animatable、AnimatedVisibility 、AnimatedContent动画,他们已经将使用进行的简单的封装。这其中AnimatedVisibility(动画控制显示与隐藏) 和 AnimatedContent(动画控制内容切换)已经封装成了容器组件
- 另一种是稍微复杂的animateFloatAsState系列的属性动画,这个更底层可以完成更复杂组合的动画。
此外除了上面的动画函数,你还需要了解插值器函数,他们会控制动画的执行中的效果或者持续时间,他们一共有以下几种:
- tween:补间插值器,这个插值器可以让你设置动画的持续时间,这是最常用的插值器
- spring: 弹跳插值器,spring可以实现类似弹簧或者弹跳的动画效果,它不可以设置动画时间,但是可以设置阻尼值以调整回弹效果
- keyframes:关键帧插值器,关键帧动画可以逐帧设置当前动画的轨迹
- snap:提前动画,直接不执行动画过程,直接到达动画结果
官网文档:https://developer.android.google.cn/jetpack/compose/animation?hl=zh-cn
Animatable与tween插值器
下面通过Animatable与tween插值器,实现了一个颜色渐变的动画效果。
效果图
代码,代码中使用了tween插值器,此外easing动画数值曲线还有以下选择:
- FastOutSlowInEasing 快出慢进
- FastOutLinearInEasing 先快出,后匀速线性进
- LinearOutSlowInEasing 先匀速线性出,后快进
- LinearEasing 匀速线性
@Composable
fun ColorAnimation() {
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(true) {
color.animateTo(Color.Green,
//delayMillis = 动画开始延迟时间 , durationMillis = 动画持续时间 , easing = 动画数值曲线
animationSpec = tween(durationMillis = 3000, delayMillis = 1000, easing = LinearEasing)
)
}
Box(modifier = Modifier.fillMaxSize()) {
Surface(
color = color.value,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
){}
}
}
spring弹跳插值器
下面通过Animatable与spring插值器,实现了一个掉落动画。
请注意!下面的代码中,设置动画数值的函数是graphicsLayer。 这里不建议直接使用offset或者scale等等这些函数来说实现动画(可以在graphicsLayer里调用offset),因为直接offset会引起父类容器的重组,导致整个页面都在重组刷新,有些时候还会引起移动时抖动或者拖影的现象。 这里建议使用graphicsLayer会将重组范围限制到当前组件里,避免父类与其他组件都连带重组。
效果图
代码
@Composable
fun FallOffAnimation() {
val yAnimation1 = remember { Animatable(0f) }
val yAnimation2 = remember { Animatable(0f) }
val yAnimation3 = remember { Animatable(0f) }
val yAnimation4 = remember { Animatable(0f) }
LaunchedEffect(true) {
delay(2000)
yAnimation1.animateTo(
200f,
//dampingRatio = 阻尼比 , stiffness = 刚度
//DampingRatioHighBouncy = 阻尼比高弹性 , StiffnessHigh = 刚度高
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh)
)
yAnimation2.animateTo(
200f,
//DampingRatioHighBouncy = 阻尼比高弹性 , StiffnessMediumLow = 刚度中低
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessMediumLow)
)
yAnimation3.animateTo(
200f,
//DampingRatioMediumBouncy = 阻尼比中弹性 , StiffnessMedium = 刚度中
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow)
)
//DampingRatioNoBouncy = 阻尼比无弹性 , StiffnessVeryLow = 刚度非常低
yAnimation4.animateTo(
200f,
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessVeryLow)
)
}
Row(modifier = Modifier.fillMaxSize().padding(100.dp)) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation1.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation2.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation3.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation4.value
}
) {}
}
}
keyframes插值器
下面通过Animatable与keyframes插值器,通过自定义设置动画帧的速度,实现了一个先快中间慢最后快的掉落动画。
效果图
代码
@Composable
fun FallOffAnimation() {
val yAnimation1 = remember { Animatable(0f) }
LaunchedEffect(true) {
delay(2000)
yAnimation1.animateTo(
300f,
animationSpec = keyframes {
//动画持续时间
durationMillis = 3000
/*
简单说明下面代码意思:
下面代码实现了先以100毫秒从0f移动到100f的位置,然后以2800毫秒从100f移动到200f的位置,再接着以100毫秒从200f移动到最后的300f位置
*/
0f at 0 with FastOutLinearInEasing //从0f位置0毫秒开始以FastOutLinearInEasing数值曲线运行到下一个阶段
100f at 100 with LinearEasing //经过了100毫秒运动到100f此位置,现在从100f位置以LinearEasing数值曲线运行到下一个阶段
200f at 2900 with FastOutLinearInEasing //经过了2800毫秒运动到200f此位置,现在从200f位置以LinearEasing数值曲线运行到300f结束
}
)
}
Row(modifier = Modifier
.fillMaxSize()
.padding(100.dp)) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation1.value
}
) {}
}
}
动画的重复
repeatable:可重复
infiniteRepeatable:无限重复
效果图
代码
@Composable
fun RotationAnimation() {
val zRotationAnimation1 = remember { Animatable(0f) }
val zRotationAnimation2 = remember { Animatable(0f) }
//repeatable 设置重复次数
LaunchedEffect(true) {
zRotationAnimation1.animateTo(
360f,
//iterations 重复次数
animationSpec = repeatable(iterations = 2, animation = tween(durationMillis = 1000))
)
}
//infiniteRepeatable 无限重复
LaunchedEffect(true) {
zRotationAnimation2.animateTo(
360f,
animationSpec = infiniteRepeatable(animation = tween(durationMillis = 1000))
)
}
Row(
modifier = Modifier
.fillMaxSize()
.padding(100.dp)
) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
rotationZ = zRotationAnimation1.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
rotationZ = zRotationAnimation2.value
}
) {}
}
}
AnimatedVisibility 隐藏显示动画
默认效果
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true){
for (i in 0..10){
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = imageVisible.value, modifier = Modifier.align(Alignment.Center)) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_apple),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
淡入淡出效果 fadeIn与fadeOut
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>淡入淡出效果 fadeIn与fadeOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = fadeIn(
initialAlpha = 0f
),
exit = fadeOut(targetAlpha = 0f)
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
滑入滑出 slideIn与slideOut
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>滑入滑出 slideIn与slideOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideIn { IntOffset(-100, -100) },
exit = slideOut { IntOffset(100, 100) }
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
水平滑入与水平滑出 slideInHorizontally与slideOutHorizontally
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>水平滑入与水平滑出 slideInHorizontally与slideOutHorizontally
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideInHorizontally(initialOffsetX = {fullWidth->
return@slideInHorizontally -(fullWidth/2)
}),
exit = slideOutHorizontally(targetOffsetX = {fullWidth->
return@slideOutHorizontally fullWidth/2
})
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
垂直滑入与垂直滑出 slideInVertically与slideOutVertically
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>垂直滑入与垂直滑出 slideInVertically与slideOutVertically
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideInVertically(initialOffsetY = {fullHeight->
return@slideInVertically -(fullHeight/2)
}),
exit = slideOutVertically(targetOffsetY = {fullHeight->
return@slideOutVertically fullHeight/2
})
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
缩放进入与缩放退出 scaleIn与scaleOut
效果图
代码
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>缩放进入与缩放退出 scaleIn与scaleOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = scaleIn(animationSpec = spring(),initialScale = 0.1f, transformOrigin = TransformOrigin.Center),
exit = scaleOut(animationSpec = spring(),targetScale = 0.1f, transformOrigin = TransformOrigin.Center)
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
扩展与收缩 expandIn与shrinkOut
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>扩展与收缩 expandIn与shrinkOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandIn(),
exit = shrinkOut()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
水平扩展与水平收缩 expandHorizontally与shrinkHorizontally
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>水平扩展与水平收缩 expandHorizontally与shrinkHorizontally
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandHorizontally(),
exit = shrinkHorizontally()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
垂直扩展与垂直收缩 expandVertically与shrinkVertically
效果图
代码
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//这边用一个协程,以1500毫秒反复显示与隐藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>垂直扩展与垂直收缩 expandVertically与shrinkVertically
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandVertically(),
exit = shrinkVertically()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
AnimatedContent 内容切换动画
请注意,AnimatedContent还是实验性API,此外AnimatedContent内容切换动画 与 上面的AnimatedVisibility 隐藏显示动画在使用上非常相似,这里稍微举例几个切换效果,其他效果的实现可以参考上面的AnimatedVisibility例子
垂直滑动切换动画
效果图
代码
代码中的with连接方法非常重要,它连接了动画的进入与退出效果
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun VerticallyAnimatedContent() {
val data = remember { mutableStateOf(0) }
Box(
modifier = Modifier
.fillMaxSize()
) {
AnimatedContent(targetState = data.value, transitionSpec = {
slideInVertically { fullHeight -> return@slideInVertically -fullHeight } with
slideOutVertically { fullHeight -> return@slideOutVertically fullHeight }
}, modifier = Modifier.align(Alignment.Center)) {
Text(text = "${data.value}", fontSize = 100.sp, modifier = Modifier.clickable {
data.value++
})
}
}
}
横向滑动+淡入淡出的组合切换动画
效果图
代码
请注意下面的写法 slideInHorizontally + fadeIn with slideOutHorizontally + fadeOut
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun HorizontallyAnimatedContent() {
val data = remember { mutableStateOf(0) }
Box(
modifier = Modifier
.fillMaxSize()
) {
AnimatedContent(targetState = data.value, transitionSpec = {
slideInHorizontally(
initialOffsetX = { fullWidth ->
return@slideInHorizontally -fullWidth
}) + fadeIn(initialAlpha = 0f, animationSpec = tween(1000)) with
slideOutHorizontally(targetOffsetX = { fullWidth -> return@slideOutHorizontally fullWidth }) + fadeOut(
targetAlpha = 0f,
animationSpec = tween(1000)
)
}, modifier = Modifier.align(Alignment.Center)) {
Text(text = "${data.value}", fontSize = 100.sp, modifier = Modifier.clickable {
data.value++
})
}
}
}
属性动画
一共有如下类型值的属性动画函数,他们的使用都是类似的。此外这些属性动画函数还需要配合插值器使用。
- animateValueAsState : 其他的animateXxxAsState内部都是调用的这个
- animateRectAsState : 参数是传的一个Rect对象,Rect(left,top,right,bottom)
- animateIntAsState : 参数传的是Int
- animateDpAsState : 参数传的是Dp
- animateFloatAsState : 参数传的是Float
- animateColorAsState : 参数传的是Color
- animateOffsetAsState : 参数传的是Offset,Offset(x,y),x和y是Float类型
- animateIntOffsetAsState : 参数传的是IntOffset,IntOffset(x,y),x和y是Int类型
- animateSizeAsState : 参数传的是Size,Size(width,height),width和height是Float类型
- animateIntSizeAsState : 参数传的是IntSize,IntSize(width,height),width和height是Int类型
animateFloatAsState与spring插值器
上面的属性动画函数使用都是类似的,这里以animateFloatAsState与spring插值器来举例
y轴移动动画
效果图
代码
@Composable
fun APage() {
val startAnim = remember {
mutableStateOf(false)
}
//创建y轴的动画数值
val yAnimValue = animateDpAsState(
//targetValue为动画的目标数值
targetValue = if (startAnim.value) 200.dp else 0.dp,
//animationSpec动画的可选插值器,目前的结尾弹跳效果就是spring这个插值器实现的
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
Box(
modifier = Modifier.fillMaxSize()
) {
Surface(color = Color.Gray, modifier = Modifier
.graphicsLayer {
translationY = yAnimValue.value.toPx()
}
.size(50.dp)
.align(Alignment.Center)
.clickable {
//点击启动动画
startAnim.value = !startAnim.value
}) {
}
}
}
tween插值器
tween插值器可以设置动画时间
效果图
代码
@Composable
fun Animation() {
val position = remember {
mutableStateOf(Offset(0f, 0f))
}
val duration = remember {
mutableStateOf(3000)
}
//动画数值
val animValue = animateOffsetAsState(
targetValue = position.value,
animationSpec = tween(durationMillis = duration.value, easing = LinearEasing)
)
//移动位置集合
val starList = listOf(
//第一个参数是坐标,第二个参数延迟时间
Offset(-100f, -100f) to 1000,//左上
Offset(-100f, 100f) to 1000,//左下
Offset(100f, 100f) to 1000,//右下
Offset(100f, -100f) to 1000,//右上
)
LaunchedEffect(true) {
delay(2000)
for (item in starList) {
position.value = item.first
duration.value = item.second
delay(duration.value.toLong())
}
}
Box(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.mipmap.ic_red_dot),
contentDescription = null,
modifier = Modifier
.graphicsLayer {
translationX = animValue.value.x
translationY = animValue.value.y
}
.align(Alignment.Center)
)
}
}
在Canvas里的使用例子
效果图
代码
@Composable
fun AnimationBg() {
//-400.dp 到 -1500.dp 动画范围值
val yPosition = remember {
mutableStateOf(-400.dp)
}
//y轴动画数值
val yAnimValue = animateDpAsState(
targetValue = yPosition.value,
animationSpec = tween(durationMillis = 15_000, easing = LinearEasing)
)
val rotate = remember {
mutableStateOf(0f)
}
//旋转动画,请注意这里使用的是animateFloatAsState
val rotateAnimValue = animateFloatAsState(
targetValue = rotate.value,
animationSpec = tween(durationMillis = 5000, easing = LinearEasing)
)
LaunchedEffect(true) {
var isFront = true
val angleList = listOf<Float>(0f, 45f, 90f, 135f)
var count = 0
while (isActive) {
isFront = !isFront
if (isFront) {
yPosition.value = -400.dp
} else {
yPosition.value = -1500.dp
}
delay(15_000)
count++
if (count > 2){
count = 0
rotate.value = angleList.random()
}
}
}
Canvas(
modifier = Modifier
.fillMaxSize()
.rotate(rotateAnimValue.value)
) {
val itemHeight = size.height / 30
for (index in 0..200) {
if (index % 2 == 0) {
//黑色
drawLine(
color = Color.Black,
start = Offset(-500f, itemHeight * index + yAnimValue.value.toPx()),
end = Offset(size.width + 500f, itemHeight * index + yAnimValue.value.toPx()),
strokeWidth = itemHeight
)
} else {
//白色
drawLine(
color = Color.White,
start = Offset(-500f, itemHeight * index + yAnimValue.value.toPx()),
end = Offset(size.width + 500f, itemHeight * index + yAnimValue.value.toPx()),
strokeWidth = itemHeight
)
}
}
}
}
end
本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/17617292.html