アニメーションの動きに特殊な効果を加えることができます。
特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。
上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。
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の場合と同じ
関連記事: