自訂共用元素轉換

如要自訂共用元素轉換動畫的執行方式,可以使用幾個參數變更共用元素的轉換方式。

動畫規格

如要變更用於大小和位置移動的動畫規格,您可以在 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 參數

調整大小模式

在兩個共用邊界之間製作動畫時,您可以將 resizeMode 參數設為 RemeasureToBoundsScaleToBounds。這個參數會決定共用元素在兩種狀態間的轉換方式。ScaleToBounds 會先使用預先查看 (或目標) 限制條件測量子項版面配置。接著,系統會縮放子項的穩定版面配置,以符合共用界線。ScaleToBounds 可以視為狀態之間的「圖形比例」。

相較之下,RemeasureToBounds 會根據目標大小,使用動畫固定限制條件重新測量及重新配置 sharedBounds 的子項版面配置。系統會因邊界大小變更而觸發重新測量,這可能發生在每個影格。

對於 Text 可組合項,建議使用 ScaleToBounds,因為這樣可避免重新配置版面,以及將文字重新換行。如果邊界長寬比不同,且您希望兩個共用元素之間有流暢的連續性,建議使用 RemeasureToBounds

如要瞭解這兩種調整大小模式的差異,請參閱下列範例:

ScaleToBounds

RemeasureToBounds

動態啟用及停用共用元素

根據預設,只要在目標狀態中找到相符的鍵,sharedElement()sharedBounds() 就會設定為將版面配置變更製作成動畫。不過,您可能會想根據特定條件 (例如導覽方向或目前的 UI 狀態),動態停用這項動畫。

如要控管是否發生共用元素轉場效果,可以自訂傳遞至 rememberSharedContentState()SharedContentConfigisEnabled 屬性會決定共用元素是否有效。

以下範例說明如何定義設定,只在特定畫面之間導覽時啟用共用轉換功能 (例如只從 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。

跳到最終版面配置

根據預設,在兩個版面配置之間轉換時,版面配置大小會在開始和最終狀態之間產生動畫效果。如果動畫內容是文字,這可能不是您想要的行為。

以下範例說明描述文字「Lorem Ipsum」進入畫面的兩種方式。在第一個範例中,文字會隨著容器大小增加而重排。在第二個範例中,文字不會隨著成長而重排。加入 Modifier.skipToLookaheadSize() 可防止重排。

Modifier.skipToLookaheadSize() - 請注意「Lorem Ipsum」文字會重排

Modifier.skipToLookaheadSize() - 請注意,「Lorem Ipsum」文字在動畫開始時會維持最終狀態

剪輯片段和疊加效果

如要讓共用元素在不同可組合項之間共用,可組合項的算繪作業會提升至圖層疊加,以便在轉換開始時,與目的地中的相符項目共用。這麼做會導致該子項逸出父項的界線,並略過父項的圖層轉換 (例如 alpha 和縮放)。

並會顯示在其他非共用 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

(請注意,清單中的其他項目會因其中一個項目變大而向下移動)