以價值為準的動畫

使用 animate*AsState 為單一值設定動畫效果

animate*AsState 函式是 Compose 中最簡單的動畫 API,可用於為單一值建立動畫效果。您只需提供目標值 (或結束值),該 API 就會從現值開始播放動畫,直到達到指定值。

以下範例說明如何使用這個 API 為 alpha 建立動畫效果。只要將目標值納入 animateFloatAsState 中,alpha 值就會成為所提供的值 (在本例中為 1f0.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 的特點在於可為 FloatColorDpSizeOffsetRectIntIntOffsetIntSize 提供 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 相同,可用來做為轉場效果是否完成的訊號。

我們有時會希望初始狀態與第一個目標狀態不同,這時可將 updateTransitionMutableTransitionState 搭配使用來達成。舉例來說,這麼做可讓我們在程式碼進入組合階段後立即開始執行動畫。

// 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
            }
        )
    }
}

搭配 AnimatedVisibilityAnimatedContent 使用轉換

AnimatedVisibilityAnimatedContent 可做為 Transition 的擴充函式。Transition.AnimatedVisibilityTransition.AnimatedContenttargetState 衍生自 Transition,會在 TransitiontargetState 變更時視需要觸發進入/結束轉換效果。這些擴充函式可允許原本位於 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 的例項。同時可使用 animateColoranimatedFloatanimatedValue 新增子動畫。您還需要指定 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 的某些自訂功能。如要進一步瞭解 AnimationVectorAnimationSpec,請參閱「自訂動畫」。

這張圖表展示各種低階動畫 API 之間的關聯性

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.GrayAnimatable 執行個體。根據布林值旗標 ok 的值,動畫顏色會設為 Color.GreenColor.Red。對布林值進行後續變更時,動畫會開始呈現另一個顏色。如果在變更該值時有正在執行的動畫,則系統會取消動畫,且新動畫會以目前速率從目前的快照值開始執行。

以上便是根據上一節所述的 animate*AsState API 建構的動畫實作內容。與 animate*AsState 相比,我們可直接使用 Animatable 多方面進行更精細的控制。首先,Animatable 的初始值可以與其第一個目標值不同。舉例來說,上述程式碼範例最初顯示灰色方塊,隨後開始呈現轉為綠色或紅色的動畫效果。其次,Animatable 針對內容值提供更多操作,也就是 snapToanimateDecaysnapTo 會立即將現值設為目標值。如果動畫本身並非唯一的可靠來源,且需要與其他狀態 (例如觸控事件) 保持同步,這個函式就非常實用。animateDecay 會開始執行從指定速率開始減慢的動畫,有助於實作快速滑過行為。詳情請參閱「手勢和動畫」。

Animatable 的特點在於可支援 FloatColor,但只要納入 TwoWayConverter 即可使用任何資料類型。詳情請參閱「AnimationVector」。

您可以納入 AnimationSpec 來自訂動畫規格。詳情請參閱 AnimationSpec

Animation:手動控制的動畫

Animation 是可用的最低階 Animation API。目前為止,我們看到的許多動畫都是以 Animation 為基礎。Animation 有兩個子類型:TargetBasedAnimationDecayAnimation

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。相反,它會根據啟動條件 (由 initialVelocityinitialValue 以及提供的 DecayAnimationSpec 設定) 計算其 targetValue

衰退動畫通常在在翻轉手勢後使用,以將元素放慢至停止。動畫速率以 initialVelocityVector 設定的值開始,且會隨時間變慢。