compose
中本身封装了很多动画,我们可以拿来直接使用,动画也可以从官网进行学习:Compose动画
一、AnimationSpec
compose中的动画效果都是由AnimationSpec
定义的,它包含了动画执行时长,估值器,插值器的功能,我们也可以通过AnimationSpec
自定义动画效果,所以在真正使用compose
动画之前,先对AnimationSpec
来做学习
1.spring
spring
就是一个弹弹乐效果的插值器,stiffness
定义弹簧应向结束值移动的速度,dampingRatio
定义弹簧的弹性,官方给出的效果图示如下:
例子:
@Preview
@Composable
fun MySpring() {
var targetValue by remember { mutableStateOf(0.dp) }
val paddingTop by animateDpAsState(
targetValue = targetValue,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
Box(
modifier = Modifier
.padding(top = paddingTop)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
.clickable {
targetValue = 100.dp
}
)
}
效果:
2.tween
tween
用于指定动画执行时间 durationMillis
、延迟执行时间 delayMillis
、运动速率变化 easing
2.1 基本使用
@Preview
@Composable
fun MyTween() {
var targetValue by remember { mutableStateOf(0.dp) }
val paddingTop by animateDpAsState(
targetValue = targetValue,
animationSpec = tween(2000)
)
Box(
modifier = Modifier
.padding(top = paddingTop)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
.clickable {
targetValue = 100.dp
}
)
}
效果:
2.2 easing
运动速率变化默认提供了以下几种,不指定时使用FastOutSlowInEasing
:
/**
* 快速加速,逐渐减速
*/
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 }
Easing | |
---|---|
3.keyframes
keyframes
可以选择在不同的时段,手动控制值的变化,并可以使用with
指定Easing
:
@Preview
@Composable
fun MyKeyframes() {
var targetValue by remember { mutableStateOf(0.dp) }
val paddingTop by animateDpAsState(
targetValue = targetValue,
animationSpec = keyframes {
// 执行时长为1s
durationMillis = 1000
// 在200ms内,以LinearEasing达到目标值的1/4
targetValue / 4 at 200 with LinearEasing
// 在200ms - 500ms,以LinearOutSlowInEasing达到目标值的1 / 2
targetValue / 2 at 500 with LinearOutSlowInEasing
// 在500ms - 800ms,达到目标值的3 / 4
targetValue * 3 / 4 at 800
// 在900ms后到执行结束,达到目标值
targetValue at 900
}
)
Box(
modifier = Modifier
.padding(top = paddingTop)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
.clickable {
targetValue = 300.dp
}
)
}
效果:
4.repeatable
repeatable
可以为基于时长的动画(如tween
、keyframes
)加上可以重复执行的效果,repeatMode
用来指定重复的模式:从头开始 (RepeatMode.Restart
) , 从结尾开始 (RepeatMode.Reverse
)
@Preview
@Composable
fun MyRepeatable() {
var targetValue by remember { mutableStateOf(0.dp) }
val paddingTop by animateDpAsState(
targetValue = targetValue,
animationSpec = repeatable(
iterations = 3,
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier
.padding(top = paddingTop)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
.clickable {
targetValue = 100.dp
}
)
}
效果:
5.infiniteRepeatable
infiniteRepeatable
为无限循环执行的动画
6.snap
snap
会立即将值切换到结束值,您可以指定 delayMillis
来延迟动画播放的开始时间
@Preview
@Composable
fun MySnap() {
var targetValue by remember { mutableStateOf(0.dp) }
val paddingTop by animateDpAsState(
targetValue = targetValue,
animationSpec = snap(
delayMillis = 50
)
)
Box(
modifier = Modifier
.padding(top = paddingTop)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
.clickable {
targetValue = 100.dp
}
)
}
效果:
二、高级动画
高级动画就是compose
专门迎合MD
风格封装的动画,也足够我们在日常开发中使用了
1.AnimatedVisibility
前面我们已经使用过该组件了,AnimatedVisibility
可为内容的出现和消失添加动画效果,默认为所有内容组件添加以淡入和扩大的方式出现,以淡出和缩小的方式消失
1.1 基本使用
直接上代码:
@Preview
@Composable
fun MyAnimeVisible() {
var visible by remember { mutableStateOf(false) }
Row {
AnimatedVisibility(visible = visible) {
Icon(Icons.Rounded.Build, contentDescription = null)
}
Button(onClick = { visible = !visible }) {
Text("click")
}
}
}
效果:
1.2 EnterTransition&ExitTransition
也可以通过给AnimatedVisibility
指定 EnterTransition
和 ExitTransition
来自定义这种过渡效果, EnterTransition
和 ExitTransition
都支持了运算符重载,可以方便的组合各个过渡效果:
@Preview
@Composable
fun MyAnimeVisible2() {
var visible by remember { mutableStateOf(false) }
val density = LocalDensity.current
Row {
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Icon(Icons.Rounded.Build, contentDescription = null)
}
Button(onClick = { visible = !visible }) {
Text("click")
}
}
}
效果:
官网给出的各个效果图示如下:
EnterTransition | ExitTransition |
---|---|
fadeIn |
fadeOut |
slideIn |
slideOut |
slideInHorizontally |
slideOutHorizontally |
slideInVertically |
slideOutVertically |
scaleIn |
scaleOut |
expandIn |
shrinkOut |
expandHorizontally |
shrinkHorizontally |
expandVertically |
shrinkVertically |
我们还可以通过它们的animationSpec
属性,改变动画的执行过程,如执行时间、运动轨迹等
1.3 animateEnterExit修饰
此外,除了指定全体内容组件外,还记得在Modifier
中可以使用animateEnterExit
修饰来指定特定的内容组件的出现和消失动画吗?这种方式会和AnimatedVisibility
中的动画进行组合,如果你不想要AnimatedVisibility
中的默认动画效果,可以指定为 EnterTransition.None
和 ExitTransition.None
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MyAnimeVisible3() {
var visible by remember { mutableStateOf(false) }
Row {
AnimatedVisibility(
visible = visible,
enter = EnterTransition.None,//去除默认动画效果
exit = ExitTransition.None
) {
Column {
Icon(Icons.Rounded.Build, contentDescription = null)
Icon(
Icons.Rounded.Favorite, contentDescription = null,
// 单独使用特定的动画
modifier = Modifier.animateEnterExit(
enter = scaleIn(),
exit = scaleOut()
)
)
}
}
Button(onClick = { visible = !visible }) {
Text("click")
}
}
}
效果:
1.4 transition
在AnimatedVisibilityScope
中可以通过transition
创建自定义的动画效果:
例子,给Box
设置背景颜色变化的动画:
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MyAnimeVisible5() {
var visible by remember { mutableStateOf(false) }
Row {
AnimatedVisibility(
visible = visible,
enter = EnterTransition.None,//去除默认动画效果
exit = ExitTransition.None
) {
val bg by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(
modifier = Modifier
.size(50.dp)
.background(bg)
)
}
Button(onClick = { visible = !visible }) {
Text("click")
}
}
}
效果:
2.animate*AsState
通过animate*AsState
,可以创建简单的动画:
例子:
@Preview
@Composable
fun MyAnimateState() {
var enabled by remember { mutableStateOf(true) }
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Row {
Box(
Modifier
.size(50.dp)
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
Button(onClick = { enabled = !enabled }) {
Text("click")
}
}
}
效果:
3.AnimatedContent
AnimatedContent
需要绑定State
状态,当状态发生改变,导致重组时,会为内容添加动画
3.1 基本使用
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MyAnimatedContentPreview() {
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
}
效果:
3.2 transitionSpec
transitionSpec
,可以指定内容显示和消失的动画,使用with
将显示和消失动画进行结合
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MyAnimatedContentPreview2() {
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > 5) {//如果大于5
// 从上到下滑入滑出,淡入淡出
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 = "Count: $targetCount")
}
}
}
效果:
3.3 SizeTransform
SizeTransform
可以更自由的在AnimatedContent
执行时穿插动画效果:
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MySizeTransformPreview() {
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {// 如果展开
keyframes {
// 在150ms内,先将宽度渐渐变为targetSize.width
IntSize(targetSize.width, initialSize.height) at 150
// 执行总时长1s
durationMillis = 1000
}
} else {
keyframes {
// 在150ms内,先将高度渐渐变为targetSize.width
IntSize(initialSize.width, targetSize.height) at 150
// 执行总时长1s
durationMillis = 1000
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
}
@Composable
fun Expanded() {
Box(contentAlignment = Alignment.Center) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.matchParentSize(),
contentScale = ContentScale.FillBounds
)
Text(
"hi,This creates a SizeTransform with the provided clip \n" +
"and sizeAnimationSpec. By default, clip will be true. \n" +
"This means during the size animation, the content will be clipped to the animated size.\n" +
" sizeAnimationSpec defaults to return a spring animation."
)
}
}
@Composable
fun ContentIcon() {
Box(contentAlignment = Alignment.Center) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.matchParentSize(),
contentScale = ContentScale.FillBounds
)
Icon(Icons.Rounded.Call, contentDescription = null)
}
}
效果:
4.animateContentSize修饰
animateContentSize
修饰会在内容大小发生变化时,有一个动画效果,直接变化会导致显得突兀:
@Preview
@Composable
fun MyAnimateContentSizePreview() {
val textArr = arrayOf("hello", "hi", "hello world")
var textPosition by remember { mutableStateOf(0) }
Column {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.animateContentSize()
.padding(10.dp)
) {
Text(textArr[textPosition], color = MaterialTheme.colorScheme.onPrimary)
}
Button(onClick = { textPosition = Random.nextInt(textArr.size) }) {
Text(text = "点我", color = MaterialTheme.colorScheme.onPrimary)
}
}
}
效果:
5.Crossfade
Crossfade
会在内容组件重组时,有一个淡入淡出的动画效果:
@Preview
@Composable
fun MyCrossfade() {
var currentPage by remember { mutableStateOf("A") }
Column() {
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text(
text = "Page A",
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(10.dp),
color = MaterialTheme.colorScheme.onSurface
)
"B" -> Text(
text = "Page B",
modifier = Modifier
.background(MaterialTheme.colorScheme.error)
.padding(10.dp),
color = MaterialTheme.colorScheme.onError
)
}
}
Button(onClick = {
if (currentPage == "A") {
currentPage = "B"
} else {
currentPage = "A"
}
}) {
Text("click")
}
}
}
效果:
6.Transition
AnimatedVisibilityScope
中可以直接获取到transition
进而自定义一些动画,该对象为Transition
,可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画,通过Transition
也可以直接使用AnimatedVisibility
和 AnimatedContent
6.1 基本使用updateTransition
通过updateTransition
获取一个Transition
对象,结合状态定义动画效果:
@Preview
@Composable
fun MyTransition() {
var currentState by remember { mutableStateOf(MyState.Normal) }
// 定义Transition对象
val transition = updateTransition(currentState)
// 根据状态改变边框线宽度
val borderWidth by transition.animateDp { state ->
when (state) {
MyState.Normal -> Dp.Hairline
MyState.Expand -> 1.dp
}
}
Column(
Modifier
.background(MaterialTheme.colorScheme.surface)
.border(borderWidth, MaterialTheme.colorScheme.outline)
.padding(10.dp)
) {
Button(onClick = {
currentState = if (currentState == MyState.Normal) {
MyState.Expand
} else {
MyState.Normal
}
}) {
Text("click")
}
}
}
效果:
6.2 transitionSpec
和AnimatedContent
的transitionSpec
类似,Transition
的transitionSpec
可以为过渡状态变化的指定不同的AnimationSpec
,AnimationSpec
可以用于改变动画的执行过程,在传统安卓开发中,我们称之为插值器:
@Preview
@Composable
fun MyTransition2() {
var currentState by remember { mutableStateOf(MyState.Normal) }
// 定义Transition对象
val transition = updateTransition(currentState)
val contentWidth = 100.dp
val contentHeight = 50.dp
// 根据状态改变边框线左边坐标
val offsetLeft by transition.animateDp(transitionSpec = {
when {
MyState.Normal isTransitioningTo MyState.Expand ->// 当从MyState.Normal到MyState.Expand的过程
// 弹性动画,移动的较慢
spring(stiffness = Spring.StiffnessVeryLow)
else ->
// 指定动画的执行事件,移动的较快
spring(stiffness = Spring.StiffnessMedium)
}
}) { state ->
// 从 0 到 contentWidth
when (state) {
MyState.Normal -> 0.dp
MyState.Expand -> contentWidth
}
}
// 根据状态改变边框线右边边坐标
// 右边的transitionSpec和左边相反
val offsetRight by transition.animateDp(transitionSpec = {
when {
MyState.Normal isTransitioningTo MyState.Expand ->// 当从MyState.Normal到MyState.Expand的过程
// 弹性动画,移动的较快
spring(stiffness = Spring.StiffnessMedium)
else ->
// 指定动画的执行事件,移动的较慢
spring(stiffness = Spring.StiffnessVeryLow)
}
}) { state ->
// 从 contentWidth 到 contentWidth*2
when (state) {
MyState.Normal -> contentWidth
MyState.Expand -> contentWidth * 2
}
}
// 根据状态改变边框颜色
val color by transition.animateColor { state ->
when (state) {
MyState.Normal -> Color.Red
MyState.Expand -> Color.Green
}
}
ConstraintLayout(
Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(5.dp)
) {
val (row, box, btn) = createRefs()
Row(
modifier = Modifier.constrainAs(row) {
top.linkTo(parent.top)
}
) {
androidx.compose.material.Text(
"normal", modifier = Modifier
.size(contentWidth, contentHeight),
textAlign = TextAlign.Center
)
androidx.compose.material.Text(
"expand", modifier = Modifier
.size(contentWidth, contentHeight),
textAlign = TextAlign.Center
)
}
Box(
modifier = Modifier
.constrainAs(box) {
top.linkTo(row.top)
}
.offset(x = offsetLeft)
.height(contentHeight)
.width(offsetRight - offsetLeft)
.fillMaxSize()
.border(BorderStroke(1.dp, color), RoundedCornerShape(4.dp))
)
Button(
modifier = Modifier.constrainAs(btn) {
top.linkTo(box.bottom)
}, onClick = {
currentState = if (currentState == MyState.Normal) {
MyState.Expand
} else {
MyState.Normal
}
}) {
Text("click")
}
}
}
效果:
6.3 createChildTransition
Transition
还可以通过createChildTransition()
方法创建新的子Transition
,当需要分离多个子组件用到的过渡时,可以通过该方式
@OptIn(ExperimentalTransitionApi::class)
@Preview
@Composable
fun MyTransition3() {
var currentState by remember { mutableStateOf(MyState.Normal) }
// 定义Transition对象
val transition = updateTransition(currentState)
val childTransitionState = transition.createChildTransition {
MyState.Expand
}
val childTransitionBool = transition.createChildTransition {
it == MyState.Expand
}
}
6.4 rememberInfiniteTransition
rememberInfiniteTransition
可以生成一个一直运行的Transition
,方法和一般的Transition
相同,使用时需要指定infiniteRepeatable
:
@Preview
@Composable
fun MyRememberInfiniteTransition() {
val transition = rememberInfiniteTransition()
val animateColor by transition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
Box(
Modifier
.background(animateColor, MaterialTheme.shapes.medium)
.size(50.dp))
}
效果:
三、低级动画
关于低级动画的介绍可以查看官方文档:低级别动画
高级动画已经和compose
进行了结合,而低级动画都是基于协程的API,也就是在使用过程中,我们需要手动启动协程,我们可以使用附带效应的LaunchedEffect()
在compose
中启动一个协程,关于附带效应后续会详细介绍
1.Animation
Animation
是可用的最低级别的动画API,子类型有两种:TargetBasedAnimation
和 DecayAnimation
。除非你需要手动控制动画时间,否则建议使用基于这些类构建的更高级别动画 API,由于平时基本不会使用,这部分仅作了解即可
1.1 TargetBasedAnimation
TargetBasedAnimation
就是正常的执行动画,需要手动根据执行的时间获取动画值:
@Preview
@Composable
fun MyAnimation() {
val basedAnimation = remember {
TargetBasedAnimation(
// 动画时间1s
animationSpec = infiniteRepeatable(tween(1000)),
// 值为float类型
typeConverter = Float.VectorConverter,
// 初始值200f
initialValue = 200f,
// 目标值400f
targetValue = 400f,
)
}
var width by remember {
mutableStateOf(0.dp)
}
LaunchedEffect(basedAnimation) {
val startTime = withFrameNanos { it }
var playTime = 0L
while (true) {
playTime = withFrameNanos { it } - startTime
width = Dp(basedAnimation.getValueFromNanos(playTime))
}
}
Box(
modifier = Modifier
.width(width)// 宽度随动画值而变化
.height(50.dp)
.background(Color.Blue)
)
}
效果:
1.2 DecayAnimation
DecayAnimation
是执行动画过程会有衰减,下面会在Animatable
中使用它
2.Animatable
Animatable
为 Float 和 Color 提供开箱即用的支持,也可以通过TwoWayConverter
对其他类型支持
2.1 基本使用
@Preview
@Composable
fun MyAnimatable() {
val animatable = remember { Animatable(Color.Gray) }
var ok by remember { mutableStateOf(false) }
LaunchedEffect(ok) {
animatable.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
modifier = Modifier
.background(animatable.value)
.size(50.dp)
.clickable {
ok = !ok
}
)
}
效果:
2.2 animateDecay
animateDecay
对DecayAnimation
进行了封装,我们需要传入初始速度initialVelocity
,以及对应的DecayAnimationSpec
,DecayAnimationSpec
直接通过封装好的rememberSplineBasedDecay()
获取即可
suspend fun animateDecay(
initialVelocity: T,// 初始速度
animationSpec: DecayAnimationSpec<T>,// 衰减,估值器
block: (Animatable<T, V>.() -> Unit)? = null
)
@Preview
@Composable
fun MyDecayAnimation() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val decaySpec = rememberSplineBasedDecay<Dp>()
LaunchedEffect(anim) {
delay(3000)
anim.animateDecay(1000.dp, decaySpec)
}
Box(
modifier = Modifier
.padding(top = anim.value)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
)
}
效果:
2.3 exponentialDecay
exponentialDecay
以指数进行衰减,可以传入摩擦系数:
@Preview
@Composable
fun MyDecayAnimation() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
// val decaySpec = rememberSplineBasedDecay<Dp>()
val exponentDecay = exponentialDecay<Dp>(frictionMultiplier = 0.9f)
LaunchedEffect(anim) {
delay(3000)
anim.animateDecay(1000.dp, exponentDecay)
}
Box(
modifier = Modifier
.padding(top = anim.value)// 上边距随动画值而变化
.width(50.dp)
.height(50.dp)
.background(Color.Blue)
)
}
效果:
四、可绘制图形动画
1.Drawable动画
就是传统通过定义xml
的方式的帧动画
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/ic_launcher_foreground"
android:duration="100" />
<item
android:drawable="@drawable/ic_launcher_background"
android:duration="100" />
</animation-list>
2.矢量图动画
矢量图动画可以参考官方文档:矢量图动画
由于需要配合SVG图片,这边不做展示