Jetpack Compose 动画
在compose中,没有属性动画,而是使用了一套全新的动画API
状态转移型动画 animateXxxAsState
以需要给box的size从46dp平滑扩大到96dp为例
fun AnimateAsState() {
var isSmall by remember { mutableStateOf(true) }
val size = animateDpAsState(if (isSmall) 46.dp else 96.dp)
Box(Modifier.size(size.value).clickable {
isSmall = !isSmall
}.background(Color.Green)) {
}
}
在使用animateAsState动画时不需要使用remember因为在内部实现,同样不需要使用mutableStateOf包裹,同样是因为内部已经实现
不可以使用by 因为by是说把左边的读写交给右边代理
这样写会报错
var size by animateDpAsState(if (isSmall) 46.dp else 96.dp)
Type 'State<Dp>' has no method 'setValue(Nothing?, KProperty<*>, Dp)' and thus it cannot serve as a delegate for var (read-write property)
因为animateDpAsState只可以读,不可以写
我们无法手动更改animateDpAsState里面的值,但可以通过recompose时里面的值发生了变化从而由animateDpAsState自动改变
那我们怎么定义动画呢?需要在最开始就定义好,然后通过在recompose时改变 animateDpAsState里面的值
可以使用的animateAsState动画
再看一个多个状态的例子
private object SizeState {
val SMALL_SIZE = 46.dp
val MIDDLE_SIZE = 96.dp
val LARGE_SIZE = 142.dp
}
@Composable
fun AnimateAsState() {
var size by remember { mutableStateOf(SizeState.SMALL_SIZE) }
val sizeAnim = animateDpAsState(size)
Box(Modifier.size(sizeAnim.value).clickable {
if (size == SizeState.SMALL_SIZE) {
size = SizeState.MIDDLE_SIZE
} else if (size == SizeState.MIDDLE_SIZE) {
size = SizeState.LARGE_SIZE
} else if (size == SizeState.LARGE_SIZE) {
size = SizeState.SMALL_SIZE
}
}.background(Color.Green)) {
}
}
Animatable
animAsState是对Animatable使用便捷性的扩展,无法指定初值,功能受到了限制,如果需要可以使用Animatable
Animatable需要传Float类型的参数,如果传其它类型的参数需要定义TwoWayConverter指定怎么将其它类型的参数转化为Float以及将Float转为其它类型的参数
以Dp为例
val anim = remember { Animatable(8.dp, Dp.VectorConverter) }
/**
* A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
*/
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
get() = DpToVector
ps:其实float类型也有一个TwoWayConverter Float.VectorConverter
val anim = remember { Animatable(8f) }
fun Animatable(
initialValue: Float,
visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) = Animatable(
initialValue,
Float.VectorConverter,
visibilityThreshold
)
private val FloatToVector: TwoWayConverter<Float, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it) }, { it.value })
但其实AnimationVector1D只是对float简单的包装,在Float.VectorConverter里的转换规则可以看出这一点
一个使用的简单例子
@Composable
fun AnimatableLearn() {
var big by remember { mutableStateOf(false) }
var size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
//不会重复执行
LaunchedEffect(Unit) {
anim.animateTo(size)
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
Animatable动画的启动要在协程中,并且我们不能直接使用lifecycleScope.launch
会报错并且提示Calls to launch should happen inside a LaunchedEffect and not composition
这是因为这样开启的协程并没有做compose重组的支持从而导致协程的重复执行
我们可以使用 LaunchedEffect,跟随一个key和要执行的代码块,如果只需要执行一次就填入Unit的key
如何设置初始值呢
使用snapTo函数
@Composable
fun AnimatableLearn() {
var big by remember { mutableStateOf(false) }
var size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
//不会重复执行
LaunchedEffect(Unit) {
//here
anim.snapTo(if(big) 192.dp else 0.dp)
anim.animateTo(size)
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
AnimationSpec
AnimationSpec定义了动画的具体配置
AnimationSpec的继承树:
TweenSpec
AnimationSpec可以用来设置动画的变化曲线以及持续时间,延迟开始事件
首先介绍TweenSpec
使用示例:
@Composable
fun TweenSpecLearn() {
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
//这俩种方式都可以 第二种写法方便些
// anim.animateTo(size, TweenSpec(1000, 0, LinearEasing))
anim.animateTo(size, tween(1000, 0, LinearEasing))
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
TweenSpec参数解释
/**
* Creates a TweenSpec configured with the given duration, delay, and easing curve.
*
* @param durationMillis duration of the [VectorizedTweenSpec] animation.
* @param delay the number of milliseconds the animation waits before starting, 0 by default.
* @param easing the easing curve used by the animation. [FastOutSlowInEasing] by default.
*/
@Immutable
class TweenSpec<T>(
val durationMillis: Int = DefaultDurationMillis,
val delay: Int = 0,
val easing: Easing = FastOutSlowInEasing
) : DurationBasedAnimationSpec<T>
前俩个很好理解,最后一个easing可以设置动画的变化曲线
有四种默认的
/**
* Elements that begin and end at rest use this standard easing. They speed up quickly
* and slow down gradually, in order to emphasize the end of the transition.
*
* Standard easing puts subtle attention at the end of an animation, by giving more
* time to deceleration than acceleration. It is the most common form of easing.
*
* This is equivalent to the Android `FastOutSlowInInterpolator`
*/
//先加速再减速 适合从一个状态到另一个状态
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 }
除此之外,还可以自定义CubicBezierEasing曲线
SnapSpec
类似于snapTo 可以让动画突变,区别是SnapSpec可以设置延时
@Composable
fun TweenSpecLearn() {
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
// anim.animateTo(size, TweenSpec(1000, 0, LinearEasing))
// anim.animateTo(size, tween(1000, 0, LinearEasing))
anim.animateTo(size, SnapSpec(1000))
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
KeyframeSpec
可以用来定义关键帧
@Composable
fun KeyframeSpecLearn() {
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, keyframes {
durationMillis = 450
delayMillis = 100
//表示在150ms时变到144dp 同时使用with中缀函数指定速度曲线 若不填则默认是线性变化
144.dp at 150 with FastOutSlowInEasing //一定要注意这里是150到300ms的速度曲线 而不是 0-150ms的
//表示在300ms时变到20dp
20.dp at 300
})
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
SpringSpec
SpringSpec基于弹簧物理模型,无法定义精确的运动时间,可以通过阻尼比dampingRatio和刚度stiffness来定义动画的形式
代码示例
@Composable
fun SpringSpecLearn() {
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, spring(Spring.DampingRatioHighBouncy, StiffnessLow))
//炸弹效果
anim.animateTo(48.dp, spring(Spring.DampingRatioHighBouncy, StiffnessMedium), 2000.dp)
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
spring接收三个参数
@Stable
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T> =
SpringSpec(dampingRatio, stiffness, visibilityThreshold)
dampingRatio为阻尼比,可以理解成弹簧的回弹次数,这个值越小回弹次数越多
stiffness为刚度,这个值越大回弹速度越快
visibilityThreshold 定义弹簧回弹幅度到哪个界限时停止弹动 这个值越小越精确,过大会导致弹簧突然跃变到原位
RepeatableSpec
RepeatableSpec 可以让动画重复执行 需要和DurationBasedAnimationSpec配合使用(即tween snap keyframe)
示例代码
@Composable
fun RepeatableSpecLearn() {
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(
size, RepeatableSpec(
3, tween(), RepeatMode.Reverse,
StartOffset(500, StartOffsetType.FastForward)
)
)
}
Box(
Modifier.size(anim.value)
.background(Color.Green).clickable {
big = !big
}
)
}
参数说明:
class RepeatableSpec<T>(
//重复次数
val iterations: Int,
//真正执行的Spec
val animation: DurationBasedAnimationSpec<T>,
//有俩种模式Restart和Reverse
//[Restart] will restart the animation and animate from the start value to the end value.
//[Reverse] will reverse the last iteration as the animation repeats.
val repeatMode: RepeatMode = RepeatMode.Restart,
//默认动画启动的延时 StartOffset接收俩个参数 一个是时间,还有一个是StartOffsetType
//Delay是延迟动画的启动时间 FastForward是快进动画的时间
val initialStartOffset: StartOffset = StartOffset(0)
)
animateDecay
消散型动画,常用来滑动减速,需要指定初始速度,不指定目标值
使用示例:
@Composable
fun AnimateDecayLearn() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val decay = remember { exponentialDecay<Dp>() }
// val decay = rememberSplineBasedDecay<Dp>()
LaunchedEffect(Unit) {
delay(1000)
anim.animateDecay(1000.dp, decay)
}
Box(
Modifier
.padding(0.dp, anim.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Green)
)
}
DecayAnimationSpec
DecayAnimationSpec和AnimationSpec是独立的俩个接口,我们可以使用rememberSplineBasedDecay和exponentialDecay
前者是安卓原生recyclerview和listview以及scrollview中的滑动算法,专门争对px设计,会根据像素密度对滑动进行修正
后者没有专门争对px设计,适用于比如角度的旋转或者需要使用dp的场景
如果需要争对px做动画,则用SplineBasedDecay,其他情况可以用exponentialDecay
使用block参数监听每一帧
block高阶函数会在动画的每一帧都被执行一次
示例:让俩个小方块齐头并进
@Composable
fun BlockParaLearn() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
var padding = 0.dp
val decay = rememberSplineBasedDecay<Dp>()
LaunchedEffect(Unit) {
delay(1000)
anim.animateDecay(1000.dp, decay) {
padding = value
}
}
Row {
Box(
Modifier
.padding(0.dp, anim.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Green)
)
Box(
Modifier
.padding(0.dp, padding, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Red)
)
}
}
动画的取消
一个动画开始取消另外一个动画
@Composable
fun AnimationCancellation() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val anim1 = remember { Animatable(0.dp, Dp.VectorConverter) }
val padding = remember { mutableStateOf(0.dp) }
val decay = rememberSplineBasedDecay<Dp>()
LaunchedEffect(Unit) {
delay(1000)
anim.animateDecay(1000.dp, decay) {
padding.value = value
}
}
LaunchedEffect(Unit) {
delay(1500)
anim1.animateDecay(1500.dp, decay) {
padding.value = value
}
}
Box(
Modifier
.padding(0.dp, padding.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Green)
)
}
stop取消,注意不能在开启动画的协程里
可以看到方块的运动会突停
@Composable
fun AnimationCancellation() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val padding = remember { mutableStateOf(0.dp) }
val decay = rememberSplineBasedDecay<Dp>()
LaunchedEffect(Unit) {
delay(1000)
anim.animateDecay(2000.dp, decay) {
padding.value = value
}
}
LaunchedEffect(Unit) {
delay(1100)
anim.stop()
}
Box(
Modifier
.padding(0.dp, padding.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Green)
)
}
上下界 anim.updateBounds
@Composable
fun AnimationCancellation() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
anim.updateBounds(0.dp, 1000.dp)
val padding = remember { mutableStateOf(0.dp) }
val decay = rememberSplineBasedDecay<Dp>()
LaunchedEffect(Unit) {
anim.animateDecay(2000.dp, decay) {
padding.value = value
}
}
Box(
Modifier
.padding(0.dp, padding.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Green)
)
}
animateDecay的返回值是一个AnimationResult
class AnimationResult<T, V : AnimationVector>(
/**
* The state of the animation in its last frame before it's canceled or reset. This captures
* the animation value/velocity/frame time, etc at the point of interruption, or before the
* velocity is reset when the animation finishes successfully.
*/
val endState: AnimationState<T, V>,
/**
* The reason why the animation has ended. Could be either of the following:
* - [Finished], when the animation finishes successfully without any interruption
* - [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
* [upperBound][Animatable.upperBound] in any dimension, the animation will end with
* [BoundReached] being the end reason.
*/
val endReason: AnimationEndReason
) {
override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
}
enum class AnimationEndReason {
/**
* Animation will be forced to end when its value reaches upper/lower bound (if they have
* been defined, e.g. via [Animatable.updateBounds])
*
* Unlike [Finished], when an animation ends due to [BoundReached], it often falls short
* from its initial target, and the remaining velocity is often non-zero. Both the end value
* and the remaining velocity can be obtained via [AnimationResult].
*/
BoundReached,
/**
* Animation has finished successfully without any interruption.
*/
Finished
}
里面可以返回动画停止的原因和停止时的状态比如速度
Transition
相比较于原来的Animatable针对于单个属性做动画,Transition更加适合于给多个属性做动画
Animatable创建了多个对象 多个协程
Transition一个协程
同时 transition可以使用preview功能方便的调试动画(需要加上label属性)
@Composable
fun TransitionSquare() {
var big by remember { mutableStateOf(false) }
val transition = updateTransition(big, label = "big")
val size by transition.animateDp(label = "size") { if (it) 96.dp else 48.dp }
val corner by transition.animateDp(label = "corner") { if (it) 0.dp else 18.dp }
//原来我们的写法。也可以实现同样的
// val size by animateDpAsState(if (big) 96.dp else 58.dp)
// val corner by animateDpAsState(if (big) 0.dp else 18.dp)
Box(
Modifier
.size(size)
.clip(RoundedCornerShape(corner))
.background(Color.Green)
.clickable {
big = !big
}
)
}
spec参数
可以根据不同的状态设置不同的spec
@Preview
@Composable
fun TransitionSquare() {
var big by remember { mutableStateOf(false) }
val transition = updateTransition(big, label = "big")
val size by transition.animateDp({
when {
//根据状态选择不同的AnimationSpec
false isTransitioningTo true -> spring()
else -> tween()
}
}, label = "size") { if (it) 96.dp else 48.dp }
val corner by transition.animateDp(label = "corner") { if (it) 0.dp else 18.dp }
Box(
Modifier
.size(size)
.clip(RoundedCornerShape(corner))
.background(Color.Green)
.clickable {
big = !big
}
)
}
AnimatedVisibility
AnimatedVisibility是对Transition动画的封装,可以用来控制组件出现和消失的动画
它只可以用于一个组件的布局,多个组件需要使用多个AnimatedVisibility
基本使用
@Composable
fun AnimatedVisibilityColumn() {
Card(Modifier.fillMaxSize()) {
var expanded by remember { mutableStateOf(false) }
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(expanded) {
TransitionSquare()
}
Button(onClick = { expanded = !expanded }) {
Text("expand")
}
}
}
}
AnimatedVisibility参数
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
)
重点介绍enter参数
可以配置
fadeIn()淡入淡出动画
slideIn()滑入动画
expandIn()裁切动画 expandin expandFrom设置裁切方向 clip设置是否裁切,默认为true,如果为false,则动画元素不裁切直接出现,其它元素发生缓慢位移,若为true则其它元素会直接到动画后的位置
scaleIn()伸缩动画
这些xxxIn()的参数可以填 animationSpec和初始值
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityColumn() {
Card(Modifier.fillMaxSize()) {
var expanded by remember { mutableStateOf(false) }
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(expanded, enter = fadeIn(tween(durationMillis = 5000), 0.3f)) {
TransitionSquare()
}
AnimatedVisibility(
expanded,
enter = slideIn(
tween(durationMillis = 5000),
initialOffset = { fullSize: IntSize ->
//fullSize是动画元素的宽高
IntOffset(
-fullSize.width,
-fullSize.height
)
})
) {
TransitionSquare()
}
AnimatedVisibility(
expanded,
enter = scaleIn(
tween(durationMillis = 5000),
//从x = 0 y = 0 开始伸缩
transformOrigin = TransformOrigin(0f, 0f)
)
) {
TransitionSquare()
}
AnimatedVisibility(
expanded,
// expandFrom可以控制从哪里开始裁切
enter = expandIn(tween(durationMillis = 5000), expandFrom = Alignment.TopCenter)
) {
TransitionSquare()
}
Button(onClick = { expanded = !expanded }) {
Text("start")
}
}
}
}
enter参数的默认参数右边的expandVertically就是由上面所说的expandIn配制而成
@Stable
fun expandVertically(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
expandFrom: Alignment.Vertical = Alignment.Bottom,
clip: Boolean = true,
initialHeight: (fullHeight: Int) -> Int = { 0 },
): EnterTransition {
return expandIn(animationSpec, expandFrom.toAlignment(), clip) {
IntSize(it.width, initialHeight(it.height))
}
}
+号重载符做的是+号左边如果对于上面所提到的四个属性自己没有则取+号右边的,自己有则取自己的
operator fun plus(enter: EnterTransition): EnterTransition {
return EnterTransitionImpl(
TransitionData(
fade = data.fade ?: enter.data.fade,
slide = data.slide ?: enter.data.slide,
changeSize = data.changeSize ?: enter.data.changeSize,
scale = data.scale ?: enter.data.scale
)
)
}
CrossFade
与AnimatedVisibility控制组件的消失与出现,而CrossFade可以用来控制俩个组件之间的切换,并且俩个组件之间的切换动画为fade动画
@Composable
fun CrossFadeLearn() {
Column {
//除了使用boolean外也可以使用interesting,然后使用when语句切换
var expanded by remember { mutableStateOf(true) }
Crossfade(expanded) {
//必须要使用it 而不能使用expanded
if (it) {
TransitionSquare()
} else {
Box(Modifier.size(24.dp).background(Color.Red))
}
}
Button(onClick = { expanded = !expanded }) {
Text("start")
}
}
}
AnimatedContent
CrossFade控制俩个组件间的切换使用简单但是效果也很简单
AnimatedContent可以进行更加复杂的控制
标题:Jetpack Compose 动画
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2022/12/06/1669883580403.html