Animation 中 Spec 用来设置动画效果。Animation 接口有两个实现类 TargetBasedAnimation 和 DecayAnimation, 他们都有各自的 Spec 。
AnimationSpec
AnimationSpec 是TargetBasedAnimation 中使用的 Spec 。 TargetBasedAnimation 是属性值变化范围确定的动画,即有确定的初始值和结束值。下面的介绍中不再重复说明属性值这一条件。
蓝色的是接口,绿色的是类。
泛型 T (属性的数据类型) 的是调用者使用的 Spec
泛型 V 的是 <V:AnimationVector> 框架内部使用的,AnimationVector 我们上一篇有讲。
FloatAnimationSpec 接口专门处理 Float 类型数据 FloatTweenSpec 就相当于 TweenSpec<Float>
。 所以我们只需要了解六个实现类就可以了,我们再把这六个 Spec 分个类。
单次中根据参数不同我们把这四个 Spec 分成时间和弹性两种,先来看弹性
SpringSpec
<T : Any?> SpringSpec(
dampingRatio: Float,// 阻尼比
stiffness: Float, //刚性
visibilityThreshold: T?//可见阈值
)
弹性动画
对比上图
- dampingRatio: 阻尼比越小弹的幅度越大,且弹的次数越多
- stiffness: 刚性越大弹的过程中回到 target 值的时间越短
- visibilityThreshold: T?: 动画可见阈值 动画值小于阈值后动画值不再变化一直是 targetValue
可用常量值
object Spring {
const val StiffnessHigh = 10_000f
const val StiffnessMedium = 1500f
const val StiffnessMediumLow = 400f
const val StiffnessLow = 200f
const val StiffnessVeryLow = 50f
const val DampingRatioHighBouncy = 0.2
const val DampingRatioMediumBouncy = 0.5f
const val DampingRatioLowBouncy = 0.75f
const val DampingRatioNoBouncy = 1f
const val DefaultDisplacementThreshold = 0.01f
}
TweenSpec
TweenSpec(
durationMillis: Int,//动画的时长
delay: Int,//动画延
easing: Easing //插值器
)
可以设置动画时长、延时、插值器。Easing 就是插值器,换成 Interpolator 大家就熟悉了,用来设置动画效果。Easing 我们最后详细介绍。
KeyframesSpec
<T : Any?> KeyframesSpec(config: KeyframesSpec.KeyframesSpecConfig<T>)
也是基于时长的 Spec ,不同于 tween 它可以通过 config 指定关键帧( 值 at time)。
相当于用关键帧将 Spec 分成了几段 TweenSpec,而且分出来的每段都可以指定一个 Easing 。
SnapSpec
<T : Any?> SnapSpec(delay: Int)
没有动画效果,作用就是设置一个延时,经过 dealay 毫秒后直接变成 targetValue。
RepeatableSpec
<T : Any?> RepeatableSpec(
iterations: Int, // 重复几次
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode,
initialStartOffset: StartOffset
)
InfiniteRepeatableSpec
<T : Any?> InfiniteRepeatableSpec(
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode,
initialStartOffset: StartOffset
)
比 RepeatableSpec 少了 iterations 参数,因为它会一直重复 。
Easing
除了 SpringSpec 和 SnapSpec 动画效果都是由 Easing 控制的。还记得上一篇文章我们的 MyTargetBasedAnimation 值的计算么?
override fun getValueFromNanos(playTimeNanos: Long): T {
val valueDiff = targetValueVector.value - initValueVector.value
val progress = playTimeNanos / durationNanos.toFloat()
val valueVector = AnimationVector1D(initValueVector.value + valueDiff * progress)
return typeConverter.convertFromVector(valueVector)
}
progress 是根据时间计算出动画的进度再乘以结束值和初始值的差来计算出动画进行的差量。时间是线性增加的所以整个动画的效果也是线性的。
Easing 接口中的 ,transfrom 方法传入的 fraction 就是上面的 progress ,返回一个新的 progress 。算法不同返回的 progress 不同,计算出的动画差量也不同 ,这样就可以产生不同的动画效果了。
@Stable
fun interface Easing {
fun transform(fraction: Float): Float
}
Compose 中提供了 4 中常用的 Easing 。 LinearEasing 就是传入什么返回什么,间接验证了我们上面的说法。
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
val LinearEasing: Easing = Easing { fraction -> fraction }
CubicBezierEasing
除了 LinearEasing 其他的都是通过 CubicBezierEasing 实现的,所以我们也可以通过 CubicBezierEasing 来自定义 Easing 。
class CubicBezierEasing(
private val a: Float,
private val b: Float,
private val c: Float,
private val d: Float
) : Easing
通过三个点可以画一个贝塞尔曲线
CubicBezierEasing 中一共有 5 个点
- Ps (0,0) 初始点,
- P1(a,b) 第一个控制点,
- Pc(0.5,0.5) 中心点
- P2(c,d) 第二个控制点
- Pe(1,1) 结束点
分成两组 Ps ,P1,Pc ; Pc,P2,Pe , x 坐标是时间
Interpolator 转换成 Easing
添加拓展函数
fun TimeInterpolator.toEasing() = Easing { x -> getInterpolation(x) }
这样就可以使用 android.animation 包中实现 TimeInterpolator 接口的插值器了。
本文代码
代码比较简单直接贴了
AnimationTrackPanel.kt
private const val TAG = "AnimationTrackPanel"
@Composable
fun AnimationTrackPanel(minY: Int, maxY: Int,durationInMs:Int,spec: AnimationSpec<Float>,specDesc:String) {
val textSize = 16.sp
Column(
Modifier
.fillMaxSize()
.padding(top = 10.dp, end = 10.dp)) {
Row(modifier = Modifier.weight(1f)) {
Box(modifier = Modifier
.fillMaxHeight()
.width(20.dp)){
YAxleLabel(minY,maxY,textSize)
}
Box(modifier = Modifier.fillMaxSize()){
Canvas(modifier = Modifier.fillMaxSize()){
val paintColor = Color.Black
drawRect(paintColor, size = size, style = Stroke(1.dp.toPx()))
(minY +1 until maxY).forEach { value ->
val offsetY = size.height - size.height * ((value - minY).toFloat() / (maxY - minY))
drawLine(paintColor, Offset(0f,offsetY), Offset(size.width,offsetY))
}
}
AnimationTrackPath(minY,maxY,durationInMs,spec)
}
}
// X Label
Row(modifier = Modifier.fillMaxWidth().padding(start = 20.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = "[ $specDesc ]", fontSize = textSize)
Text(text = "${durationInMs}ms", fontSize = textSize)
}
}
}
@Composable
fun AnimationTrackPath(minY: Int, maxY: Int,durationInMs:Int,spec: AnimationSpec<Float>){
// forXAxle
val timeProgress = remember(spec) { Animatable(0f) }
//forYAxle
val specTrack = remember(spec) { Animatable(0f) }
LaunchedEffect(timeProgress){
timeProgress.animateTo(1f, tween(durationInMs, easing = LinearEasing))
}
LaunchedEffect(specTrack){
specTrack.animateTo(1f, animationSpec = spec)
}
val path by remember(spec) { mutableStateOf(Path()) }
Canvas(modifier = Modifier.fillMaxSize()){
val y = size.height * (1 - (specTrack.value - minY) / (maxY - minY))
if (path.isEmpty){
path.moveTo(0f,y)
}
val x = timeProgress.value * size.width
path.lineTo(x,y)
drawPath(path, Color.Red, style = Stroke(2.dp.toPx()))
}
}
@Composable
fun YAxleLabel(minValue: Int, maxValue: Int, textSize: TextUnit){
Canvas(modifier = Modifier.fillMaxHeight()) {
val textPaint = Paint().asFrameworkPaint().apply {
this.textSize = textSize.toPx()
}
drawIntoCanvas { canvas ->
for(value in minValue .. maxValue){
val offsetY = size.height - size.height * ((value - minValue).toFloat() / (maxValue - minValue))
val textY = offsetY + (textSize.toPx() / 2)
canvas.nativeCanvas.drawText(
value.toString(),
0f,
textY,
textPaint
)
}
}
}
}
SpecsEnum.kt
enum class SpecsEnum(
val minValue: Int,
val maxValue: Int,
val durationMs: Int,
val spec: AnimationSpec<Float>,
val desc: String
) {
Spring_02_200(
0, 2, 5000,
spring(dampingRatio = 0.2f, stiffness = 200f),
"spring(dampingRatio = 0.2f, stiffness = 200f)"
),
Spring_04_200(
0, 2, 5000,
spring(dampingRatio = 0.4f, stiffness =200f),
"spring(dampingRatio = 0.4f, stiffness =200f)"
),
Spring_02_400(
0, 2, 5000,
spring(dampingRatio = 0.2f, stiffness = 400f),
"spring(dampingRatio = 0.2f, stiffness = 400f)"
),
Spring_02_400_02(
0, 2, 5000,
spring(dampingRatio = 0.2f, stiffness = 400f, visibilityThreshold = 0.2f),
"spring(dampingRatio = 0.2f, stiffness = 400f, visibilityThreshold = 0.2f)"
),
Tween_LinearEasing(
0, 1, 1000,
tween(durationMillis = 1000, easing = LinearEasing),
"tween(durationMillis = 1000, easing = LinearEasing)"
),
Tween_LinearDelay(
0, 1, 2000,
tween(durationMillis = 1000, delayMillis = 500, easing = LinearEasing),
"tween(durationMillis = 1000, delayMillis = 500, easing = LinearEasing)"
),
Tween_FastOutSlowIn(
0, 1, 1000,
tween(durationMillis = 1000, easing = FastOutSlowInEasing),
"tween(durationMillis = 1000, easing = FastOutSlowInEasing)"
),
KeyFrames(
0, 1, 2000,
keyframes {
durationMillis = 2000
0f at 0 with FastOutSlowInEasing // 可省略
0.25f at 1000
0.5f at 1600 with LinearOutSlowInEasing
1f at 2000 //可省略
},
"keyframes"
),
Snap_500(
0, 1, 1000,
snap(delayMillis = 500),
"snap(delayMillis = 500)"
),
RepeatableReverse(
0, 1, 2000,
repeatable(
iterations = 2,
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
"repeatable(2,tween(1000,LinearEasing),RepeatMode.Reverse)"
),
RepeatableRestart(
0, 1, 2000,
repeatable(
iterations = 2,
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
"repeatable(2,tween(1000,LinearEasing),RepeatMode.Restart)"
),
CubicBezierEasing(
0, 1, 1000,
tween(durationMillis = 1000, easing = CubicBezierEasing(0.25f,1f,0.75f,0f)),
"CubicBezierEasing(0.25f,1f,0.75f,0f)"
),
BounceInterpolator(
0, 1, 2000,
tween(durationMillis = 1000, easing = BounceInterpolator().toEasing()),
"BounceInterpolator"
)
}
fun TimeInterpolator.toEasing() = Easing {
x -> getInterpolation(x)
}
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeAnimationSpecTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
var currentSpec by remember { mutableStateOf(SpecsEnum.Spring_02_200) }
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.padding(horizontal = 10.dp)) {
items(SpecsEnum.values()) {
Button( onClick = { currentSpec = it }, shape = RoundedCornerShape(2.dp)) {
Text(text = it.name) }
}
}
AnimationTrackPanel(
minY = currentSpec.minValue,
maxY = currentSpec.maxValue,
durationInMs = currentSpec.durationMs,
spec = currentSpec.spec,
specDesc = currentSpec.desc
)
}
}