ジェスチャーとスプリングアニメーション
物理法則に基づく自然な動きを実現します。
「ジェスチャーとスプリングアニメーション」は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フィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- アニメーションが重要な理由
- animate*AsStateの基本
- AnimatedVisibilityとトランジション
- ジェスチャーとスプリングアニメーション