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

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

アニメーション仕様

サイズと位置の移動に使用されるアニメーション仕様を変更するには、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 を使用することをおすすめします。アスペクト比が異なる境界で、2 つの共有要素をスムーズに連続させる場合は、RemeasureToBounds が推奨されます。

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

ScaleToBounds

RemeasureToBounds

最終的なレイアウトにスキップ

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

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

Modifier.skipToLookahead() なし - 「Lorem Ipsum」のテキストが再フローされていることに注目

Modifier.skipToLookahead() - 「Lorem Ipsum」というテキストは、アニメーションの開始時に最終状態を維持します。

クリップとオーバーレイ

Compose で共有要素を作成する際の重要なコンセプトは、異なるコンポーザブル間で共有するには、移行が開始されて宛先で一致すると、コンポーザブルのレンダリングがレイヤ オーバーレイにエレベートされることです。その結果、親の境界とそのレイヤ変換(アルファやスケールなど)から外れます。

他の共有されていない 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 つのアイテムが拡大すると、リスト内の他のアイテムが下に移動します)。