在 Jetpack Compose 中為元素建立動畫效果

1. 簡介

Jetpack Compose 標誌

上次更新時間:2023 年 11 月 21 日

在本程式碼研究室中,您將瞭解如何使用 Jetpack Compose 中的一些動畫 API。

Jetpack Compose 是專為簡化 UI 開發程序而設計的新型 UI 工具包。如果您是第一次使用 Jetpack Compose,不妨先參考以下的程式碼研究室。

課程內容

  • 如何使用多個基本動畫 API

必要條件

軟硬體需求

2. 開始設定

下載程式碼研究室的程式碼。您可以複製存放區,如下所示:

$ git clone https://github.com/android/codelab-android-compose.git

或者,您也可以將存放區下載為 ZIP 檔案:

在 Android Studio 中匯入 AnimationCodelab 專案。

將動畫程式碼研究室匯入 Android Studio

專案中包含多個模組:

  • start 是本程式碼研究室的起始狀態。
  • finished 是完成本程式碼研究室之後的應用程式最終狀態。

請確定您已選取執行設定下拉式選單內的 start

顯示已選取 Android Studio 中的「start」

我們會從下一節開始介紹幾種動畫情境。本程式碼研究室使用的每個程式碼片段都會加上 // TODO 註解。只要開啟 Android Studio 中的「TODO」工具視窗,就能瀏覽該章節的每個 TODO 註解。

Android Studio 中顯示的 TODO 清單

3. 為簡易值變更加上動畫效果

首先要使用 Compose 中最簡單的動畫 API 之一:animate*AsState API。如要為 State 變更加上動畫效果,則應使用這個 API。

請執行 start 設定,然後點選頂端的「Home」和「Work」按鈕,嘗試切換分頁。這項操作並不會實際切換分頁內容,但內容的背景顏色會變換。

已選取「Home」分頁

已選取「Work」分頁

按一下「TODO」工具視窗中的「TODO 1」,並查看實作方式。這個內容位於 Home 可組合函式。

val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight

此處的 tabPage 是由 State 物件支援的 TabPage。背景色彩會依照值切換為蜜桃色和綠色。我們要為這種值變更行為加上動畫效果。

為這種簡單的值變更行為加上動畫效果時,可以使用 animate*AsState API。只要把變更的值納入 animate*AsState 可組合函式的對應變數 (在此情況下為 animateColorAsState),就能建立動畫值。回傳的值為 State<T> 物件,因此可以使用本機指派屬性搭配 by 宣告,將該物件視為普通變數。

val backgroundColor by animateColorAsState(
        targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
        label = "background color")

請再次執行應用程式,並嘗試切換分頁。現在色彩變更時會有動畫效果。

切換分頁時的色彩變更動畫效果

4. 為顯示項目加上動畫效果

如果您捲動應用程式內容,就會發現懸浮動作按鈕隨著捲動方向展開和縮小。

展開的「Edit」懸浮動作按鈕

縮小的「Edit」懸浮動作按鈕

找到 TODO 2-1 並看看運作方式。這個項目位於 HomeFloatingActionButton 可組合函式。系統會使用 if 陳述式決定要顯示或隱藏「EDIT」文字。

if (extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

如要為這種顯示變更行為加上動畫效果,只要將 if 替換為 AnimatedVisibility 可組合函式即可。

AnimatedVisibility(extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

請執行應用程式,查看懸浮動作按鈕 (FAB) 現在如何展開和縮小。

「EDIT」懸浮動作按鈕動畫

每當指定的 Boolean 值有所變更,AnimatedVisibility 都會執行動畫。根據預設,AnimatedVisibility 顯示元素時會淡入並展開,隱藏元素時則會淡出並縮小。這項行為非常適合此範例中的懸浮動作按鈕 (FAB),但我們也可以自訂行為。

嘗試點選懸浮動作按鈕 (FAB) 後,應該會看到「Edit feature is not supported」訊息。如要加上出現和消失動畫效果,也會用到 AnimatedVisibility。接下來,您將會自訂這項行為,讓這個訊息從頂端滑入,然後再滑出頂端。

表示不支援編輯功能的訊息

請找出 TODO 2-2,然後查看 EditMessage 可組合函式中的程式碼。

AnimatedVisibility(
    visible = shown
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

為了自訂動畫,請在 AnimatedVisibility 可組合函式中加入 enterexit 參數。

enter 參數應該是 EnterTransition 的例項。在本範例中,我們可以利用 slideInVertically 函式建立 EnterTransitionslideOutVertically,製作退出轉場效果。請變更程式碼,如下所示:

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(),
    exit = slideOutVertically()
)

請再度執行應用程式,然後點選「EDIT」按鈕。您可能會發現動畫效果更流暢,但不太正確,這是因為 slideInVerticallyslideOutVertically 的預設行為只使用項目的一半高度。

只垂直滑出一半就結束

針對進入轉場效果,我們可以設定 initialOffsetY 參數,將預設行為調整為使用項目的完整高度,製作出合適的動畫效果。initialOffsetY 應是會傳回初始位置的 lambda。

這個 lambda 會接收元素高度引數。為確保項目從畫面頂端滑入,我們會傳回負值,因為畫面頂端的值為 0。我們想讓動畫從 -height 開始再到 0 (最終位置),這樣動畫就會從上方開始進入。

使用 slideInVertically 時,滑入後的目標偏移值一律為 0 (像素)。initialOffsetY 可以指定為絕對值,或以 lambda 函式指定為元素完整高度的百分比。

同樣地,slideOutVertically 也會將初始偏移值假設為 0,所以只需要指定 targetOffsetY

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight }
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight }
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

請再度執行應用程式,就可以看到動畫更接近預期效果:

加上偏移的滑入動畫

我們可以利用 animationSpec 參數,進一步自訂動畫。animationSpec 是許多動畫 API 的常用參數,包括 EnterTransitionExitTransition。我們可以傳遞其中一種 AnimationSpec 類型,指定動畫值應如何隨著時間變化。在這個範例中,我們要使用以時間長度為基礎的簡易 AnimationSpec。您可以使用 tween 函式建立這個項目。時間長度為 150 毫秒,加/減速為 LinearOutSlowInEasing。至於退出動畫,我們在 animationSpec 參數使用相同的 tween 函式,但是時間設為 250 毫秒,加/減速為 FastOutLinearInEasing

完成的程式碼應如下所示:

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

請再度執行應用程式,然後點選懸浮動作按鈕 (FAB)。您現在可以看到訊息從頂端滑入並滑出,且具有不同的加/減速函式和時間長度:

顯示編輯訊息從頂端滑入的動畫

5. 為內容大小變更加上動畫效果

這個應用程式的內容會顯示多個主題。您可以嘗試點選任何主題,主題應會展開並顯示內文。主題顯示或隱藏內文時,含有文字的資訊卡分別會展開和縮小。

收合的主題清單

展開的主題清單

請查看 TopicRow 可組合函式中 TODO 3 的程式碼。

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
) {
    // ... the title and the body
}

Column 可組合函式會在內容變更時變更大小。我們可以加入 animateContentSize 修飾符,為這個大小變更行為加上動畫效果。

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
        .animateContentSize()
) {
    // ... the title and the body
}

請執行應用程式,然後點選其中一個主題。您可以看到主題展開和縮小的動畫。

主題清單展開和收合的動畫

經過自訂的 animationSpec 也可以用來自訂 animateContentSize。我們可以提供選項,將動畫類型從彈簧動畫改成補間動畫等等。詳情請參閱自訂動畫說明文件

6. 為多個值加上動畫效果

現在我們已熟悉基本的動畫 API,接下來要瞭解如何使用 Transition API 製作更複雜的動畫。使用 Transition API 時,我們可以追蹤 Transition 上的所有動畫何時播放完畢,而先前介紹的個別 animate*AsState API 沒有這項功能。Transition API 也可以用來定義狀態轉換期間的不同 transitionSpec。以下說明使用方式:

在本範例中,我們要自訂分頁指標,也就是目前所選取分頁上的矩形。

已選取「Home」分頁

已選取「Work」分頁

請在 HomeTabIndicator 可組合函式中找出 TODO 4,瞭解如何實作分頁指標。

val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green

此處 indicatorLeft 指的是指標左側邊緣在分頁列中的水平位置。indicatorRight 則是指標右側邊緣的水平位置。此外,色彩會切換為蜜桃色和綠色。

如要同時為多個值加上動畫效果,可以使用 Transition。您可以利用 updateTransition 函式建立 Transition。請將目前所選取分頁的索引傳遞為 targetState 參數。

每個動畫值都可以使用 Transitionanimate* 擴充函式進行宣告。本範例使用的是 animateDpanimateColor。這些擴充函式會接收 lambda 區塊,而我們可以指定每個狀態的指定值。我們已知道所有指定值,因此能透過以下方式納入值。請注意,我們可以使用 by 宣告,在此處再次將它變成本機委派屬性,因為 animate* 函式會傳回 State 物件。

val transition = updateTransition(tabPage, label = "Tab indicator")
val indicatorLeft by transition.animateDp(label = "Indicator left") { page ->
   tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
   tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = "Border color") { page ->
   if (page == TabPage.Home) PaleDogwood else Green
}

立即執行應用程式,您可以看到分頁切換的過程變得更生動了。點選分頁標籤會變更 tabPage 狀態的值,因此所有與 transition 相關聯的動畫值都會開始以動畫形式切換成目標狀態所指定的值。

「Home」和「Work」分頁的切換動畫

此外,我們還可以指定 transitionSpec 參數,自訂動畫行為,例如讓靠近目的地該側的移動速度大於另一側,創造出指標的彈跳效果。我們可以在 transitionSpec lambda 中使用 isTransitioningTo 中置函式,決定狀態變更的方向。

val transition = updateTransition(
    tabPage,
    label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right.
            // The left edge moves slower than the right edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        } else {
            // Indicator moves to the left.
            // The left edge moves faster than the right edge.
            spring(stiffness = Spring.StiffnessMedium)
        }
    },
    label = "Indicator left"
) { page ->
    tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right
            // The right edge moves faster than the left edge.
            spring(stiffness = Spring.StiffnessMedium)
        } else {
            // Indicator moves to the left.
            // The right edge moves slower than the left edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        }
    },
    label = "Indicator right"
) { page ->
    tabPositions[page.ordinal].right
}
val color by transition.animateColor(
    label = "Border color"
) { page ->
    if (page == TabPage.Home) PaleDogwood else Green
}

請再度執行應用程式,然後嘗試切換分頁。

切換分頁時的自訂彈跳效果

Android Studio 支援在 Compose 預覽中檢查轉場效果。如要使用動畫預覽,請先按一下預覽畫面 (動畫預覽圖示 圖示) 可組合函式右上角的「Start Animation Preview」圖示,開啟互動模式。請嘗試點選 PreviewHomeTabBar 可組合函式的圖示。這麼做會開啟新的「Animations」窗格。

按一下「Play」圖示按鈕即可執行動畫。此外,只要拖曳跳轉滑桿,即可查看每一個動畫影格。您可以在 updateTransitionanimate* 方法內指定 label 參數,為動畫值提供更清楚的說明。

在 Android Studio 中使用跳轉滑桿查看動畫

7. 重複播放動畫

請嘗試點選目前溫度旁的重新整理圖示按鈕。應用程式會假裝開始載入最新的天氣資訊。在完成載入作業之前,您會看到載入指標,也就是灰色圓圈和長條。我們接下來要為這個指標的 Alpha 值加上動畫效果,讓使用者更清楚瞭解程序正在執行。

未加上動畫效果的預留位置資訊卡靜態圖片。

請在 LoadingRow 可組合函式中找出 TODO 5

val alpha = 1f

我們要讓這個值在 0f 和 1f 之間重複播放動畫。使用 InfiniteTransition 即可達成此效果。這個 API 和上一章節的 Transition API 非常類似。這兩種 API 都可以為多個值加上動畫效果,但 Transition 會根據狀態變更來執行值的動畫,InfiniteTransition 則會無限期執行值的動畫。

為建立 InfiniteTransition,請使用 rememberInfiniteTransition 函式。然後,您可以使用 InfiniteTransition 的其中一個 animate* 擴充函式,宣告每個值變更行為的動畫。在本範例中,我們要為 Alpha 值加上動畫效果,所以要使用 animatedFloatinitialValue 參數應該為 0ftargetValue 則是 1f。我們也可以為動畫指定 AnimationSpec,但這個 API 只接受 InfiniteRepeatableSpec。請使用 infiniteRepeatable 函式建立該項目。此 AnimationSpec 會納入任何以時間長度為基礎的 AnimationSpec,並設為可重複。舉例來說,完成的程式碼應如下所示。

val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = keyframes {
            durationMillis = 1000
            0.7f at 500
        },
        repeatMode = RepeatMode.Reverse
    ),
    label = "alpha"
)

repeatMode 預設為 RepeatMode.Restart。轉場效果會從 initialValuetargetValue,然後再度從 initialValue 開始。把 repeatMode 設定為 RepeatMode.Reverse,動畫就會從 initialValue 變為 targetValue,再從 targetValue 變為 initialValue。動畫會從 0 到 1,再從 1 到 0。

keyFrames 動畫是另一類型的 animationSpec,可在不同毫秒時變更處理中的值。其他類型包括 tweenspring。我們最初將 durationMillis 設為 1000 毫秒,之後則可以定義動畫的主要畫面格,例如在動畫 500 毫秒時將 Alpha 值設為 0.7f。這樣做能夠改變動畫的速度,在動畫前 500 毫秒裡,從 0 變成 0.7 的速度會比較快,而到最後從 500 毫秒變成 1000 毫秒裡,從 0.7 變成 1.0 的速度比較慢。

如果想設定多個主要畫面格,可以定義多個 keyFrames,如下所示:

animation = keyframes {
   durationMillis = 1000
   0.7f at 500
   0.9f at 800
}

請執行應用程式,然後嘗試點選重新整理按鈕。您現在可以看到載入指標的動畫。

重複播放預留位置內容的動畫

8. 手勢動畫

在最後這一節中,我們會學到如何執行以觸控輸入為基礎的動畫。我們會從頭開始建構 swipeToDismiss 修飾符。

請在 swipeToDismiss 修飾符中找出 TODO 6-1。我們在這裡要嘗試製作修飾詞,讓元素可以用觸控方式滑動。當元素快速滑過畫面邊緣時,我們會呼叫 onDismissed 回呼,讓系統移除元素。

想建構 swipeToDismiss 修飾符之前,我們需要釐清幾個重要概念。首先,當使用者用手指觸碰螢幕時,會產生一個有 x 和 y 座標的觸控事件,然後使用者會把手指移到右邊或左邊,根據移動方式移動 x 跟 y。使用者觸碰的項目需要跟著手指一起移動,所以我們會根據觸控事件的位置和速率更新項目的位置。

我們可以運用 Compose 手勢說明文件所述的幾種概念。透過使用 pointerInput 修飾符,我們可以取得低階存取權,進而存取傳入的指標觸控事件,並利用同一指標追蹤使用者的拖曳速率。如果使用者在項目超過關閉邊界之前就放開手指,項目會回到原有位置上。

在這個情境中,需要考量一些獨特事項。首先,播放中的動畫可能會遭到觸控事件中斷。再來,動畫值可能不是唯一的資料來源。也就是說,我們可能需要同步處理動畫值和來自觸控事件的值。

Animatable 是我們目前看到最低階的 API。這個 API 可以為手勢情境提供多種實用功能,例如立即切換成來自手勢的新值,以及在觸發新觸控事件時停止任何播放中的動畫。我們可以建立 Animatable 的執行個體,並用來代表滑動式元素的水平偏移。請務必從 androidx.compose.animation.core.Animatable 匯入 Animatable,而非從 androidx.compose.animation.Animatable

val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
    // Used to calculate a settling position of a fling animation.
    val decay = splineBasedDecay<Float>(this)
    // Wrap in a coroutine scope to use suspend functions for touch events and animation.
    coroutineScope {
        while (true) {
            // ...

TODO 6-2 就是剛才接收觸控事件的地方。如果目前動畫正在執行,我們應該中斷動畫。只要在 Animatable 上呼叫 stop 即可達成這個效果。請注意,如果目前沒有執行動畫,系統就會忽略此呼叫。VelocityTracker 會計算使用者從左到右的移動速度。awaitPointerEventScope 屬於暫停函式,可以等待使用者輸入事件,再做出回應。

// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {

我們會在 TODO 6-3 持續接收拖曳事件,而且必須將觸控事件的位置同步處理至動畫值。在 Animatable 上使用 snapTo 即可達到這個效果。您必須在其他 launch 區塊內呼叫 snapTo,因為 awaitPointerEventScopehorizontalDrag 是受限制的協同程式範圍。也就是說,這些範圍只能為 awaitPointerEventssuspend,但 snapTo 並不是指標事件。

horizontalDrag(pointerId) { change ->
    // Add these 4 lines
    // Get the drag amount change to offset the item with
    val horizontalDragOffset = offsetX.value + change.positionChange().x
    // Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
    launch {
        // Instantly set the Animable to the dragOffset to ensure its moving
        // as the user's finger moves
        offsetX.snapTo(horizontalDragOffset)
    }
    // Record the velocity of the drag.
    velocityTracker.addPosition(change.uptimeMillis, change.position)

    // Consume the gesture event, not passed to external
    if (change.positionChange() != Offset.Zero) change.consume()

}

TODO 6-4 是剛才釋放和快速滑過元素的位置。我們需要計算快速滑過設定的最終位置,才能決定是把元素移回本來的位置,還是滑出並叫用回呼。我們會使用先前建立的 decay 來計算 targetOffsetX

// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)

TODO 6-5,我們將要開始執行動畫。在此之前,我們需要為 Animatable 設定上限值與下限值,以便其在達到限制時立刻停止 (即 -size.widthsize.width,因為我們不想讓 offsetX 超過這兩個值)。pointerInput 修飾符可透過 size 屬性存取元素大小,因此我們要使用此修飾符取得邊界。

offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)

TODO 6-6,我們終於可以開始執行動畫了。首先,要比較先前計算的快速滑過最終位置和元素大小。如果最終位置低於大小,表示快速滑過速率不足。我們可以使用 animateTo,將動畫值設回 0f。如果是其他情況,就能使用 animateDecay 開始執行快速滑過動畫。動畫結束時,就可以呼叫回呼。動畫結束原因很可能是先前設定的上下限。

launch {
    if (targetOffsetX.absoluteValue <= size.width) {
        // Not enough velocity; Slide back.
        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
    } else {
        // Enough velocity to slide away the element to the edge.
        offsetX.animateDecay(velocity, decay)
        // The element was swiped away.
        onDismissed()
    }
}

最後,請查看 TODO 6-7。我們已設定所有動畫和手勢,因此請務必為元素套用偏移。這樣一來,畫面上的元素就會移動到手勢或動畫所產生的值:

.offset { IntOffset(offsetX.value.roundToInt(), 0) }

本節結束後,您的程式碼應如下所示:

private fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    // This Animatable stores the horizontal offset for the element.
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        // Used to calculate a settling position of a fling animation.
        val decay = splineBasedDecay<Float>(this)
        // Wrap in a coroutine scope to use suspend functions for touch events and animation.
        coroutineScope {
            while (true) {
                // Wait for a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                // Interrupt any ongoing animation.
                offsetX.stop()
                // Prepare for drag events and record velocity of a fling.
                val velocityTracker = VelocityTracker()
                // Wait for drag events.
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Record the position after offset
                        val horizontalDragOffset = offsetX.value + change.positionChange().x
                        launch {
                            // Overwrite the Animatable value while the element is dragged.
                            offsetX.snapTo(horizontalDragOffset)
                        }
                        // Record the velocity of the drag.
                        velocityTracker.addPosition(change.uptimeMillis, change.position)
                        // Consume the gesture event, not passed to external
                        change.consumePositionChange()
                    }
                }
                // Dragging finished. Calculate the velocity of the fling.
                val velocity = velocityTracker.calculateVelocity().x
                // Calculate where the element eventually settles after the fling animation.
                val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
                // The animation should end as soon as it reaches these bounds.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // Not enough velocity; Slide back to the default position.
                        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
                    } else {
                        // Enough velocity to slide away the element to the edge.
                        offsetX.animateDecay(velocity, decay)
                        // The element was swiped away.
                        onDismissed()
                    }
                }
            }
        }
    }
        // Apply the horizontal offset to the element.
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
}

請執行應用程式,並嘗試滑動其中一個任務項目。您可以看到元素滑回預設位置,或在滑出後遭到移除,具體取決於快速滑過手勢的速率。您也可以在動畫播放過程中擷取元素。

滑動關閉項目的手勢動畫

9. 恭喜!

恭喜!您已瞭解基本的 Compose 動畫 API。

在本程式碼研究室中,我們學到如何使用以下 API:

高階動畫 API:

  • animatedContentSize
  • AnimatedVisibility

低階動畫 API:

  • animate*AsState:可為單一值加上動畫效果
  • updateTransition:可為多個值加上動畫效果
  • infiniteTransition:可無限期為值加上動畫效果
  • Animatable:可利用觸控手勢建構自訂動畫

後續步驟

請參閱 Compose 課程中的其他程式碼研究室。

如果想瞭解更多資訊,請參閱 Compose 動畫相關文章和以下參考文件: