0Pricing
Android Academy · レッスン

ジェスチャーとスプリングアニメーション

物理法則に基づく自然な動きを実現します。

「ジェスチャーとスプリングアニメーション」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

インタラクティブで物理的な動き

最も魅力的なアニメーションは、ユーザーの指に反応し、勢いや弾みのある実物のような動きをします。これを作るには、ジェスチャーと物理ベースのアニメーションを組み合わせます。

このレッスンでは、Animatable、スプリング、フリング減衰、およびそれらをドラッグジェスチャーに接続して自然でインタラクティブなUIを作る方法を扱います。

Animatable:低レベルの制御

animate*AsState が状態に反応するのに対し、Animatable では命令型の制御ができます。remember で1つ保持し、コルーチンから animateTo や snapTo などを呼び出して動かします。

これはジェスチャー駆動の動きの基盤になります。

@Composable
fun BounceOnClick() {
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .graphicsLayer { scaleX = scale.value; scaleY = scale.value }
            .clickable {
                scope.launch {
                    scale.animateTo(1.3f, spring())
                    scale.animateTo(1f, spring())
                }
            }
    )
}

Springs:dampingRatioとstiffness

spring 仕様は、次の2つの値で定義されます。

  • dampingRatio - 弾みやすさ。値が低いほど大きく弾みます。1f(臨界減衰)ではオーバーシュートしません。
  • stiffness - 速さ。値が高いほど、より速く目標値に到達します。

Composeには Spring.DampingRatioMediumBouncy や Spring.StiffnessLow などの定数が用意されているため、通常は数値を直接指定する必要はありません。

val spec = spring<Float>(
    dampingRatio = Spring.DampingRatioLowBouncy,
    stiffness = Spring.StiffnessMedium
)

// later, inside a coroutine:
// offset.animateTo(targetValue = 0f, animationSpec = spec)

ドラッグジェスチャーの検出

動きをインタラクティブにするには、タッチ入力を読み取ります。Modifier.pointerInputとdetectDragGesturesを使うと、ユーザーが指を動かしたときのドラッグの移動量が通知されます。

その移動量をオフセットに加算し、コンポーザブルに適用します。

@Composable
fun Draggable() {
    var offsetX by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .size(80.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectDragGestures { change, drag ->
                    change.consume()
                    offsetX += drag.x
                }
            }
    )
}

スプリングで元の位置に戻す

ドラッグとAnimatableを組み合わせると、「元の位置に戻る」効果を作れます。ドラッグ中はsnapToで指の位置に即座に移動し、指を離したらanimateTo(0f)をスプリングで実行して、要素を跳ね返るように元の位置へ戻します。

@Composable
fun SnapBack() {
    val offsetX = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .offset { IntOffset(offsetX.value.roundToInt(), 0) }
            .size(80.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { change, drag ->
                        change.consume()
                        scope.launch { offsetX.snapTo(offsetX.value + drag.x) }
                    },
                    onDragEnd = {
                        scope.launch { offsetX.animateTo(0f, spring()) }
                    }
                )
            }
    )
}

速度を付けてフリングする

実際のフリングでは、指を離した後も動き続けます。VelocityTrackerで速度を追跡し、それをanimateDecayに渡すことで、摩擦ベースのDecayAnimationSpecを使って要素を自然に停止させます。

val decay = rememberSplineBasedDecay<Float>()

// on drag end, with measured velocity:
// scope.launch {
//     offsetX.animateDecay(
//         initialVelocity = velocity,
//         animationSpec = decay
//     )
// }

アンカーを使ったスワイプで閉じる操作

スワイプで閉じる操作のように、停止位置が固定されたスワイプジェスチャーにはanchoredDraggableを使います。状態とそのピクセル位置を定義すると、Composeが状態間のスプリングアニメーションを処理します。

enum class DragValue { Start, End }

val state = remember {
    AnchoredDraggableState(
        initialValue = DragValue.Start,
        anchors = DraggableAnchors {
            DragValue.Start at 0f
            DragValue.End at 300f
        }
    )
}

無限アニメーションと繰り返しアニメーション

点滅するドットやローディングのシマーなど、連続する動きにはrememberInfiniteTransitionとinfiniteRepeatableを使います。RepeatModeをReverseにすると往復し、Restartにすると最初から繰り返します。

@Composable
fun Pulse() {
    val transition = rememberInfiniteTransition(label = "pulse")
    val scale by transition.animateFloat(
        initialValue = 1f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
            animation = tween(700),
            repeatMode = RepeatMode.Reverse
        ),
        label = "scale"
    )
    Box(Modifier.graphicsLayer { scaleX = scale; scaleY = scale })
}

ジェスチャーとスプリングの組み合わせ

代表的なインタラクティブパターンは、ドラッグして動かし、指を離して停止させることです。ドラッグ中に速度を追跡し、距離や速度に応じて、animateDecayでフリングするか、springでアンカーへ移動させます。

ボトムシート、カルーセル、スワイプで閉じられるカードが自然に感じられるのは、まさにこの仕組みによるものです。

// pattern:
// 1) onDrag    -> animatable.snapTo(current + delta)
// 2) onDragEnd -> if (fast) animatable.animateDecay(velocity, decay)
//                 else      animatable.animateTo(nearestAnchor, spring())

純粋なKotlinでスプリングを停止させる

スプリングがどのように停止するかを理解するため、純粋なKotlinだけで書いた最小限のループを見てみましょう。各ステップで対象に向かう復元力を適用し、速度を減衰させます。Composeがフレームごとに行っているのも同じ考え方です。

fun main() {
    var pos = 0f
    var vel = 0f
    val target = 200f
    val stiffness = 0.2f
    val damping = 0.75f
    repeat(8) { step ->
        vel = (vel + (target - pos) * stiffness) * damping
        pos += vel
        println("step $step pos=${"%.1f".format(pos)}")
    }
}

タップ、押下、複数のジェスチャー

ジェスチャーはドラッグだけではありません。pointerInputには、タップ、ダブルタップ、長押しに使うdetectTapGesturesや、ピンチズームと回転に使うdetectTransformGesturesも用意されています。

ここでは、長押しをきっかけにスプリングで拡大・縮小し、ジェスチャーの検出とAnimatableを組み合わせます。

@Composable
fun LongPressScale() {
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .graphicsLayer { scaleX = scale.value; scaleY = scale.value }
            .pointerInput(Unit) {
                detectTapGestures(
                    onLongPress = {
                        scope.launch { scale.animateTo(1.4f, spring()) }
                    },
                    onPress = {
                        tryAwaitRelease()
                        scope.launch { scale.animateTo(1f, spring()) }
                    }
                )
            }
    )
}

確認テスト

ドラッグできるカードを作成しており、指を離したときに元の位置へ跳ね返るように戻したいとします。Composeで慣用的な組み合わせはどれでしょうか。

まとめ:ジェスチャーとスプリング

これで、自然でインタラクティブな動きを作成できるようになりました。

  • Animatableを使うと、animateToやsnapToによって、コルーチン駆動の命令型制御ができます。
  • springは、dampingRatio(跳ね返り)とstiffness(速度)で調整します。
  • pointerInputとdetectDragGesturesでタッチを動きに変換し、animateDecayでフリングを追加できます。
  • anchoredDraggableを使うと、スワイプ操作で固定位置に移動できます。
  • rememberInfiniteTransitionで、点滅やシマーなどのループする効果を実行できます。

これでJetpack Composeのアニメーションの学習は完了です。値、表示・非表示、コンテンツ、ジェスチャーを自信を持ってアニメーション化できるようになりました。

よくある質問

「ジェスチャーとスプリングアニメーション」レッスンは無料ですか?

はい。「ジェスチャーとスプリングアニメーション」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「ジェスチャーとスプリングアニメーション」で何を学びますか?

物理法則に基づく自然な動きを実現します。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「ジェスチャーとスプリングアニメーション」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. アニメーションが重要な理由
  2. animate*AsStateの基本
  3. AnimatedVisibilityとトランジション
  4. ジェスチャーとスプリングアニメーション
← Android Academyに戻る