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

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

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

Compose には、コンテンツの表示、非表示、レイアウトの変更をアニメーション化するコンポーザブルがいくつか用意されています。

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

緑色のコンポーザブルが自身を表示したり非表示にしたりする
図 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
    // ...
}

デフォルトでは、コンテンツの表示にはフェードインと拡大が使われ、非表示にはフェードアウトと縮小が使われます。このトランジションをカスタマイズするには、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
fadeIn
UI 要素が徐々にフェードインして表示されます。
fadeOut
UI 要素が徐々にフェードアウトして見えなくなる。
slideIn
UI 要素が画面外からスライドして表示される。
slideOut
UI 要素が画面外にスライドして消える。
slideInHorizontally
UI 要素が水平方向にスライドして表示されます。
slideOutHorizontally
UI 要素が水平方向にスライドしてビューから外れます。
slideInVertically
UI 要素が垂直方向にスライドして表示されます。
slideOutVertically
UI 要素が垂直方向にスライドしてビューから外れます。
scaleIn
UI 要素が拡大されて表示されます。
scaleOut
UI 要素が縮小して画面外に移動する。
expandIn
UI 要素が中央の点から拡大して表示されます。
shrinkOut
UI 要素が中央の点に向かって縮小して非表示になる。
expandHorizontally
UI 要素が水平方向に拡大して表示されます。
shrinkHorizontally
UI 要素が水平方向に縮小して非表示になる。
expandVertically
UI 要素が垂直方向に展開されて表示されます。
shrinkVertically
UI 要素が垂直方向に縮小してビューから消えます。

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 を使用してアニメーションを管理する方法については、transition で複数のプロパティを同時にアニメーション化するをご覧ください。

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

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")
    }
}

デフォルトでは、初期コンテンツがフェードアウトしてから、ターゲット コンテンツがフェードインします(この動作はフェードスルーと呼ばれています)。このアニメーションの動作をカスタマイズするには、transitionSpec パラメータに ContentTransform オブジェクトを指定します。ContentTransform のインスタンスを作成するには、with 中置関数を使用して EnterTransition オブジェクトと ExitTransition オブジェクトを組み合わせます。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 をご覧ください。

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")
    }
}

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

Compose には、コンポーザブルで特定の変更を直接アニメーション化するための修飾子が用意されています。

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

サイズ変更をスムーズにアニメーション化する緑色のコンポーザブル。
図 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 レイアウト アイテムのアニメーションに関するドキュメントをご覧ください。