Jetpack Compose 簡易動畫

1. 事前準備

在這個程式碼研究室中,您將瞭解如何在 Android 應用程式中加入簡單的動畫。動畫可讓應用程式更具互動性、有趣,且能讓使用者更容易理解。在提供許多資訊的畫面中以動畫方式呈現個別最新資訊,可協助使用者瞭解變更的內容。

在應用程式使用者介面中,有多動畫的類型可以使用。項目可以淡入或淡出的方式顯示或消失,可以移入或移出畫面,或者也可以有趣的方式轉換。這都可以讓應用程式的使用者介面更生動易懂。

動畫還可以讓應用程式看起來更精緻、增添應用程式的外觀與風格,同時還可以協助使用者:

能吸引使用者完成任務的動畫,能為使用者歷程中的重要時刻帶來更有意義的影響。

能回應撥號鍵盤輸入的動畫元素可提供意見回饋,顯示操作是否成功。

動畫清單項目是預留位置,表示內容正在載入。

動畫式滑動以開啟操作可邀請並鼓勵使用者使用所需的手勢。

動畫圖示可以巧妙地補充或加入圖示的代表含意。

必要條件

  • 對 Kotlin 的瞭解,包括函式、lambda 和無狀態可組合項。
  • 對如何在 Jetpack Compose 中建構版面配置有基本瞭解。
  • 對如何在 Jetpack Compose 建立清單有基本瞭解。
  • 對質感設計有基本瞭解。

課程內容

  • 如何使用 Jetpack Compose 建構簡易的彈性動畫。

建構項目

  • 您將使用 Jetpack Compose 程式碼研究室,以 Woof 應用程式為基礎進行建構,並加入簡單的動畫來確認使用者的動作。

軟硬體需求

  • 最新版 Android Studio。
  • 網際網路連線以下載範例程式碼。

2. 應用程式總覽

在 Jetpack Compose 質感設計主題設定程式碼研究室中,您已使用質感設計建立 Woof 應用程式,並顯示犬隻清單及其資訊。

7252aa244a54ad90.png

在這個程式碼研究室中,您將在 Woof 應用程式中加入動畫。您將加入可在展開清單項目時顯示的興趣資訊。您還要加入彈性動畫,以動畫方式展示展開的清單項目:

1e9cf1dbc490924a.gif

取得範例程式碼

如要開始使用,請先下載範例程式碼:

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

您可以瀏覽 Woof app GitHub 存放區中的程式碼。

3. 新增展開更多資訊圖示

建立彈性動畫的第一步,就是加入展開更多資訊的圖示 f88173321938c003.png。展開更多資訊圖示可提供按鈕,讓使用者展開清單項目。

9fbd3fb0daf35fd3.png

圖示

圖示是一種符號,可透過視覺呈現的方式協助使用者瞭解使用者介面的預定功能,而且通常會以使用者預期在實體世界遇到的物體為靈感。圖示設計往往會將詳細資料精細程度降至供使用者熟悉所需的最低程度。舉例來說,實體世界中的鉛筆代表寫字,因此對應的圖示通常表示「建立」或「編輯」

相片來源:Angelina Litvin 發表於 Unsplash 網站上

黑白鉛筆圖示

質感設計提供多種圖示,並以常見類別排列,方便您依照需求選擇使用。

bfdb896506790c69.png

新增 Gradle 依附元件

在專案中加入 material-icons-extended 程式庫依附元件。您將使用此程式庫中的 Icons.Filled.ExpandLess 30c384f00846e69b.pngIcons.Filled.ExpandMore f88173321938c003.png 圖示。

  1. 在專案窗格中,開啟「Gradle Scripts」(Gradle 指令碼) >「build.gradle」(模組:Woof.app)

f7fe58e936bbad3e.png

  1. 捲動至 build.gradle (Module: Woof.app) 檔案的結尾。在 dependencies{} 區塊中,加入以下這行內容:
implementation "androidx.compose.material:material-icons-extended:$compose_version"

新增圖示可組合項

新增函式以顯示來自質感設計圖示媒體庫的展開更多資訊圖示,然後選擇做為按鈕。

  1. MainActivity.kt 中,於 DogItem() 函式後建立一個名稱為 DogItemButton() 的新可組合函式。
  2. 傳入 Boolean 做為展開狀態、按鈕點擊事件的 lambda 運算式,以及選用的 Modifier,如下所示:
@Composable
private fun DogItemButton(
    expanded: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {

}
  1. DogItemButton() 函式中,加入 IconButton() 接受 onClick 命名參數的可組合項、傳遞此圖示時會叫用的 lambda (使用結尾 lambda 語法),然後將其設為在 onClick 引數中傳遞。
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. IconButton() lambda 區塊中,加入 Icon 可組合項及名稱為 imageVector 的命名參數,然後將其設為 Icons.Filled.ExpandMore。這是會在清單項目結尾處顯示的圖示按鈕 f88173321938c003.png。Android Studio 會針對 Icon() 可組合項參數顯示警告訊息,然後您可以在後續步驟中修正。
  2. 加入命名參數 tint,然後將圖示的顏色設為 MaterialTheme.colors.secondary。加入命名參數 contentDescription,然後將其設為字串資源 R.string.expand_button_content_description
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore

IconButton(onClick = onClick) {
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       tint = MaterialTheme.colors.secondary,
       contentDescription = stringResource(R.string.expand_button_content_description)
   )
}

顯示圖示

在版面配置中加入 DogItemButton() 可組合項以顯示。

  1. DogItem() 可組合函式的開頭加入 var,以儲存清單項目的展開狀態。將初始值設為 false
var expanded by remember { mutableStateOf(false) }
  1. DogItem() 可組合函式 Row 區塊的結尾呼叫 DogItemButton() 函式,然後針對回呼傳入展開狀態和空白的 lambda。此程式碼會在清單項目中顯示圖示按鈕。
  2. 如要在清單項目內顯示圖示按鈕,請在 Row 區塊結尾的 DogItem() 可組合函式中呼叫 DogInformation(),然後後呼叫 DogItemButton()。傳入回呼的 expanded 狀態及空白的 lambda。您將在後續步驟中定義此 lambda 函式。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. 在「Design」(設計) 窗格中的「Buid & Refresh」(建構和重新整理) 預覽。

a49643f08701a8d.png

請注意,展開更多按鈕不會和清單項目結尾處對齊。您將在下一個步驟予以修正。

對齊展開更多按鈕

如要對齊展開更多按鈕與清單項目結尾,您必須在版面配置中使用 Modifier.weight() 屬性加入空格字元。

在 Woof 應用程式中,每個清單項目列都有一張犬隻圖片、犬隻資訊和展開更多資訊按鈕。您將在展開更多資訊按鈕之前,使用 1f 權重加入空格字元可組合項,以便日後對齊按鈕圖示。由於空格字元是列中唯一的加權子項元素,因此在測量其他未加權子項元素長度之後,就會填滿列中剩餘的空間。

6c2b523849f0f626.png

在清單項目列中加入空格

  1. DogItem() 可組合函式的 Row 區塊結尾,加入 Spacer。使用 weight(1f) 傳入 ModifierModifier.weight() 會使空格字元填入列中的剩餘空間。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(Modifier.weight(1f))
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. 在「Design」(設計) 窗格中的「Buid & Refresh」(建構和重新整理) 預覽。請注意,展開更多資訊按鈕現在已和清單項目結尾對齊。

f6a140413de9ad54.png

4. 新增可組合項以顯示興趣

在這項工作中,您將加入 Text 可組合項以顯示犬隻的興趣資訊。

66ea5cc5c7253d55.png

  1. 建立可接收犬隻興趣字串資源 ID 和選擇性 Modifier 的新可組合函式,名稱為 DogHobby()
  2. DogHobby() 函式中建立一個欄,並在其中加入以下邊框間距屬性,以便在欄和子項可組合項之間加入空格。
import androidx.annotation.StringRes

@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
            top = 8.dp,
            bottom = 16.dp,
            end = 16.dp
       )
   ) { }
}
  1. 在欄區塊內加入兩個 Text 可組合項,一個在興趣資訊上方顯示「About」(關於),另一種則顯示興趣資訊。

3051387c4b9c7455.png

  1. 請將「About」(關於) 的文字樣式設為 h3 (標題 3),顏色則為 onBackground。而興趣資訊的樣式則設為 body1
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. 完成的 DogHobby() 可組合函式看起來就會像這樣。
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
           top = 8.dp,
           bottom = 16.dp,
           end = 16.dp
       )
   ) {
       Text(
           text = stringResource(R.string.about),
           style = MaterialTheme.typography.h3
       )
       Text(
           text = stringResource(dogHobby),
           style = MaterialTheme.typography.body1
       )
   }
}
  1. 如要顯示 DogHobby() 可組合項,請在 DogItem() 中使用 Row 納入 Column。呼叫 DogHobby() 函式,在傳入 Row 為第二個子項後傳入 dog.hobbies 為參數。
Column() {
   Row(
       //..
   ) {
       //..
   }
   DogHobby(dog.hobbies)
}

完整的 DogItem() 函式應如下所示:

@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
        elevation = 4.dp,
       modifier = modifier.padding(8.dp)
   ) {
       Column() {
           Row(
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { expanded = !expanded },
               )
           }
           DogHobby(dog.hobbies)
       }
   }
}
  1. 在「Design」(設計) 窗格中的「Buid & Refresh」(建構和重新整理) 預覽。犬隻興趣就會顯示。

9e2e68a4bc4a8ae1.png

5. 在按鈕點擊上顯示或隱藏興趣

應用程式的每個清單項目都會有一個展開更多資訊按鈕,但這個按鈕暫時無法使用!在本節中,您將加入當使用者按一下展開更多資訊按鈕時會隱藏或顯示興趣資訊的選項。

  1. DogItem() 可組合函式中,請於 DogItemButton() 函式呼叫中定義 onClick() lambda 運算式,將按一下按鈕時的 expanded 布林值狀態值變更為 true,然後將此設定變更回按一下按鈕時的 false
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItemButton() 函式中,使用 expanded 布林值的 if 檢查納入 DogHobby() 函式呼叫。
// No need to copy over
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       //..
   ) {
       Column() {
           Row(
               //..
           ) {
               //..
           }
           if (expanded) {
               DogHobby(dog.hobbies)
           }
       }
   }
}

在上述程式碼中,犬隻興趣資訊只會在 expanded 的值為 true 時顯示。

  1. 預覽會顯示使用者介面的外觀,您也可以與之互動。如要與 UI 預覽互動,請按一下「Design」(設計) 窗格右上角的「Interactive Mode」(互動模式) 按鈕 42379dbe94a7a497.png。然後系統就會在互動模式中啟動預覽。

2a4ad1f3d2d0bff7.png

  1. 按一下展開更多資訊按鈕,就可以與預覽互動。請注意,犬隻興趣資訊會先隱藏,在按一下展開更多資訊按鈕後才會顯示。

6ee6774b5b14c7e1.gif

請注意,展開清單項目時,展開更多按鈕圖示不會改變。為了能有更好的使用者體驗,您將變更圖示,讓 ExpandMore 可以顯示向下箭頭 c761ef298c2aea5a.pngExpandLess 則顯示向上箭頭 b380f933be0b6ff4.png

  1. DogItemButton() 函式中,根據 expanded 狀態更新 imageVector 值,如下所示:
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   //..
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           //..
       )
   }
}
  1. 在裝置或模擬器上執行應用程式,或在預覽中再次使用互動模式。請注意,ExpandMore c761ef298c2aea5a.pngExpandLess b380f933be0b6ff4.png 圖示會有所不同。

bf8bb280a774a6d4.gif

圖示更新成功!

展開清單項目時,有發現高度突然改變了嗎?高度會突然改變,表示這個應用程式設計並不完善。要解決這個問題,接下來您就需要在應用程式中加入動畫。

6. 新增動畫

動畫可以加入視覺提示,讓使用者知道應用程式的目前情況。在使用者介面變更狀態時 (例如載入新內容或有新操作時),這種功能就特別實用。動畫還可以為應用程式增添細緻的視覺效果。

在本節中,您將新增一個彈性動畫,為清單項目高度變化加上動畫效果。

彈性動畫

彈性動畫是一種以彈力為主的物理動畫。使用彈性動畫時,移動的值和速度會根據套用的彈力計算。

舉例來說,如果您在畫面中拖曳某個應用程式圖示,然後移開手指以放開應用程式圖示,該圖示就會以肉眼不可見的力道移回原本的位置。

以下動畫是彈力效果的示範。手指從圖示上放開後,圖示就會往回跳,模仿彈簧的動作。

7b52f63dc639c28d.gif

彈性特效

彈力是由下列兩種屬性引導:

  • 阻尼比:彈性彈力。
  • 硬度等級:彈性硬度,也就是朝向末端彈跳移動的速度。

以下動畫範例使用不同的阻尼比和硬度等級

彈性特效高彈力

彈性特效無彈力

高硬度

極低硬度

現在,您將在應用程式中加入一個彈性動畫!

  1. MainActivity.kt,於 DogItem() 中加入 modifier 參數至 Column 版面配置。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
          modifier = Modifier
       ){
           //..
       }
   }
}

您會在 DogItem() 可組合函式中看到 DogHobby() 函式呼叫。根據 expanded 布林值,犬隻興趣資訊會包含在構圖之中。清單項目高度則會隨著興趣資訊的顯示或隱藏而變更。您將使用 animateContentSize 輔助鍵加入新高度與舊高度之間的轉場效果。

// No need to copy over
@Composable
fun DogItem(...) {

        //..
           if (expanded) {
               DogHobby(dog.hobbies)
           }
}
  1. 使用 animateContentSize 輔助鍵連結輔助鍵,即可以動畫方式呈現大小 (清單項目高度) 變更。
import androidx.compose.animation.animateContentSize

Column(
           modifier = Modifier
               .animateContentSize()
       ) {
            //..
       }

根據目前的實作,您將以動畫方式處理應用程式中的清單項目高度。然而,由於動畫太小了,執行應用程式時會難以辨識。如要解決這個問題,您可以使用選用的 animationSpec 參數自訂動畫。

  1. animateContentSize() 函式呼叫中加入 animationSpec 參數。使用 DampingRatioMediumBouncyStiffnessLow 參數,將其設為彈性動畫。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
           )
       )
)
  1. 在「Design」(設計) 窗格中的「Build & Refresh」(建立並重新整理) 預覽,然後使用互動模式或在模擬器或裝置上執行應用程式,即可看到彈性動畫的效果。

8cf711b8821b4696.gif

將應用程式傳回至模擬器或裝置,就可獲得有精美動畫效果的應用程式!

1e9cf1dbc490924a.gif

7. (選用) 嘗試使用其他動畫

animate*AsState

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

Compose 提供 FloatColorDpSizeOffsetInt 等的 animate*AsState() 函式。使用接受一般類型的 animateValueAsState(),就可以為其他資料類型輕鬆加入支援功能。

展開清單項目時,使用 animateColorAsState() 函式就可以動畫方式呈現顏色。

提示:

  1. 宣告顏色,然後將其初始化委派為 animateColorAsState() 函式。
  2. 設定 targetValue 命名的參數 (視 expanded 布林值而定)。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. 將您在上方宣告的 color 設為 Column 的背景輔助鍵。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   //..
                   )
               )
               .background(color = color)
       ) {...}
}

8. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 Git 指令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。

如要查看解決方案程式碼,請前往 GitHub 檢視

9. 結語

恭喜!您加入了可隱藏和顯示犬隻相關資訊的按鈕。還利用彈性動畫提升了使用者體驗。而且您也已經瞭解如何在設計窗格中使用互動模式。

此外,您也可以嘗試使用不同類型的 Jetpack Compose 動畫。記得使用 #AndroidBasics,透過社群媒體分享您的作品!

瞭解詳情