アニメーションの動きに特殊な効果を加えることができます。
特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。
上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。
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はアニメーションの形状を定義するインターフェースです。
1 2 3 4 5 | 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クラスです。
1 2 3 4 5 6 | @Stable fun <T> keyframes( init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit ): KeyframesSpec<T> { return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init)) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 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:制御対象の値の型 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @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(マップ)へ登録されます。
1 2 3 4 5 | infix fun T.at(timeStamp: Int): KeyframeEntity<T> { return KeyframeEntity( this ).also { keyframes[timeStamp] = it // timeStamp:変化ポイントの時刻 } // it:KeyframeEntity(変化ポイント) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @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(マップ)へ登録されます。
1 2 3 | infix fun T.atFraction(fraction: Float): KeyframeEntity<T> { return at((durationMillis * fraction).roundToInt()) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @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編)」を参照
1 2 3 | infix fun KeyframeEntity<T>.with(easing: Easing) { this .easing = easing } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @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なので、次のような記述が可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @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の場合と同じ
関連記事: