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

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

アニメーションの仕様

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