Jetpack Compose は、アプリの UI でさまざまなアニメーションを簡単に実装できる強力で拡張可能な API を提供します。このドキュメントでは、これらの API の使用方法と、アニメーションのシナリオに応じてどの API を使用すべきかについて説明します。
概要
最新のモバイルアプリでは、スムーズでわかりやすいユーザー エクスペリエンスを実現するためにアニメーションが不可欠です。多くの Jetpack Compose Animation API は、レイアウトやその他の UI 要素と同様にコンポーズ可能な関数として使用できます。また、これらの API は、Kotlin コルーチンの suspend 関数で作成された低レベル API によってサポートされています。このガイドでは、最初に多くの実践的なシナリオで役立つ高レベル API について説明し、その後でより細かい制御とカスタマイズが可能な低レベル API について解説します。
下の図は、アニメーションの実装に使用する API を決定する際に役立ちます。
- レイアウト内のコンテンツの変更をアニメーション化する場合:
- 表示と非表示をアニメーション化する場合:
AnimatedVisibility
を使用します。
- 状態に基づいてコンテンツを入れ替える場合:
- コンテンツをクロスフェードする場合:
Crossfade
を使用します。
- それ以外の場合は、
AnimatedContent
を使用します。
- コンテンツをクロスフェードする場合:
- それ以外の場合は、
Modifier.animateContentSize
を使用します。
- 表示と非表示をアニメーション化する場合:
- アニメーションが状態ベースである場合:
- コンポジション中にアニメーションが発生する場合:
- アニメーションが無限である場合:
rememberInfiniteTransition
を使用します。
- 複数の値を同時にアニメーション化する場合:
updateTransition
を使用します。
- それ以外の場合は、
animate*AsState
を使用します。
- アニメーションが無限である場合:
- コンポジション中にアニメーションが発生する場合:
- アニメーション時間をより細かく制御する場合:
Animation
(TargetBasedAnimation
やDecayAnimation
など)を使用します。
- アニメーションが唯一の信頼できる情報源である場合:
Animatable
を使用します。
- それ以外の場合は、
AnimationState
またはanimate
を使用します。
高レベル アニメーション API
Compose には、多くのアプリで使用される一般的なアニメーション パターン用の高レベル アニメーション API が用意されています。これらの API は、マテリアル デザイン モーションのベスト プラクティスに合わせてカスタマイズされています。
AnimatedVisibility
AnimatedVisibility
コンポーザブルは、コンテンツの表示と非表示をアニメーション化します。
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
デフォルトでは、コンテンツの表示にはフェードインと拡大が使われ、非表示にはフェードアウトと縮小が使われます。この遷移は、EnterTransition
と ExitTransition
を指定することでカスタマイズできます。
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
上記の例に示すように、複数の EnterTransition
オブジェクトまたは ExitTransition
オブジェクトを +
演算子で結合できます。各オブジェクトは、動作をカスタマイズするためのオプション パラメータを受け入れます。詳細については、それぞれのリファレンスをご覧ください。
EnterTransition
と ExitTransition
の例
AnimatedVisibility
には、MutableTransitionState
を取る派生形も用意されています。これにより、AnimatedVisibility
がコンポジション ツリーに追加された直後にアニメーションをトリガーできます。アニメーションの状態を監視する場合にも便利です。
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
子の開始と終了をアニメーション化する
AnimatedVisibility
内のコンテンツ(直接または間接の子)は、animateEnterExit
修飾子を使用して、それぞれに異なるアニメーションの動作を指定できます。子それぞれの視覚効果は、AnimatedVisibility
コンポーザブルで指定されたアニメーションと、子自身の開始アニメーションと終了アニメーションの組み合わせです。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
場合によっては、AnimatedVisibility
にアニメーションをまったく適用しないようにして、子の animateEnterExit
でそれぞれ独自のアニメーションを持つようにしたい場合があります。これを実現するには、AnimatedVisibility
コンポーザブルで EnterTransition.None
と ExitTransition.None
を指定します。
カスタム アニメーションを追加する
組み込みの開始アニメーションと終了アニメーション以外のカスタム アニメーション効果を追加する場合は、AnimatedVisibility
のコンテンツ ラムダ内の transition
プロパティを使用して、元となる Transition
インスタンスにアクセスします。Transition インスタンスに追加されたアニメーションの状態は、AnimatedVisibility
の開始アニメーションと終了アニメーションと同時に実行されます。AnimatedVisibility
は、Transition
内のすべてのアニメーションが終了するまで待ってから、コンテンツを削除します。(animate*AsState
を使用するなどして)Transition
とは独立して作成された終了アニメーションについては、AnimatedVisibility
では考慮できないため、終了前にコンテンツ コンポーザブルを削除する場合があります。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
Transition
の詳細については、updateTransition をご覧ください。
animate*AsState
animate*AsState
関数は、Compose で単一の値をアニメーション化するための最もシンプルなアニメーション API です。終了値(またはターゲット値)を指定するだけで、API は現在の値から指定された値までのアニメーションを開始します。
この API を使用してアルファ値をアニメーション化する例を以下に示します。ターゲット値を animateFloatAsState
でラップするだけで、アルファ値は、指定した値(この場合は 1f
または 0.5f
)の間のアニメーション値になります。
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
アニメーション クラスのインスタンスを作成したり、割り込みを処理したりする必要はありません。内部では、アニメーション オブジェクト(Animatable
インスタンス)がコールサイトで作成および記憶され、最初のターゲット値が初期値として指定されます。その後、このコンポーザブルに別のターゲット値を指定すると、その値までのアニメーションが自動的に開始されます。実行中のアニメーションがある場合、アニメーションは現在の値から同じ速度で開始し、ターゲット値までアニメーション化します。アニメーション中に、このコンポーザブルは再コンポーズされ、更新されたアニメーション値をフレームごとに返します。
Compose には、Float
、Color
、Dp
、Size
、Offset
、Rect
、Int
、IntOffset
、IntSize
用の animate*AsState
関数が、すぐに使える状態で用意されています。汎用型を受け入れる animateValueAsState
に TwoWayConverter
を指定することにより、他のデータ型のサポートを簡単に追加できます。
AnimationSpec
を指定することで、アニメーションの仕様をカスタマイズできます。詳細については、AnimationSpec をご覧ください。
AnimatedContent(試験運用版)
AnimatedContent
コンポーザブルは、ターゲット状態に基づいてコンテンツが変化するのに応じて、コンテンツをアニメーション化します。
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
必ずラムダ パラメータを使用し、それをコンテンツに反映する必要があります。API はこの値をキーとして使用して、現在表示されているコンテンツを識別します。
デフォルトでは、初期コンテンツがフェードアウトしてから、ターゲット コンテンツがフェードインします(この動作はフェードスルーと呼ばれています)。このアニメーションの動作をカスタマイズするには、transitionSpec
パラメータに ContentTransform
オブジェクトを指定します。ContentTransform
を作成するには、with
中置関数を使用して EnterTransition
と ExitTransition
を組み合わせます。SizeTransform
を ContentTransform
に適用するには、using
中置関数で接続します。
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
は、ターゲット コンテンツの表示方法を定義し、ExitTransition
は、初期コンテンツの消失方法を定義します。AnimatedVisibility
で使用可能なすべての EnterTransition
関数と ExitTransition
関数に加えて、AnimatedContent
には slideIntoContainer
と slideOutOfContainer
が用意されています。これらは slideInHorizontally/Vertically
と slideOutHorizontally/Vertically
に代わる便利な方法であり、AnimatedContent
コンテンツの初期コンテンツとターゲット コンテンツのサイズに基づいてスライド距離を計算します。
SizeTransform
は、初期コンテンツとターゲット コンテンツの間のサイズのアニメーション方法を定義します。アニメーションの作成時に、初期サイズとターゲット サイズの両方にアクセスできます。SizeTransform
は、アニメーション中にコンテンツをコンポーネントのサイズに切り取るかどうかも制御します。
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
子の開始 / 終了をアニメーション化する
AnimatedVisibility
と同様に、animateEnterExit
修飾子は AnimatedContent
のコンテンツ ラムダ内で使用できます。これを使用して、EnterAnimation
と ExitAnimation
を直接または間接の子のそれぞれに個別に適用します。
カスタム アニメーションを追加する
AnimatedVisibility
と同様に、transition
フィールドは AnimatedContent
のコンテンツ ラムダ内で使用できます。これを使用して、AnimatedContent
遷移と同時に実行されるカスタム アニメーション効果を作成します。詳細については、updateTransition をご覧ください。
animateContentSize
animateContentSize
修飾子は、サイズ変更をアニメーション化します。
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
は、クロスフェード アニメーションで 2 つのレイアウト間をアニメーション化します。current
パラメータに渡される値を切り替えると、コンテンツがクロスフェード アニメーションで切り替わります。
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
Transition
は、1 つ以上のアニメーションを子として管理し、複数の状態間で同時に実行します。
この状態はどのデータ型でもかまいません。多くの場合、次の例のようにカスタムの enum
型を使用することで型の安全性を確保できます。
enum class BoxState {
Collapsed,
Expanded
}
updateTransition
は Transition
のインスタンスを作成して記憶し、その状態を更新します。
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)
次に、animate*
拡張関数のいずれかを使用して、この遷移で子アニメーションを定義できます。各状態のターゲット値を指定します。これらの animate*
関数は、遷移状態が updateTransition
で更新されると、アニメーション中にフレームごとに更新されるアニメーション値を返します。
val rect by transition.animateRect { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 1.dp
BoxState.Expanded -> 0.dp
}
}
必要に応じて、transitionSpec
パラメータを渡して、遷移状態の変更の各組み合わせごとに異なる AnimationSpec
を指定できます。詳細については、AnimationSpec をご覧ください。
val color by transition.animateColor(
transitionSpec = {
when {
BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
spring(stiffness = 50f)
else ->
tween(durationMillis = 500)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.background
}
}
遷移がターゲット状態に到達すると、Transition.currentState
は Transition.targetState
と同じになります。これは、遷移が完了したかどうかのシグナルとして使用できます。
最初のターゲット状態とは異なる初期状態が必要になる場合があります。これを実現するには、updateTransition
を MutableTransitionState
とともに使用します。たとえば、コードがコンポジションに入り次第、アニメーションを開始できます。
// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...
複数のコンポーズ可能な関数が関係する複雑な遷移の場合は、createChildTransition
を使用して子遷移を作成できます。この手法を使うと、複雑なコンポーザブルで複数のサブコンポーネントに関心を分離できます。親遷移は、子遷移のすべてのアニメーション値を認識します。
enum class DialerState { DialerMinimized, NumberPad }
@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun Dialer(dialerState: DialerState) {
val transition = updateTransition(dialerState)
Box {
// Creates separate child transitions of Boolean type for NumberPad
// and DialerButton for any content animation between visible and
// not visible
NumberPad(
transition.createChildTransition {
it == DialerState.NumberPad
}
)
DialerButton(
transition.createChildTransition {
it == DialerState.DialerMinimized
}
)
}
}
AnimatedVisibility と AnimatedContent で遷移を使う
AnimatedVisibility
と AnimatedContent
は、Transition
の拡張関数として使用できます。Transition.AnimatedVisibility
と Transition.AnimatedContent
の targetState
は、Transition
から派生し、Transition
の targetState
が変更されたときに、必要に応じて enter / exit 遷移をトリガーします。これらの拡張関数により、AnimatedVisibility
/ AnimatedContent
の内部に入っていたであろう enter / exit / sizeTransform アニメーションが、すべて Transition
に取り込まれます。これらの拡張関数を使用すると、AnimatedVisibility
/ AnimatedContent
の状態変化を外部から監視できます。このバージョンの AnimatedVisibility
は、ブール値の visible
パラメータではなく、親遷移のターゲット状態をブール値に変換するラムダを取ります。
詳細については、AnimatedVisibility と AnimatedContent をご覧ください。
var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
onClick = { selected = !selected },
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = elevation
) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(text = "Hello, world!")
// AnimatedVisibility as a part of the transition.
transition.AnimatedVisibility(
visible = { targetSelected -> targetSelected },
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(text = "It is fine today.")
}
// AnimatedContent as a part of the transition.
transition.AnimatedContent { targetState ->
if (targetState) {
Text(text = "Selected")
} else {
Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
}
}
}
}
遷移をカプセル化して再利用可能にする
シンプルなユースケースでは、UI と同じコンポーザブルで遷移アニメーションを定義することは、有効な選択肢です。ただし、多数のアニメーション値を持つ複雑なコンポーネントを作成している場合は、アニメーション実装をコンポーザブル UI から分離することをおすすめします。
そのためには、すべてのアニメーション値を保持するクラスと、そのクラスのインスタンスを返す「update」関数を作成します。遷移の実装は、新しい別の関数に抽出できます。このパターンは、アニメーション ロジックを一元管理する必要がある場合や、複雑なアニメーションを再利用可能にする必要がある場合に役立ちます。
enum class BoxState { Collapsed, Expanded }
@Composable
fun AnimatingBox(boxState: BoxState) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
rememberInfiniteTransition
InfiniteTransition
は 1 つ以上の子アニメーション(Transition
など)を保持します。ただし、それらのアニメーションはコンポジションに入るとすぐに開始し、削除されない限り停止しません。InfiniteTransition
のインスタンスは rememberInfiniteTransition
で作成できます。子アニメーションは、animateColor
、animatedFloat
、または animatedValue
を使用して追加できます。また、アニメーションの仕様を指定するには、infiniteRepeatable を指定する必要があります。
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(Modifier.fillMaxSize().background(color))
低レベル アニメーション API
前のセクションで説明したすべての高レベル アニメーション API は、低レベル アニメーション API の基盤の上に構築されています。
animate*AsState
関数は最もシンプルな API で、即時の値の変化をアニメーション値としてレンダリングします。これらの関数は、単一の値をアニメーション化するコルーチン ベースの API である Animatable
によってサポートされています。updateTransition
は、複数の値のアニメーション化を管理し、状態変化に基づいて実行する遷移オブジェクトを作成します。rememberInfiniteTransition
はこれと似ていますが、無限に実行される複数のアニメーションを管理できる無限遷移を作成します。Animatable
を除き、これらの API はすべてコンポーザブルであるため、これらのアニメーションはコンポジションの外部で作成できます。
これらの API はすべて、さらに基本的な Animation
API に基づいています。ほとんどのアプリは Animation
と直接やり取りはしませんが、Animation
のカスタマイズ機能の中には、高レベル API を通じて利用できるものもあります。AnimationVector
と AnimationSpec
の詳細については、アニメーションをカスタマイズするをご覧ください。
Animatable
Animatable
は、animateTo
を介して変化する値をアニメーション化できる値ホルダーです。この API は、animate*AsState
の実装をバックアップします。一貫した継続性と相互排他性が保証されるため、値の変化は常に連続的となり、進行中のアニメーションはキャンセルされます。
Animatable
の多くの機能(animateTo
を含む)は suspend 関数として提供されるため、適切なコルーチン スコープでラップする必要があります。たとえば、LaunchedEffect
コンポーザブルを使用して、指定された Key-Value の持続時間だけスコープを作成できます。
// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))
上記の例では、Animatable
のインスタンスを初期値 Color.Gray
で作成および記憶しています。ブール値フラグ ok
の値に応じて、Color.Green
または Color.Red
へのアニメーションが行われます。その後ブール値を変更すると、他の色へのアニメーションが開始されます。値が変更されたときに進行中のアニメーションが存在した場合、アニメーションはキャンセルされ、新しいアニメーションが、現在のスナップショットの値から、現在の速度で開始されます。
これは、前のセクションで説明した animate*AsState
API をバックアップするアニメーションの実装です。animate*AsState
と比較した場合、Animatable
を直接使用することにより、いくつかの点でより細かい制御が可能になります。まず、Animatable
には最初のターゲット値とは異なる初期値を設定できます。たとえば、上のサンプルコードでは、最初にグレーのボックスが表示された後、すぐに緑色または赤色へのアニメーションが開始されます。次に、Animatable
は、コンテンツ値に対する他のオペレーション(snapTo
と animateDecay
)を提供します。snapTo
は、現在の値を直ちにターゲット値に設定します。これは、アニメーション自体が唯一の信頼できる情報源ではなく、タップイベントなどの他の状態と同期する必要がある場合に便利です。animateDecay
は、指定された速度より遅くなるアニメーションを開始します。これは、フリング動作を実装する際に役立ちます。詳細については、操作とアニメーションをご覧ください。
Animatable
ですぐに使用できるのは Float
と Color
のみですが、TwoWayConverter
を指定することで、すべてのデータ型を使用できます。詳細については、AnimationVector をご覧ください。
AnimationSpec
を指定することで、アニメーションの仕様をカスタマイズできます。詳細については、AnimationSpec をご覧ください。
Animation
Animation
は、利用可能な最も低いレベルのアニメーション API です。これまでに見てきたアニメーションの多くは、Animation の上に構築されています。Animation
のサブタイプには、TargetBasedAnimation
と DecayAnimation
の 2 つがあります。
Animation
は、アニメーションの時間を手動で制御する場合にのみ使用します。Animation
はステートレスであり、ライフサイクルのコンセプトはありません。上位レベルの API で使用されるアニメーション計算エンジンとして機能します。
TargetBasedAnimation
他の API はほとんどのユースケースに対応していますが、TargetBasedAnimation
を直接使用すると、アニメーションの再生時間を自分で制御できます。以下の例では、TargetAnimation
の再生時間は、withFrameNanos
で提供されるフレーム時間に基づいて手動で制御されます。
val anim = remember {
TargetBasedAnimation(
animationSpec = tween(200),
typeConverter = Float.VectorConverter,
initialValue = 200f,
targetValue = 1000f
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(anim) {
val startTime = withFrameNanos { it }
do {
playTime = withFrameNanos { it } - startTime
val animationValue = anim.getValueFromNanos(playTime)
} while (someCustomCondition())
}
DecayAnimation
TargetBasedAnimation
と異なり、DecayAnimation
では targetValue
を指定する必要はありません。代わりに、initialVelocity
と initialValue
、および指定の DecayAnimationSpec
によって設定された開始条件に基づいて targetValue
を計算します。
DecayAnimatioin は多くの場合、フリング操作の後に使用され、要素の速度を遅くして停止させます。アニメーションの速度は、initialVelocityVector
で設定された値から始まり、時間の経過とともに遅くなります。
アニメーションをカスタマイズする
アニメーション API の多くは、動作をカスタマイズするためのパラメータを受け入れます。
AnimationSpec
ほとんどのアニメーション API では、オプションの AnimationSpec
パラメータでアニメーションの仕様をカスタマイズできます。
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
各種アニメーションの作成に対応した、さまざまなタイプの AnimationSpec
があります。
spring
spring
は、開始値と終了値の間で物理学ベースのアニメーションを作成します。dampingRatio
と stiffness
の 2 つのパラメータを受け取ります。
dampingRatio
は、ばねの弾性を定義します。デフォルト値は Spring.DampingRatioNoBouncy
です。
stiffness
は、終了値までのばねの移動速度を定義します。デフォルト値は Spring.StiffnessMedium
です。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
継続時間ベースの AnimationSpec
型と比べて、spring
は中断をより適切に処理できます。これは、spring の場合、アニメーション中にターゲット値が変更されたときに速度の継続性が保証されるためです。spring
は、animate*AsState
や updateTransition
など、多くのアニメーション API によってデフォルトの AnimationSpec として使用されます。
tween
tween
は、イージング カーブを使用して、指定された durationMillis
の開始値と終了値の間をアニメーション化します。詳細については、イージングをご覧ください。delayMillis
を指定して、アニメーションの開始を延期することもできます。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
は、アニメーションの持続時間内の異なるタイムスタンプで指定されたスナップショット値に基づいてアニメーション化します。アニメーション値は、常に 2 つのキーフレーム値の間で補間されます。これらのキーフレームごとに、イージングを指定して補間曲線を設定できます。
0 ms と持続時間の値の指定はオプションです。これらの値を指定しない場合は、デフォルトでアニメーションの開始値と終了値が設定されます。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
は、指定された繰り返し回数に達するまで、持続時間ベースのアニメーション(tween
や keyframes
など)を繰り返します。repeatMode
パラメータを渡すと、アニメーションを繰り返す際に、最初から(RepeatMode.Restart
)開始するか、最後から(RepeatMode.Reverse
)開始するかを指定できます。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
は repeatable
に似ていますが、繰り返し回数が無限である点が異なります。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
ComposeTestRule
を使用したテストでは、infiniteRepeatable
を使用したアニメーションは実行されません。各アニメーション値の初期値を使用してコンポーネントがレンダリングされます。
snap
snap
は、値をすぐに終了値に切り替える特別な AnimationSpec
です。delayMillis
を指定して、アニメーションの開始を遅らせることができます。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
持続時間ベースの AnimationSpec
オペレーション(tween
、keyframes
など)では、Easing
を使用してアニメーションの割合を調整します。これにより、アニメーション化する値の速度を変えながら動かすことができます。割合は、0(開始)から 1.0(終了)までの値で、アニメーションの現在の位置を示します。
Easing とは、実際には 0~1.0 の小数値を受け取り、浮動小数点数を返す関数です。戻り値は、オーバーシュートまたはアンダーシュートを表すために境界外となる場合もあります。カスタム Easing は、次のようなコードで作成できます。
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose には、ほとんどのユースケースに対応できる組み込みの Easing
関数がいくつか用意されています。シナリオに応じた Easing の使用については、速度 - マテリアル デザインをご覧ください。
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- 詳細を見る
AnimationVector
ほとんどの Compose アニメーション API は、すぐに使えるアニメーション値として Float
、Color
、Dp
などの基本的なデータ型をサポートしています。ただし、カスタムのデータ型など、他のデータ型のアニメーション化が必要な場合もあります。アニメーション中、アニメーション化する値は AnimationVector
として表されます。値は、対応する TwoWayConverter
によって AnimationVector
に変換されます(逆も同様です)。これにより、コア アニメーション システムが値を均一に処理できるようになります。たとえば、Int
は、単一の浮動小数点値を保持する AnimationVector1D
として表されます。Int
の TwoWayConverter
は次のようになります。
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
は基本的に 4 つの値(赤、緑、青、アルファ)のセットであるため、Color
は 4 つの浮動小数点値を保持する AnimationVector4D
に変換されます。このように、アニメーションで使用されるデータ型はすべて、次元数に応じて AnimationVector1D
、AnimationVector2D
、AnimationVector3D
、AnimationVector4D
のいずれかに変換されます。これにより、オブジェクトの異なるコンポーネントを別々にアニメーション化し、それぞれの速度をトラッキングすることができます。基本的なデータ型の組み込みコンバータには、Color.VectorConverter
、Dp.VectorConverter
などを使用してアクセスできます。
新しいデータ型のサポートをアニメーション化する値として追加する場合は、独自の TwoWayConverter
を作成して API に指定できます。たとえば、次のように animateValueAsState
を使用してカスタムのデータ型をアニメーション化できます。
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
アニメーション化ベクター リソース(試験運用版)
AnimatedVectorDrawable
リソースを使用するには、animatedVectorResource
を使用してドローアブル ファイルを読み込み、boolean
型で渡してドローアブルの開始状態と終了状態を切り替えます。
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
ドローアブル ファイルの形式について詳しくは、ドローアブル グラフィックをアニメーションにするをご覧ください。
リストアイテムのアニメーション
Lazy リストまたは Lazy グリッド内でアイテムの並べ替えをアニメーション化する場合は、Lazy レイアウト アイテムのアニメーションに関するドキュメントをご覧ください。
操作とアニメーション(上級者向け)
タップイベントやアニメーションを使用する場合は、アニメーションだけを使用する場合と比べて、考慮すべき点がいくつかあります。まず、ユーザー操作を最優先する必要があるため、場合によっては、タップイベントの開始時に実行中のアニメーションを中断する必要があります。
以下の例では、Animatable
を使用して円コンポーネントのオフセット位置を表しています。タップイベントは、pointerInput
修飾子で処理されます。新しいタップイベントを検出すると、animateTo
を呼び出して、タップ位置までのオフセット値をアニメーション化します。アニメーション中にタップイベントが発生する場合もあります。その場合、animateTo
は実行中のアニメーションを中断し、中断されたアニメーションの速度を維持しながら、新しいターゲット位置までのアニメーションを開始します。
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
もう 1 つのよくあるパターンでは、アニメーション値を、ドラッグなどのタップイベントから取得した値と同期させる必要があります。以下の例では、SwipeToDismiss
コンポーザブルを使用するのではなく、Modifier
として「スワイプで閉じる」を実装しています。要素の水平オフセットは、Animatable
として表されます。この API には操作アニメーションで役立つ特性があります。この値は、タップイベントとアニメーションによって変更できます。タップイベントを受け取ると、進行中のすべてのアニメーションがインターセプトされるように、stop
メソッドにより Animatable
を停止します。
ドラッグ イベントでは、snapTo
を使用して、タップイベントから計算された値で Animatable
の値を更新します。フリングについては、Compose に用意されている VelocityTracker
を使うと、ドラッグ イベントを記録して速度を計算できます。速度は animateDecay
に直接送られ、フリング アニメーションに使用されます。オフセット値を元の位置に戻すには、animateTo
メソッドでターゲット オフセット値を 0f
に指定します。
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
テスト
Compose の ComposeTestRule
を使用すると、決定論的な方法で、テストクロックを完全に制御しながらアニメーションのテストを作成できます。これにより、アニメーションの中間値を検証できます。また、テストでは、実際の持続時間よりも短い時間でアニメーションを実行できます。
ComposeTestRule
は、テストクロックを mainClock
として公開します。テストコードのクロックを制御するには、autoAdvance
プロパティを false に設定します。テストするアニメーションを開始したら、クロックを advanceTimeBy
で進めることができます。
advanceTimeBy
は、指定された持続時間分だけ正確にクロックを進めるのではなく、フレーム間隔の倍数に最も近い持続時間に切り上げることに注意してください。
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
ツールのサポート
Android Studio は、アニメーション プレビューでの updateTransition
と animatedVisibility
の検査をサポートしています。次のことが可能です。
- 遷移をフレームごとにプレビューする
- 遷移内のアニメーションすべての値を検査する
- 初期状態とターゲット状態の間の遷移をプレビューする
- 複数のアニメーションを一度に検査して調整する
アニメーション プレビューを開始すると、プレビュー内の任意の遷移を実行できる [Animations] ペインが表示されます。遷移とその個々のアニメーション値には、デフォルト名のラベルが付けられます。ラベルをカスタマイズするには、updateTransition
関数と AnimatedVisibility
関数で label
パラメータを指定します。詳しくは、アニメーション プレビューをご覧ください。
詳細
Jetpack Compose でのアニメーションについて詳しくは、次の参考情報をご覧ください。