Compose Animation:AnimationSpecでアニメの形状を指定(tween編)

投稿日:  更新日:

アニメーションの動きに特殊な効果を加えることができます。

特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。

上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。

AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。

この中から、「tween」と取りあげて、まとめます。

※環境: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指定時間に即座に切り替え

この中のtweenは、滑らかな曲線を描いて変化するアニメーションを提供します。tweenの実態はAnimationSpecを実装したTweenSpecクラスです。

@Stable
fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)

tweenの変化

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 = "[ tween ]")
        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 = tween(durationMillis = 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")
        }
    }
}
スポンサーリンク

関数の引数

tween関数は次のような引数を持ち、形状を調整できます。

引数概要
durationMillisIntアニメーションの継続時間
delayMillisIntアニメーションの開始までの待機時間
easingEasing曲線のアルゴリズム
・定義済みパラメータ
  FastOutSlowInEasing
  LinearOutSlowInEasing
  FastOutLinearInEasing
  LinearEasing
スポンサーリンク

継続・待機時間(durationMillis, delayMillis)

durationMillisはアニメーションの継続時間を、delayMillisはアニメーション開始までの待機時間を指定します。

継続・待機時間

例:durationMillisの違い

durationMillisの違い

durationMillisのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp

@Preview_mdpi320x480
@Composable
private fun DurationSample() {
    Column {
        val _toggle = remember { mutableStateOf(false) }
        Spacer(modifier = Modifier.height(20.dp))
        // ----------------------------------------------------------
        Text(text = "[ Duration  500ms ]")
        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 = tween(durationMillis = 500)
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ Duration 1000ms ]")
        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 = tween(durationMillis = 1000)
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ Duration 1500ms ]")
        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 = tween(durationMillis = 1500)
            )
            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")
        }
    }
}

例:delayMillisの違い

delayMillisの違い

delayMillisのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp

@Preview_mdpi320x480
@Composable
private fun DelaySample() {
    Column {
        val _toggle = remember { mutableStateOf(false) }
        Spacer(modifier = Modifier.height(20.dp))
        // ----------------------------------------------------------
        Text(text = "[ Delay  200ms ]")
        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 = tween(durationMillis = 500, delayMillis = 200)
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ Delay  500ms ]")
        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 = tween(durationMillis = 500, delayMillis = 500)
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ Delay 1000ms ]")
        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 = tween(durationMillis = 500, delayMillis = 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")
        }
    }
}
スポンサーリンク

曲線のアルゴリズム(easing)

easingは曲線のアルゴリズムを指定します。

Jetpack Composeは表に示す4つの曲線を準備していて、その内の3つは制御点の異なる3次のベジェ曲線です。

パラメータアルゴリズム定義
FastOutSlowInEasingベジェ曲線CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
LinearOutSlowInEasingCubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
FastOutLinearInEasingCubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
LinearEasing直線Easing { fraction -> fraction }
※CubicBezierEasing:3次のベジェ曲線を表現したクラス
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)

曲線のアルゴリズム

easingのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp

@Preview_mdpi320x480
@Composable
private fun EasingSample() {
    Column {
        val _toggle = remember { mutableStateOf(false) }
        Spacer(modifier = Modifier.height(20.dp))
        // ----------------------------------------------------------
        Text(text = "[ FastOutSlowIn ]")
        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 = tween(
                    durationMillis = 1000,
                    easing = FastOutSlowInEasing
                )
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ LinearOutSlowIn ]")
        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 = tween(
                    durationMillis = 1000,
                    easing = LinearOutSlowInEasing
                )
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ FastOutLinearIn ]")
        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 = tween(
                    durationMillis = 1000,
                    easing = FastOutLinearInEasing
                )
            )
            Image(
                painter = painterResource(R.drawable.baseline_toys_black_36),
                contentDescription = null,
                modifier = Modifier
                    .size(36.dp)
                    .offset(x = _posX.value)
            )
        }
        // ----------------------------------------------------------
        Text(text = "[ Linear ]")
        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 = tween(
                    durationMillis = 1000,
                    easing = LinearEasing
                )
            )
            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")
        }
    }
}
スポンサーリンク

ベジェ曲線

ベジェ曲線は始点と終点を結ぶ、滑らかな曲線を描くアルゴリズムです。中点(始点と終点の間にある点)の数と位置より、曲線の軌道が変わります。

アルゴリズム

始点、終点と中点を合わせて制御点と言います。制御点がN個の場合、曲線の式はN-1次式になります。ですので、制御点4個の曲線を、「3次のベジェ曲線」と呼びます。

アルゴリズムはとても簡単です。下記は3次のベジェ曲線を図示したものです。

ベジェ曲線のアルゴリズム

  • (0)制御点を4つ定義(P0:始点、P1,2:中点、P3:終点)
  • (1-1)線分P0-P1をt:1-tの比率で分割する点P4を打つ
  • (1-2)線分P1-P2をt:1-tの比率で分割する点P5を打つ
  • (1-3)線分P2-P3をt:1-tの比率で分割する点P6を打つ
  • (2-1)線分P4-P5をt:1-tの比率で分割する点P7を打つ
  • (2-2)線分P5-P6をt:1-tの比率で分割する点P8を打つ
  • (3-1)線分P7-P8をt:1-tの比率で分割する点P9を打つ
  • (4)t←0~1まで(1)~(3)を繰り返した時のP9が作る軌道

CubicBezierEasingクラス

CubicBezierEasingは3次のベジェ曲線を表すクラスです。アニメーションの形状を定義するために作られています。

@Immutable
class CubicBezierEasing(
    private val a: Float,
    private val b: Float,
    private val c: Float,
    private val d: Float
) : Easing { ... }

制御点は次のようになります。アニメーションの形状が「一辺が1.0fの正方形」の範囲へ正規化されている点に注意してください。

  • P0:(0.0f, 0.0f) … 固定、アニメーションの開始
  • P1:( a, b)
  • P2:( c, d)
  • P3:(1.0f, 1.0f) … 固定、アニメーションの終了

CubicBezierEasingの制御点

CubicBezierEasingの例

private val StartX = 20.dp
private val EndX = 260.dp

@Preview_mdpi320x480
@Composable
private fun CustomEasingSample() {
    Column {
        val _toggle = remember { mutableStateOf(false) }
        Spacer(modifier = Modifier.height(20.dp))
        // ----------------------------------------------------------
        Text(text = "[ Custom ]")
        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 = tween(
                    durationMillis = 1000,
                    easing = CubicBezierEasing(0.2f, 0.6f, 0.8f, 0.4f)
                )
            )
            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")
        }
    }
}
スポンサーリンク

関連記事:

animate*AsState関数は制御する値のタイプにより、数種類が準備されています。 どの関数も、animateValueAsStateが原型のラッパー関数であり、動作は同じです。 ここでは、各々の関数について、サンプルを示します。 また、animateValueAsStateのサンプルは、独自の関数を作る方法を紹介しています。 ※環境:Android Studio Iguana | 2023.2.1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.animation:* 1.5.0 ...
Jetpack Composeが提供するアニメーションAPIは非常に充実しています。 「どのAPIを使えば最適なのか?」と、その選択を迷うくらいに数が多いです。ドキュメントは、「〇〇占い」に登場するようなYes/Noの設問ツリーを掲載(アニメーションAPIを選択する)して、選択の手助け行っています。 ここでは、アニメーションAPIの中から「animate*AsState(*は型名が入る)」を取りあげて、まとめます。 animate*AsStateは、APIの中で最も汎用性があります。まず始めに抑えておくべきアニメーションAPIです。 ※環境:Android Studio Iguana | 2023.2.1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.animation:* 1.5.0 ...
アニメーションの動きに特殊な効果を加えることができます。 特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。 上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。 AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。 この中から、「spring」と取りあげて、まとめます。 ※環境:Android Studio Iguana | 2023.2.1 Patch 1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.animation:* 1.5.0 ...
アニメーションの動きに特殊な効果を加えることができます。 特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。 上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。 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はアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。 この中から、「snap」と取りあげて、まとめます。 ※環境:Android Studio Iguana | 2023.2.1 Patch 1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.animation:* 1.5.0 ...
スポンサーリンク