共有要素遷移をカスタマイズする

共有要素の遷移アニメーションの実行方法をカスタマイズするには、共有要素の遷移方法を変更するために使用できるパラメータがいくつかあります。

アニメーションの仕様

サイズと位置の移動に使用されるアニメーションの仕様を変更するには、Modifier.sharedElement() で別の boundsTransform パラメータを指定します。 これにより、初期の Rect の位置とターゲットの Rect の位置が提供されます。

たとえば、前の例のテキストを弧を描くように移動させるには、boundsTransform パラメータを keyframes 仕様を使用するように指定します。

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

任意の AnimationSpec を使用できます。この例では、keyframes 仕様を使用しています。

図 1. さまざまな boundsTransform パラメータを示す例

サイズ変更モード

2 つの共有境界の間でアニメーション化する場合は、resizeMode パラメータを RemeasureToBounds または ScaleToBounds に設定できます。このパラメータは、共有要素が 2 つの状態の間でどのように遷移するかを決定します。ScaleToBounds は、ルックアヘッド(またはターゲット)の制約を使用して、最初の子レイアウトを測定します。次に、子の安定したレイアウトが、共有境界に収まるようにスケーリングされます。 ScaleToBounds は、状態間の「グラフィカル スケール」と考えることができます。

一方、RemeasureToBounds は、ターゲット サイズに基づいて、アニメーション化された固定制約を使用して sharedBounds の子レイアウトを再測定して再レイアウトします。再測定は境界サイズの変更によってトリガーされます。これは、フレームごとに行われる可能性があります。

Text コンポーザブルの場合は、テキストの再レイアウトや別の行へのリフローを回避できるため、ScaleToBounds をおすすめします。RemeasureToBounds は、アスペクト比が異なる境界の場合や、2 つの共有要素間でスムーズな連続性を実現したい場合におすすめします。

2 つのサイズ変更モードの違いは、次の例で確認できます。

ScaleToBounds

RemeasureToBounds

共有要素を動的に有効または無効にする

デフォルトでは、sharedElement()sharedBounds() は、ターゲット状態に一致するキーが見つかったときにレイアウトの変更をアニメーション化するように構成されています。ただし、ナビゲーションの方向や現在の UI 状態など、特定の条件に基づいて、このアニメーションを動的に無効にしたい場合があります。

共有要素の遷移が発生するかどうかを制御するには、rememberSharedContentState() に渡される SharedContentConfig をカスタマイズします。isEnabled プロパティは、共有要素がアクティブかどうかを決定します。

次の例は、特定の画面間(A から B のみなど)を移動する場合にのみ共有遷移を有効にし、他の画面では無効にする構成を定義する方法を示しています。

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

デフォルトでは、アニメーションの進行中に共有要素が無効になっても、進行中のアニメーションが誤って削除されないように、現在進行中のアニメーションは完了します。アニメーションの進行中に要素を削除する必要がある場合は、SharedContentConfig インターフェースで shouldKeepEnabledForOngoingAnimation をオーバーライドして false を返すことができます。

最終レイアウトにスキップする

デフォルトでは、2 つのレイアウト間を遷移するときに、レイアウト サイズが開始状態と最終状態の間でアニメーション化されます。テキストなどのコンテンツをアニメーション化する場合、これは望ましくない動作になる可能性があります。

次の例は、説明テキスト「Lorem Ipsum」が 2 つの異なる方法で画面に表示される様子を示しています。最初の例では、コンテナのサイズが大きくなるにつれて、テキストがリフローされます。2 番目の例では、テキストは拡大してもリフローされません。Modifier.skipToLookaheadSize() を追加すると、拡大してもリフローされなくなります。

Modifier.skipToLookaheadSize() なし \- 「Lorem Ipsum」テキストがリフローされる

Modifier.skipToLookaheadSize() あり - 「Lorem Ipsum」テキストがアニメーションの開始時に最終状態を維持する

クリップとオーバーレイ

共有要素が異なるコンポーザブル間で共有されるようにするには、遷移がデスティネーションの一致する要素に開始されるときに、コンポーザブルのレンダリングがレイヤ オーバーレイに昇格 します。これにより、親の境界とそのレイヤ変換(アルファやスケールなど)からエスケープします。

共有されていない他の UI 要素の上にレンダリングされます。遷移が完了すると、要素はオーバーレイから独自の DrawScope にドロップされます。

共有要素をシェイプにクリップするには、標準の Modifier.clip() 関数を使用します。sharedElement() の後に配置します。

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

共有要素が親コンテナの外側にレンダリングされないようにするには、sharedElement()clipInOverlayDuringTransition を設定します。デフォルトでは、ネストされた共有境界の場合、clipInOverlayDuringTransition は親の sharedBounds() からクリップパスを使用します。

共有要素の遷移中に、ボトムバーやフローティング アクション ボタンなどの特定の UI 要素を常に最上部に表示するには、 Modifier.renderInSharedTransitionScopeOverlay()を使用します。デフォルトでは、この修飾子は、共有遷移がアクティブな間、コンテンツをオーバーレイに保持します。

たとえば、Jetsnack では、画面が表示されなくなるまで、BottomAppBar を共有要素の上に配置する必要があります。コンポーザブルに修飾子を追加すると、エレベーションが維持されます。

Modifier.renderInSharedTransitionScopeOverlay() なし

Modifier.renderInSharedTransitionScopeOverlay() あり

遷移前に、共有されていないコンポーザブルをアニメーションで消去し、他のコンポーザブルの上に残したい場合があります。このような場合は、renderInSharedTransitionScopeOverlay().animateEnterExit() を使用して、共有要素の遷移の実行時にコンポーザブルをアニメーションで消去します。

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

図 2. アニメーションの遷移に合わせてボトム アプリバーがスライドイン / スライドアウトする。

共有要素をオーバーレイにレンダリングしない場合は、sharedElement()renderInOverlayDuringTransition を false に設定します。

共有要素のサイズの変更を兄弟レイアウトに通知する

デフォルトでは、sharedBounds()sharedElement() は、レイアウトの遷移時に親コンテナにサイズの変更を通知しません。

遷移時にサイズ変更を親コンテナに伝播するには、placeholderSize パラメータを PlaceholderSize.AnimatedSize に変更します。これにより、アイテムが拡大または縮小します。レイアウト内の他のすべてのアイテムが変更に応答します。

PlaceholderSize.ContentSize(デフォルト)

PlaceholderSize.AnimatedSize

(1 つのアイテムが拡大したときに、リスト内の他のアイテムがどのように下に移動するかに注目してください)