アニメーションの修飾子とコンポーザブル

Compose には、一般的なアニメーションのユースケースを処理するためのコンポーザブルと修飾子が組み込まれています。

組み込みのアニメーション コンポーザブル

AnimatedVisibility で表示と非表示をアニメーション化する

自身を表示および非表示にする緑色のコンポーザブル
図 1. 列内のアイテムの表示と非表示をアニメーション化する

AnimatedVisibility コンポーザブルは、コンテンツの表示と非表示をアニメーション化します。

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

デフォルトでは、コンテンツの表示にはフェードインと拡大が使われ、非表示にはフェードアウトと縮小が使われます。この遷移は、EnterTransitionExitTransition を指定することでカスタマイズできます。

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 オブジェクトを + 演算子で結合できます。各オブジェクトは、動作をカスタマイズするためのオプション パラメータを受け入れます。詳細については、それぞれのリファレンスをご覧ください。

EnterTransitionExitTransition の例

EnterTransition ExitTransition
fadeIn
フェードインのアニメーション
fadeOut
フェードアウトのアニメーション
slideIn
スライドインのアニメーション
slideOut
スライドアウトのアニメーション
slideInHorizontally
水平方向のスライドインのアニメーション
slideOutHorizontally
水平方向のスライドアウトのアニメーション
slideInVertically
垂直方向のスライドインのアニメーション
slideOutVertically
垂直方向のスライドアウトのアニメーション
scaleIn
スケールインのアニメーション
scaleOut
スケールアウトのアニメーション
expandIn
エクスパンド インのアニメーション
shrinkOut
シュリンク アウトのアニメーション
expandHorizontally
水平方向のエクスパンドのアニメーション
shrinkHorizontally
水平方向のシュリンクのアニメーション
expandVertically
垂直方向のエクスパンドのアニメーション
shrinkVertically
垂直方向のシュリンクのアニメーション

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 コンポーザブルで指定されたアニメーションと、子自身の開始アニメーションと終了アニメーションの組み合わせです。

var visible by remember { mutableStateOf(true) }

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.NoneExitTransition.None を指定します。

カスタム アニメーションを追加する

組み込みの開始アニメーションと終了アニメーション以外のカスタム アニメーション効果を追加する場合は、AnimatedVisibility のコンテンツ ラムダ内の transition プロパティを使用して、元となる Transition インスタンスにアクセスします。Transition インスタンスに追加されたアニメーションの状態は、AnimatedVisibility の開始アニメーションと終了アニメーションと同時に実行されます。AnimatedVisibility は、Transition 内のすべてのアニメーションが終了するまで待ってから、コンテンツを削除します。(animate*AsState を使用するなどして)Transition とは独立して作成された終了アニメーションについては、AnimatedVisibility では考慮できないため、終了前にコンテンツ コンポーザブルを削除する場合があります。

var visible by remember { mutableStateOf(true) }

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(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

Transition の詳細については、updateTransition をご覧ください。

AnimatedContent を使用してターゲットの状態に基づいてアニメーション化する

AnimatedContent コンポーザブルは、ターゲット状態に基づいてコンテンツが変化するのに応じて、コンテンツをアニメーション化します。

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

必ずラムダ パラメータを使用し、それをコンテンツに反映する必要があります。API はこの値をキーとして使用して、現在表示されているコンテンツを識別します。

デフォルトでは、初期コンテンツがフェードアウトしてから、ターゲット コンテンツがフェードインします(この動作はフェードスルーと呼ばれています)。このアニメーションの動作をカスタマイズするには、transitionSpec パラメータに ContentTransform オブジェクトを指定します。ContentTransform を作成するには、with 中置関数を使用して EnterTransitionExitTransition を組み合わせます。SizeTransformContentTransform に適用するには、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() togetherWith
                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() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition は、ターゲット コンテンツの表示方法を定義し、ExitTransition は、初期コンテンツの消失方法を定義します。AnimatedVisibility で使用可能なすべての EnterTransition 関数と ExitTransition 関数に加えて、AnimatedContent には slideIntoContainerslideOutOfContainer が用意されています。これらは slideInHorizontally/VerticallyslideOutHorizontally/Vertically に代わる便利な方法であり、AnimatedContent コンテンツの初期コンテンツとターゲット コンテンツのサイズに基づいてスライド距離を計算します。

SizeTransform は、初期コンテンツとターゲット コンテンツの間のサイズのアニメーション方法を定義します。アニメーションの作成時に、初期サイズとターゲット サイズの両方にアクセスできます。SizeTransform は、アニメーション中にコンテンツをコンポーネントのサイズに切り取るかどうかも制御します。

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                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
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

子の開始と終了の遷移をアニメーション化する

AnimatedVisibility と同様に、animateEnterExit 修飾子は AnimatedContent のコンテンツ ラムダ内で使用できます。これを使用して、EnterAnimationExitAnimation を直接または間接の子のそれぞれに個別に適用します。

カスタム アニメーションを追加する

AnimatedVisibility と同様に、transition フィールドは AnimatedContent のコンテンツ ラムダ内で使用できます。これを使用して、AnimatedContent 遷移と同時に実行されるカスタム アニメーション効果を作成します。詳細については、updateTransition をご覧ください。

Crossfade を使用して 2 つのレイアウト間をアニメーション化する

Crossfade は、クロスフェード アニメーションで 2 つのレイアウト間をアニメーション化します。current パラメータに渡される値を切り替えると、コンテンツがクロスフェード アニメーションで切り替わります。

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

組み込みのアニメーション修飾子

animateContentSize を使用してコンポーザブルのサイズ変更をアニメーション化する

サイズ変更をスムーズにアニメーション化する緑色のコンポーザブル。
図 2. コンポーザブルが小さいサイズと大きいサイズの間をスムーズにアニメーション化

animateContentSize 修飾子は、サイズ変更をアニメーション化します。

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

リストアイテムのアニメーション

Lazy リストまたは Lazy グリッド内でアイテムの並べ替えをアニメーション化する場合は、Lazy レイアウト アイテムのアニメーションに関するドキュメントをご覧ください。