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

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

アニメーションの仕様

サイズと位置の移動に使用するアニメーション仕様を変更するには、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

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

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

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

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

Modifier.skipToLookahead() - アニメーションの開始時に「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 つのアイテムの拡大に応じて下に移動していることに注目してください)