Jetpack Compose 提供功能強大且可擴充的 API,可讓您輕鬆在應用程式的使用者介面中實作多種動畫。本文件說明如何使用這些 API 以及根據您的動畫情況決定要使用的 API。
總覽
動畫是現代行動應用程式中不可或缺的元素,能夠提供流暢且明瞭的使用者體驗。許多 Jetpack Compose Animation API 的使用方式如同版面配置和其他 UI 元素等可組合函式,採用以 Kotlin 協同程式暫停函式建立的低階 API。本指南從對許多實際情況都相當實用的高階 API 開始介紹,隨後說明可讓您進一步控管及自訂功能的低階 API。
下圖可協助您決定要使用哪個 API 實作動畫。
- 如果要在版面配置中為內容變更建立動畫效果:
- 如果要建立顯示與消失的動畫效果:
- 根據狀態替換內容:
- 如果要為內容建立淡出淡入效果:
- 使用
Crossfade
。
- 使用
- 否則,請使用
AnimatedContent
。
- 如果要為內容建立淡出淡入效果:
- 否則,請使用
Modifier.animateContentSize
。
- 如果要依據狀態顯示動畫:
- 如果要在組合期間顯示動畫:
- 如要讓動畫持續顯示:
- 如果您同時為多個值建立動畫效果:
- 使用
updateTransition
。
- 使用
- 否則,請使用
animate*AsState
。
- 如果要在組合期間顯示動畫:
- 如果要精確控制動畫時間:
- 使用
Animation
,例如TargetBasedAnimation
或DecayAnimation
。
- 使用
- 如果動畫是事實的唯一來源
- 使用
Animatable
。
- 使用
- 否則,請使用
AnimationState
或animate
。
高階動畫 API
Compose 提供許多應用程式中多種常見動畫模式的高階動畫 API。這些 API 的設計符合質感設計動態效果的最佳做法。
AnimatedVisibility
AnimatedVisibility
可組合項以動畫方式呈現內容的顯示與消失。
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
根據預設,內容會以淡入與展開的效果顯示,並且以淡出和縮小方式消失。只要指定 EnterTransition
和 ExitTransition
即可自訂轉場效果。
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
如上例所示,您可以將多個 EnterTransition
或 ExitTransition
物件與 +
運算子合併,並為每個物件納入選用參數來自訂行為。詳情請參閱參考資料。
EnterTransition
和 ExitTransition
範例
AnimatedVisibility
也提供可包含 MutableTransitionState
的變化版本,可讓您將 AnimatedVisibility
新增至組合樹時立即觸發動畫。這項元件也有助於觀察動畫狀態。
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
為子項建立進入與結束動畫效果
AnimatedVisibility
(直接或間接子項) 中的內容可使用 animateEnterExit
輔助鍵為每個子項指定不同的動畫行為。每個子項的視覺效果都是 AnimatedVisibility
可組合項中指定的動畫和子項本身的進入和結束動畫組合而成。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
在某些情況下,您可能希望 AnimatedVisibility
完全不套用任何動畫,即可透過 animateEnterExit
為子項個別指定不同的動畫。為此,請在 AnimatedVisibility
可組合項中指定 EnterTransition.None
和 ExitTransition.None
。
新增自訂動畫
除了內建的進入與結束動畫以外,如要新增自訂動畫效果,請透過 AnimatedVisibility
內容 lambda 中的 transition
屬性存取基礎 Transition
例項。新增至 Transition 執行個體的任何動畫狀態將會與 AnimatedVisibility
的進入與結束動畫同時執行。AnimatedVisibility
會等待 Transition
中的所有動畫都結束後,才會移除其中內容。如果是與 Transition
分開建立 (例如使用 animate*AsState
) 的結束動畫,AnimatedVisibility
將無法將其納入考量,因此可以在其完成前移除內容可組合項。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
如要進一步瞭解 Transition
,請參閱 updateConversion。
animate*AsState
animate*AsState
函式是 Compose 中最簡單的動畫 API,可用於為單一值建立動畫效果。您只需提供結束值 (或目標值),該 API 就會從現值開始播放動畫,直到達到指定值。
以下範例說明如何使用這個 API 為 alpha 建立動畫效果。只要目標值納入 animateFloatAsState
中,alpha 值就會成為所提供的值 (在本例中為 1f
或 0.5f
) 之間的動畫值。
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
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。
AnimatedContent (實驗功能)
AnimatedContent
可組合項可根據目標狀態產生變動時,為內容建立動畫效果。
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
請注意,您應一律使用 lambda 參數,並將其反映至內容當中。API 會使用這個值做為索引鍵來識別目前顯示的內容。
根據預設,初始內容會淡出,然後目標內容淡入 (這個行為稱為淡出淡入切換)。只要將 ContentTransform
物件指定至 transitionSpec
參數,即可自訂這個動畫的行為。您可以使用 with
infix 函式結合 EnterTransition
與 ExitTransition
來建立 ContentTransform
。可以將 SizeTransform
套用至 ContentTransform
,做法是將它加至 using
infix 函式裡。
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
會定義目標內容的顯示方式,而 ExitTransition
則會定義初始內容的消失方式。除了可用於 AnimatedVisibility
的所有 EnterTransition
和 ExitTransition
函式外,AnimatedContent
也提供 slideIntoContainer
和 slideOutOfContainer
。這些元件可做為 slideInHorizontally/Vertically
和 slideOutHorizontally/Vertically
簡便的替代項目,可根據初始內容的大小和 AnimatedContent
內容的目標內容來計算滑動距離。
SizeTransform
用於定義初始內容和目標內容之間動畫效果的大小。建立動畫時,您可以存取初始大小和目標大小,還能透過 SizeTransform
控制在動畫播放期間是否應該將內容裁剪為元件大小。
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
子項的進入/結束動畫
與 AnimatedVisibility
一樣,AnimatedContent
的內容 lambda 中具有animateEnterExit
修飾元。使用此方法將 EnterAnimation
和 ExitAnimation
分別套用至每個直接或間接的子項中。
新增自訂動畫
與 AnimatedVisibility
一樣,transition
欄位可在 AnimatedContent
的內容 lambda 中使用。只要使用這個元件,即可建立可與 AnimatedContent
轉場同時執行的自訂動畫效果。詳情請參閱「updateTransition」。
animateContentSize
animateContentSize
修飾詞可用於建立帶有大小變化的動畫效果。
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
可在兩個版面配置之間建立交叉漸變的動畫效果。透過切換傳遞至 current
參數的值,讓系統以交叉漸變的動畫效果切換顯示內容。
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
Transition
可管理一個或多個動畫做為其子項,並在多個狀態之間同時執行這些動畫。
狀態可以是任何資料類型。在許多情況下,您可以使用自訂 enum
類型以確保類型安全,如以下範例所示:
enum class BoxState {
Collapsed,
Expanded
}
updateTransition
可建立及記住 Transition
的執行個體,並更新其狀態。
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)
接著,您可以使用其中一個 animate*
擴充函式來定義這個轉場效果中的子動畫,並為每個狀態指定目標值。這些 animate*
函式會傳回動畫值,當您在動畫播放期間使用 updateTransition
更新轉場狀態時,該值會隨著每個影格更新。
val rect by transition.animateRect { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { 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)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.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 = updateTransition(currentState)
// ...
如果是涉及多個可組合函式的複雜轉場效果,可使用 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)
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)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
onClick = { selected = !selected },
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = 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")
}
}
}
}
封裝 Transition 以重複使用
如果是簡單用途,在與 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)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { 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()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
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 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 { mutableStateOf(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
設定的值開始,且會隨時間變慢。
自訂動畫
許多 Animation API 通常都接受參數來自訂動畫行為。
AnimationSpec
多數的動畫 API 都允許開發人員透過選用的 AnimationSpec
參數來自訂動畫規格。
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
AnimationSpec
有多種類型,可用於建立不同類型的動畫。
spring
spring
可在起始值和結束值之間建立以物理特性為基礎的動畫,並接受 2 個參數:dampingRatio
和 stiffness
。
dampingRatio
可定義彈簧的彈性,預設值為 Spring.DampingRatioNoBouncy
。
stiffness
可定義彈簧朝結束值移動的速度,預設值為 Spring.StiffnessMedium
。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
與以持續時間為基礎的 AnimationSpec
類型相比,spring
可在目標值於動畫期間發生變化時確保速率的持續性,因此能更順暢地處理中斷情形。許多動畫 API (例如 animate*AsState
和 updateTransition
) 會使用 spring
做為預設的 AnimationSpec。
tween
tween
會使用緩和曲線,透過指定的 durationMillis
於起始值和結束值之間建立動畫效果。詳情請參閱「Easing」。您也可以指定 delayMillis
來延遲動畫的開始時間。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
會根據動畫期間不同時間戳記中指定的快照值建立動畫效果。在任何指定的時間,系統會在兩個主要畫面格值之間內插動畫值。針對對每個主要畫面格,都能指定 Easing 來決定內插曲線。
您可以選擇將在 0 毫秒和持續時間處指定值。如果不指定這些值,這些值會分別預設為動畫的起始值和結束值。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
會重複執行以持續時間為基礎的動畫 (例如 tween
或 keyframes
),直到達到指定的疊代次數為止。您可以傳遞 repeatMode
參數以指定是否應該從開始 (RepeatMode.Restart
) 或結束 (RepeatMode.Reverse
) 開始重複播放動畫。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
與 repeatable
類似,但會重複無限次的疊代。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
在使用 ComposeTestRule
的測試中,系統不會執行使用 infiniteRepeatable
的動畫,而會使用每個動畫值的初始值來轉譯這個元件。
snap
snap
是一種特殊的 AnimationSpec
,可將值立即切換到結束值。您可以指定 delayMillis
以便延遲動畫的開始時間。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
以持續時間為基礎的 AnimationSpec
作業 (例如 tween
或 keyframes
) 會使用 Easing
來調整動畫的分數。如此一來,動畫值就能加速或減速,而不是以固定速率移動。分數是介於 0 (起始) 和 1.0 (結束) 之間的值,表示動畫中的目前點。
Easing 實際上是一種函式,可接受介於 0 和 1.0 之間的分數值並傳回浮點值。傳回的值可能在範圍邊界外,代表過衝或是下衝。您可以建立自訂的 Easing,如以下程式碼所示。
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose 提供多種內建 Easing
函式,可滿足大多數用途的需求。如要進一步瞭解如何根據您的情境使用哪些 Easing,請參閱「速度 - 質感設計」。
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- 顯示更多資訊
AnimationVector
多數 Compose 動畫 API 皆支援 Float
、Color
、Dp
和其他基本資料類型做為可立即使用的動畫值,但有時您需要為其他資料類型 (包括您的自訂類型) 建立動畫。在動畫播放期間,任何動畫值都會以 AnimationVector
表示。使用對應的 TwoWayConverter
即可將值轉換為 AnimationVector
,反之亦然。這樣以來,核心動畫系統就能統一處理這些內容。舉例來說,Int
是以包含單一浮點值的 AnimationVector1D
代表。「Int
」的「TwoWayConverter
」看起來會像是這樣:
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
基本上,Color
是紅色、綠色、藍色和 alpha 這 4 個值的組合,因此 Color
可轉換成包含 4 個浮點值的 AnimationVector4D
。透過這種方式,動畫中使用的每種資料類型都會轉換成 AnimationVector1D
、AnimationVector2D
、AnimationVector3D
或 AnimationVector4D
,視其維度而定。如此一來,您就能針對物件的不同元件個別建立動畫效果,而每個元件都有各自的速率追蹤。基本資料類型的內建轉換器可使用 Color.VectorConverter
、Dp.VectorConverter
等方式存取。
如要想要為新的資料類型新增支援以作為動畫值,您可以建立專屬的 TwoWayConverter
並將其提供給 API。舉例來說,您可以使用 animateValueAsState
為自訂資料類型建立動畫效果,如下所示:
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
動畫向量資源 (實驗功能)
若要使用 AnimatedVectorDrawable
資源,請使用 animatedVectorResource
載入可繪項目檔案,然後傳遞到 boolean
,以便讓可繪項目切換開始和結束狀態。
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
有關可繪項目檔案格式的詳細資訊請參閱「以動畫方式呈現可繪項目圖形」。
清單項目動畫
如果您要在 Lazy 清單或格線中為項目重新排序建立動畫,請參閱 Lazy 版面配置項目動畫文件。
手勢和動畫 (進階)
與單獨處理動畫相比,同時處理觸控事件和動畫時,必須考慮幾個事項。首先,在觸控事件開始時,我們可能必須中斷處理中的動畫,因為使用者互動事件應該擁有最高的優先順序。
在以下範例中,我們使用 Animatable
表示圓形元件的偏移位置,並透過 pointerInput
修飾詞處理觸控事件。我們偵測到新的輕觸事件時,會呼叫 animateTo
建立從位移值移往輕觸位置的動畫效果。輕觸事件也可能會在出現動畫播放期間發生,在這種情況下,animateTo
會中斷目前執行的動畫,並讓動畫移向新的目標位置,同時維持已中斷動畫的速率。
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
另一個常見的模式是需要將動畫值與來自觸控事件 (例如拖曳) 的值同步處理。在以下範例中,我們會看到以 Modifier
(而不是使用 SwipeToDismiss
可組合項) 實作的「滑動關閉」動作。該元素的水平位移會以 Animatable
表示。這個 API 的特性適合在手勢動畫中使用。觸控事件和動畫都可以變更這個值。接收到觸控事件時,我們會透過 stop
方法停止 Animatable
,以便攔截任何執行中的動畫。
在拖曳事件期間,我們會使用 snapTo
將 Animatable
值更新為從觸控事件計算得出的值。針對快速滑過,Compose 可提供 VelocityTracker
來記錄拖曳事件並計算速率。該速率可直接動態饋給至 animateDecay
來執行快速滑過動畫。如要將位移值滑回原始位置,可使用 animateTo
方法指定 0f
的目標位移值。
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
測試
Compose 提供 ComposeTestRule
,可讓您以確定性方式編寫動畫測試,並完全控管測試時鐘,方便您驗證中間動畫值。此外,測試的執行速度會比動畫的實際持續時間快。
ComposeTestRule
會公開測試時鐘做為 mainClock
。您可以將 autoAdvance
屬性設為 false,以控制測試程式碼中的時鐘。啟動要測試的動畫之後,可使用 advanceTimeBy
將時鐘往前移動。
值得一提的是,advanceTimeBy
不會完全依照指定持續時間移動時鐘,而是將秒數四捨五入至最接近的持續時間,且為影格持續時間的倍數。
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
工具支援
Android Studio 支援在 Animation Preview 中檢查 updateTransition
和 animatedVisibility
。您可以執行以下操作:
- 逐格預覽轉場效果
- 檢查轉場效果中所有動畫的值
- 預覽初始狀態和目標狀態之間的轉場效果
- 一次檢查並協調多個動畫
開啟「Animation Preview」時,畫面上會顯示「Animations」窗格,讓您執行預覽畫面中的所有轉場效果。轉場效果和其中每個動畫值都會標示預設名稱。您可以在 updateTransition
和 AnimatedVisibility
函式中指定 label
參數來自訂標籤。詳情請參閱「Animation Preview」。
瞭解詳情
如要進一步瞭解 Jetpack Compose 中的動畫,請參閱以下其他資源: