compose--动画
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 | |
|---|---|
FastOutSlowInEasing
|
LinearOutSlowInEasing
|
FastOutLinearInEasing
|
LinearEasing
|
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图片,这边不做展示
浙公网安备 33010602011771号