使用 animate*AsState
為單一值設定動畫效果
animate*AsState
函式是 Compose 中最簡單的動畫 API,可用於為單一值建立動畫效果。您只需提供目標值 (或結束值),該 API 就會從現值開始播放動畫,直到達到指定值。
以下範例說明如何使用這個 API 為 alpha 建立動畫效果。只要將目標值納入 animateFloatAsState
中,alpha 值就會成為所提供的值 (在本例中為 1f
或 0.5f
) 之間的動畫值。
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( Modifier .fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
請注意,您不需要建立任何動畫類別的執行個體,也不必處理中斷情形。基本上,系統會在呼叫點上建立及記住動畫物件 (亦即 Animatable
執行個體),並將第一個目標值設為初始值。此後,只要您為這個可組合項提供不同的目標值,系統就會自動開始執行動畫,直到達到該值為止。如果已有正在執行的動畫,動畫會從現值 (和目前速率) 開始執行動畫,直到達到目標值。在動畫播放期間,這個可組合項會重新組合,並針對每個影格傳回更新的動畫值。
Compose 的特點在於可為 Float
、Color
、Dp
、Size
、Offset
、Rect
、Int
、IntOffset
和 IntSize
提供 animate*AsState
函式。只要為可接受一般類型的 animateValueAsState
提供 TwoWayConverter
,即可輕鬆將其他資料類型納入支援。
您可以提供 AnimationSpec
來自訂動畫規格。詳情請參閱「AnimationSpec」。
使用轉場同時為多個屬性設定動畫
Transition
可管理一個或多個動畫做為其子項,並在多個狀態之間同時執行這些動畫。
狀態可以是任何資料類型。在許多情況下,您可以使用自訂 enum
類型以確保類型安全,如以下範例所示:
enum class BoxState { Collapsed, Expanded }
updateTransition
會建立及記住 Transition
的例項,並更新其狀態。
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
接著,您可以使用其中一個 animate*
擴充函式來定義這個轉場效果中的子動畫,並為每個狀態指定目標值。這些 animate*
函式會傳回動畫值,當您在動畫播放期間使用 updateTransition
更新轉場狀態時,該值會隨著每個影格更新。
val rect by transition.animateRect(label = "rectangle") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "border width") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
您也可以選擇傳遞 transitionSpec
參數,為每個轉場狀態變更組合指定不同的 AnimationSpec
。詳情請參閱「AnimationSpec」。
val color by transition.animateColor( transitionSpec = { when { BoxState.Expanded isTransitioningTo BoxState.Collapsed -> spring(stiffness = 50f) else -> tween(durationMillis = 500) } }, label = "color" ) { state -> when (state) { BoxState.Collapsed -> MaterialTheme.colorScheme.primary BoxState.Expanded -> MaterialTheme.colorScheme.background } }
當轉場效果到達目標狀態時,Transition.currentState
將與 Transition.targetState
相同,可用來做為轉場效果是否完成的訊號。
我們有時會希望初始狀態與第一個目標狀態不同,這時可將 updateTransition
與 MutableTransitionState
搭配使用來達成。舉例來說,這麼做可讓我們在程式碼進入組合階段後立即開始執行動畫。
// Start in collapsed state and immediately animate to expanded var currentState = remember { MutableTransitionState(BoxState.Collapsed) } currentState.targetState = BoxState.Expanded val transition = rememberTransition(currentState, label = "box state") // ……
如果是涉及多個可組合函式的複雜轉場效果,可使用 createChildTransition
來建立下層轉場。這個技巧適合用於在複雜的可組合項中分隔多個重要子元件。上層轉場會得知下層轉場中的所有動畫值。
enum class DialerState { DialerMinimized, NumberPad } @Composable fun DialerButton(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun NumberPad(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun Dialer(dialerState: DialerState) { val transition = updateTransition(dialerState, label = "dialer state") Box { // Creates separate child transitions of Boolean type for NumberPad // and DialerButton for any content animation between visible and // not visible NumberPad( transition.createChildTransition { it == DialerState.NumberPad } ) DialerButton( transition.createChildTransition { it == DialerState.DialerMinimized } ) } }
搭配 AnimatedVisibility
和 AnimatedContent
使用轉換
AnimatedVisibility
和 AnimatedContent
可做為 Transition
的擴充函式。Transition.AnimatedVisibility
和 Transition.AnimatedContent
的 targetState
衍生自 Transition
,會在 Transition
的 targetState
變更時視需要觸發進入/結束轉換效果。這些擴充函式可允許原本位於 AnimatedVisibility
/AnimatedContent
內部的所有 enter/exit/sizeTransform 動畫提升到 Transition
中。只要有這些擴充函式,即可從外部觀察 AnimatedVisibility
/AnimatedContent
的狀態變化情形。這個版本的 AnimatedVisibility
會接受可將上層轉換的目標狀態轉換為布林值的 lambda,但不接受布林值的 visible
參數。
詳情請參閱「AnimatedVisibility」和「AnimatedContent」。
var selected by remember { mutableStateOf(false) } // Animates changes when `selected` is changed. val transition = updateTransition(selected, label = "selected state") val borderColor by transition.animateColor(label = "border color") { isSelected -> if (isSelected) Color.Magenta else Color.White } val elevation by transition.animateDp(label = "elevation") { isSelected -> if (isSelected) 10.dp else 2.dp } Surface( onClick = { selected = !selected }, shape = RoundedCornerShape(8.dp), border = BorderStroke(2.dp, borderColor), shadowElevation = elevation ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text(text = "Hello, world!") // AnimatedVisibility as a part of the transition. transition.AnimatedVisibility( visible = { targetSelected -> targetSelected }, enter = expandVertically(), exit = shrinkVertically() ) { Text(text = "It is fine today.") } // AnimatedContent as a part of the transition. transition.AnimatedContent { targetState -> if (targetState) { Text(text = "Selected") } else { Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone") } } } }
封裝轉場效果以便重複使用
如果是簡單用途,在與 UI 相同的可組合項中定義轉換動畫是相當有效的選項。不過,使用含有多個動畫值的複雜元件時,您可能需要將動畫實作與可組合 UI 分開。
具體方法是建立包含所有動畫值的類別,同時建立可傳回該類別執行個體的「update」函式。可以將轉換實作擷取到新的獨立函式中。如果需要整合動畫邏輯,或讓複雜動畫可重複使用,這個模式相當實用。
enum class BoxState { Collapsed, Expanded } @Composable fun AnimatingBox(boxState: BoxState) { val transitionData = updateTransitionData(boxState) // UI tree Box( modifier = Modifier .background(transitionData.color) .size(transitionData.size) ) } // Holds the animation values. private class TransitionData( color: State<Color>, size: State<Dp> ) { val color by color val size by size } // Create a Transition and return its animation values. @Composable private fun updateTransitionData(boxState: BoxState): TransitionData { val transition = updateTransition(boxState, label = "box state") val color = transition.animateColor(label = "color") { state -> when (state) { BoxState.Collapsed -> Color.Gray BoxState.Expanded -> Color.Red } } val size = transition.animateDp(label = "size") { state -> when (state) { BoxState.Collapsed -> 64.dp BoxState.Expanded -> 128.dp } } return remember(transition) { TransitionData(color, size) } }
使用 rememberInfiniteTransition
建立無限重複的動畫
InfiniteTransition
會保留一個或多個子動畫,例如 Transition
,但這些動畫會在進入組合階段後立即開始執行。除非您將動畫移除,否則動畫不會停止。您可以使用 rememberInfiniteTransition
建立 InfiniteTransition
的例項。同時可使用 animateColor
、animatedFloat
或 animatedValue
新增子動畫。您還需要指定 infiniteRepeatable 來指定動畫規格。
val infiniteTransition = rememberInfiniteTransition(label = "infinite") val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "color" ) Box( Modifier .fillMaxSize() .background(color) )
低階動畫 API
上一節提及的所有高階動畫 API 都是以低階動畫 API 為基礎建構而成。
animate*AsState
函式是最簡單的 API,可將即時值變化轉譯為動畫值。這個函式採用的 Animatable
是以協同程式為基礎的 API,可為單一值建立動畫效果。updateTransition
可建立轉場物件,用於管理多個動畫值並依據狀態變化執行這些值。rememberInfiniteTransition
也是類似的函式,但可建立無限轉場效果,用於管理多個無限期執行的動畫。這些 API (Animatable
除外) 都是可組合項,意味著您可以在組合外建立這些動畫效果。
這些 API 都是根據最根本的 Animation
API 所建立。雖然多數應用程式不會直接與 Animation
互動,但可透過高階 API 使用 Animation
的某些自訂功能。如要進一步瞭解 AnimationVector
和 AnimationSpec
,請參閱「自訂動畫」。
Animatable
:以協同程式為基礎的單一值動畫
Animatable
可做為保留值的元件,並在透過 animateTo
變更值時為該值建立動畫效果。這是用來支援 animate*AsState
實作的 API,可確保一致的持續性和互斥性,這表示值的變動會持續發生,並一併取消任何執行中的動畫。
系統會提供 Animatable
的許多功能 (包括 animateTo
) 做為暫停函式。這表示這些功能需要納入適當的協同程式範圍中。舉例來說,您可以使用 LaunchedEffect
可組合項為指定鍵值的期間建立範圍。
// Start out gray and animate to green/red based on `ok` val color = remember { Animatable(Color.Gray) } LaunchedEffect(ok) { color.animateTo(if (ok) Color.Green else Color.Red) } Box( Modifier .fillMaxSize() .background(color.value) )
在上述範例中,我們建立並記住了初始值為 Color.Gray
的 Animatable
執行個體。根據布林值旗標 ok
的值,動畫顏色會設為 Color.Green
或 Color.Red
。對布林值進行後續變更時,動畫會開始呈現另一個顏色。如果在變更該值時有正在執行的動畫,則系統會取消動畫,且新動畫會以目前速率從目前的快照值開始執行。
以上便是根據上一節所述的 animate*AsState
API 建構的動畫實作內容。與 animate*AsState
相比,我們可直接使用 Animatable
多方面進行更精細的控制。首先,Animatable
的初始值可以與其第一個目標值不同。舉例來說,上述程式碼範例最初顯示灰色方塊,隨後開始呈現轉為綠色或紅色的動畫效果。其次,Animatable
針對內容值提供更多操作,也就是 snapTo
和 animateDecay
。snapTo
會立即將現值設為目標值。如果動畫本身並非唯一的可靠來源,且需要與其他狀態 (例如觸控事件) 保持同步,這個函式就非常實用。animateDecay
會開始執行從指定速率開始減慢的動畫,有助於實作快速滑過行為。詳情請參閱「手勢和動畫」。
Animatable
的特點在於可支援 Float
和 Color
,但只要納入 TwoWayConverter
即可使用任何資料類型。詳情請參閱「AnimationVector」。
您可以納入 AnimationSpec
來自訂動畫規格。詳情請參閱 AnimationSpec。
Animation
:手動控制的動畫
Animation
是可用的最低階 Animation API。目前為止,我們看到的許多動畫都是以 Animation 為基礎。Animation
有兩個子類型:TargetBasedAnimation
和 DecayAnimation
。
Animation
只該用來手動控制動畫的時間。Animation
是無狀態的,沒有任何生命週期概念。它用做較高階 API 使用的動畫計算引擎。
TargetBasedAnimation
其他 API 涵蓋大部分應用實例,但直接使用 TargetBasedAnimation
,可讓您自行控制動畫播放時間。在以下範例中,您要根據 withFrameNanos
提供的影格時間手動控制 TargetAnimation
的播放時間。
val anim = remember { TargetBasedAnimation( animationSpec = tween(200), typeConverter = Float.VectorConverter, initialValue = 200f, targetValue = 1000f ) } var playTime by remember { mutableLongStateOf(0L) } LaunchedEffect(anim) { val startTime = withFrameNanos { it } do { playTime = withFrameNanos { it } - startTime val animationValue = anim.getValueFromNanos(playTime) } while (someCustomCondition()) }
DecayAnimation
與 TargetBasedAnimation
不同,DecayAnimation
不需要提供 targetValue
。相反,它會根據啟動條件 (由 initialVelocity
和 initialValue
以及提供的 DecayAnimationSpec
設定) 計算其 targetValue
。
衰退動畫通常在在翻轉手勢後使用,以將元素放慢至停止。動畫速率以 initialVelocityVector
設定的值開始,且會隨時間變慢。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 自訂動畫 {:#customize-animations}
- Compose 中的動畫
- 動畫修飾符和可組合函式