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

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 簡介

5bb2e531a22c7de0.png

上次更新時間:2022 年 5 月 27 日

在這個程式碼研究室中,您將瞭解在 Jetpack Compose 使用某些動畫 API。

Jetpack Compose 是為了簡化 UI 開發程序而設計的新型工具包。如果您是第一次使用 Jetpack Compose,不妨先嘗試進行其他程式碼研究室。

課程內容

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

必要條件

軟硬體需求

2. 開始設定

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

$ git clone git@github.com:googlecodelabs/android-compose-codelabs.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) Purple100 else Green300

在這裡,tabPage 是由 State 物件支援的 Int。背景色彩會按照值切換為紫色和綠色。我們要為這個值的變更內容製作動畫效果。

我們可以使用 animate*AsState API 幫這種簡單的值變更內容製作動畫。只要把變更的值納入對應的 animate*AsState 可組合項變數 (這裡是 animateColorAsState),就能建立動畫的值。回傳的值是 State<T> 物件,因此我們可以使用本機指派的屬性搭配 by 宣告,把它當做普通的變數處理。

val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)

再度執行應用程式,然後嘗試切換分頁。現在色彩變化有動畫效果了。

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

4. 為顯示設定製作動畫

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

「Edit」(編輯) 懸浮動作按鈕展開

「Edit」(編輯) 懸浮動作按鈕變小

找到 TODO 2-1 並看看運作方式。這個內容位於 HomeFloatingActionButton 可組合項裡。寫著「EDIT」(編輯) 的文字會使用 if 陳述式決定要顯示或隱藏。

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.colors.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.colors.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.colors.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 無法追蹤這項資訊。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) Purple700 else Green800

在這裡,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) Purple700 else Green800
}

執行應用程式,泥可以看到分頁切換的過程更生動了。按下分頁會變更 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) Purple700 else Green800
}

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

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

Android Studio 支援在 Compose 預覽畫面中檢查轉場效果。如果想使用動畫預覽,請先按一下預覽畫面 (9c05a5608a23b407.png 圖示) 可組合項右上角的「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 則是無限期為值製作動畫。

使用 rememberInfiniteTransition 函式即可建立 InfiniteTransition。然後,您可以用 InfiniteTransitionanimate* 擴充函式宣告每個值的變更內容。在本範例中,我們會為 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
    )
)

repeatMode 預設為 RepeatMode.Restart。動畫會從 initialValue 轉場到 targetValue,並從 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 毫秒裡,從 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的執行個體,並用來代表滑動式元素的水平偏移。

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 達到這個效果。由於 awaitPointerEventScopehorizontalDrag 是有限制的協同程式範圍,所有您必須在其他 launch 區塊裡呼叫 snapTo。這表示只能 suspend awaitPointerEventssnapTo 並不是指標事件。

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, so its not passed to other event handlers
    change.consumePositionChange()
}

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:

  • animatedContentSize
  • AnimatedVisibility

低階動畫 API:

  • 可以製作單一值動畫的 animate*AsState
  • 可以製作多個值動畫的 updateTransition
  • 可以無限播放值動畫的 infiniteTransition
  • 可以用觸控手勢建構自訂動畫的 Animatable

後續步驟

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

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