アニメーションの動きに特殊な効果を加えることができます。
特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。
上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。
AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。
この中から、「keyframes」と取りあげて、まとめます。
※環境:Android Studio Iguana | 2023.2.1 Patch 1
Kotlin 1.9.0
Compose Compiler 1.5.1
androidx.compose.animation:* 1.5.0
目次
アニメーションの形状
AnimationSpecはアニメーションの形状を定義するインターフェースです。
interface AnimationSpec<T> {
fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
): VectorizedAnimationSpec<V>
}
Jetpack Composeは実装済みの形状を4つ提供しています。
| 形状 | 概要 | 補足 |
|---|---|---|
| tween | 滑らかな曲線を描いて変化 | 3次のベジェ曲線 |
| spring | バネが振動するように変化 | バネの減衰比(振動の振幅)を選択可能 バネの硬さ(振動の速さ)を選択可能 |
| keyframes | 折れ線を描いて変化 | |
| snap | 指定時間に即座に切り替え |
この中のkeyframesは、折れ線を描いて変化するアニメーションを提供します。keyframesの実態はAnimationSpecを実装したKeyframesSpecクラスです。
@Stable
fun <T> keyframes(
init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
): KeyframesSpec<T> {
return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init))
}

private val StartX = 20.dp
private val EndX = 260.dp
@Preview
@Composable
private fun AnimationSpecSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
...
Text(text = "[ keyframes ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = keyframes {
durationMillis = 1000
140.dp.at(100)
140.dp.at(200)
40.dp.at(400)
260.dp.at(1000)
}
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
...
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
関数の引数
keyframes関数は次のような引数を持ち、形状を調整できます。
| 引数 | 概要 | |
|---|---|---|
| init | KeyframesSpec.KeyframesSpecConfig | 折れ線の仕様 |
折れ線の仕様(init)
initは折れ線の仕様を指定します。
KeyframesSpecConfigは仕様を定義するクラスです。3つのプロパティに折れ線の情報を格納します。
| プロパティ | 概要 | |
|---|---|---|
| durationMillis | Int | アニメーションの継続時間 デフォルト:300 |
| delayMillis | Int | アニメーションの待機時間 デフォルト:0 |
| keyframes | MutableMap<Int, KeyFrameEntity<T>> | 変化ポイントのリスト |
| ※T:制御対象の値の型 | ||
@Immutable
class KeyframesSpec<T>(val config: KeyframesSpecConfig<T>) : DurationBasedAnimationSpec<T> {
class KeyframesSpecConfig<T> {
var durationMillis: Int = DefaultDurationMillis
var delayMillis: Int = 0
internal val keyframes = mutableMapOf<Int, KeyframeEntity<T>>()
infix fun T.at(timeStamp: Int): KeyframeEntity<T> {
return KeyframeEntity(this).also {
keyframes[timeStamp] = it // timeStamp:変化ポイントの時刻
} // it:KeyframeEntity(変化ポイント)
}
infix fun T.atFraction(fraction: Float): KeyframeEntity<T> {
return at((durationMillis * fraction).roundToInt())
}
infix fun KeyframeEntity<T>.with(easing: Easing) {
this.easing = easing
}
...
}
...
class KeyframeEntity<T> internal constructor( // 変化ポイント
internal val value: T, // ・到達値
internal var easing: Easing = LinearEasing // ・線分の形状
) {
internal fun <V : AnimationVector> toPair(convertToVector: (T) -> V) =
convertToVector.invoke(value) to easing
...
}
}
そして、keyframes(マップ)へ折れ線の変化ポイント(KeyframeEntity)の登録が必要になりますが、これには2つの方法(atとatFraction関数)が実装されています。
変化ポイント(at)
折れ線の変化ポイント(KeyframeEntity)を絶対的な値(Value)と時刻(Time)で指定します。
指定された変化ポイントは、keyframes(マップ)へ登録されます。
infix fun T.at(timeStamp: Int): KeyframeEntity<T> {
return KeyframeEntity(this).also {
keyframes[timeStamp] = it // timeStamp:変化ポイントの時刻
} // it:KeyframeEntity(変化ポイント)
}

@Preview_mdpi
@Composable
private fun TimeStampSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ TimeStamp ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = if(_toggle.value) keyframes {
durationMillis = 2000
20.dp.at(0) // a0
150.dp.at(800) // a1
130.dp.at(1200) // a2
260.dp.at(2000) // a3
} else keyframes { }
)
System.out.println("%04d,%6.4f".format(Tim4(), _posX.value.value))
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
変化ポイント(atFraction)
折れ線の変化ポイント(KeyframeEntity)を絶対的な値(Value)と割合(Fraction)で指定します。
指定された変化ポイントは、keyframes(マップ)へ登録されます。
infix fun T.atFraction(fraction: Float): KeyframeEntity<T> {
return at((durationMillis * fraction).roundToInt())
}

@Preview_mdpi
@Composable
private fun FractionSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ Fraction ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = if(_toggle.value) keyframes {
durationMillis = 2000
20.dp.atFraction(0.0f) // a0: 0 / 2000 = 0.0f
150.dp.atFraction(0.4f) // a1: 800 / 2000 = 0.4f
130.dp.atFraction(0.6f) // a2: 1200 / 2000 = 0.6f
260.dp.atFraction(1.0f) // a3: 2000 / 2000 = 1.0f
} else keyframes { }
)
System.out.println("%04d,%6.4f".format(Tim4(), _posX.value.value))
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
※サンプルの実行結果は省略、atの場合と同じ
曲線のアルゴリズム(with)
線分の形状を曲線のアルゴリズム(Easing)で指定します。
※曲線のアルゴリズムについては「Compose Animation:AnimationSpecでアニメの形状を指定(tween編)」を参照
infix fun KeyframeEntity<T>.with(easing: Easing) {
this.easing = easing
}

@Preview_mdpi
@Composable
private fun EasingSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ with Easing ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = if(_toggle.value) keyframes {
durationMillis = 2000
20.dp.at(0).with(FastOutSlowInEasing) // a0
150.dp.at(800) // a1
130.dp.at(1200).with(FastOutSlowInEasing) // a2
260.dp.at(2000) // a3
} else keyframes { }
)
System.out.println("%04d,%6.4f".format(Tim4(), _posX.value.value))
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
.clickable { _toggle.value = !_toggle.value }
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
継続・待機時間(durationMillis, delayMillis)
durationMillisはアニメーションの継続時間を、delayMillisはアニメーション開始までの待機時間を指定します。

例:durationMillisの違い
「durationMillis≠最終変化ポイントの時刻(timeStamp)」であった場合は、小さい方の時刻でアニメーションは打ち切られます。

例:delayMillisの違い

infixで記述
at、atFractionとwith関数はInfixなので、次のような記述が可能です。
@Preview_mdpi
@Composable
private fun InfixSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ Infix ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = if(_toggle.value) keyframes {
durationMillis = 2000
20.dp at 0 with FastOutSlowInEasing
150.dp at 800
130.dp at 1200 with FastOutSlowInEasing
260.dp at 2000
} else keyframes { }
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
.clickable { _toggle.value = !_toggle.value }
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
※サンプルの実行結果は省略、withの場合と同じ
関連記事:
