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

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

アニメーションの仕様

サイズと位置の移動に使用されるアニメーション仕様を変更するには、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 つのアイテムが増えるのに反応して、リスト内の他のアイテムがどのように下降していくかに注目してください)