Jetpack Compose 簡易動畫

1. 事前準備

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

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

動畫還能讓應用程式看起來更精緻、提升應用程式的外觀與風格,同時為使用者帶來助益。

必要條件

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

課程內容

  • 如何使用 Jetpack Compose 建構簡易的彈簧效果。

建構項目

軟硬體需求

  • 最新的 Android Studio 穩定版。
  • 連上網際網路,可下載範例程式碼。

2. 應用程式總覽

在程式碼研究室「使用 Jetpack Compose 進行 Material Design 主題設定」中,您已使用 Material Design 建立 Woof 應用程式,顯示狗和相關資訊的清單。

8de41607e8ff2c79.png

在本程式碼研究室中,您將在 Woof 應用程式中加入動畫,並新增可在展開清單項目時顯示的興趣資訊。您還要加入彈簧效果,以動畫呈現清單項目展開的樣子:

c0d0a52463332875.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. 新增「顯示更多內容」圖示

在本節中,您將在應用程式新增「顯示更多內容」圖示 30c384f00846e69b.png 和「顯示較少內容」f88173321938c003.png 圖示。

def59d71015c0fbe.png

圖示

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

筆記本上的鉛筆相片來源:Angelina Litvin 發表於 Unsplash 網站上

黑白鉛筆圖示

Material Design 提供多種圖示,並依常見類別排列,方便您視需求選擇使用。

Material Design 圖示庫

新增 Gradle 依附元件

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

  1. 在「Project」窗格中,依序開啟「Gradle Scripts」>「build.gradle.kts (Module :app)」
  2. 捲動至 build.gradle.kts (Module :app) 檔案的結尾。在 dependencies{} 區塊中,加入以下這行程式碼:
implementation("androidx.compose.material:material-icons-extended")

新增圖示可組合函式

請新增函式,顯示來自 Material Design 圖示庫的「顯示更多內容」圖示,並設為按鈕。

  1. MainActivity.kt 中的 DogItem() 函式後,建立名為 DogItemButton() 的新可組合函式。
  2. 為展開狀態傳入 Boolean、為 onClick 處理常式傳入 lambda 運算式,並視需要傳入 Modifier,如下所示:
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {

}
  1. DogItemButton() 函式中,新增接受 onClick 具名參數的 IconButton() 可組合函式、使用結尾 lambda 語法的 lambda (會在按下圖示時叫用),並視需要新增 modifier。請將 IconButton's onClickmodifier value parameters 設為等於傳入 DogItemButton 的項目。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. IconButton() lambda 區塊中,加入 Icon 可組合函式,並將 imageVector value-parameter 設為 Icons.Filled.ExpandMore。這會顯示在清單項目末端 f88173321938c003.png。Android Studio 會針對 Icon() 可組合函式參數顯示警告,您將在下一步驟中修正這個問題。
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

IconButton(
   onClick = onClick,
   modifier = modifier
) {
   Icon(
       imageVector = Icons.Filled.ExpandMore
   )
}
  1. 加入價值參數 tint,然後將圖示顏色設為 MaterialTheme.colorScheme.secondary。加入具名參數 contentDescription,並設為字串資源 R.string.expand_button_content_description
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}

顯示圖示

在版面配置中加入 DogItemButton() 可組合函式,即可顯示該函式。

  1. DogItem() 開頭加入 var,儲存清單項目的展開狀態。將初始值設為 false
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

var expanded by remember { mutableStateOf(false) }
  1. 在清單項目中顯示圖示按鈕。請在 DogItem() 可組合函式中 Row 區塊的結尾,於呼叫 DogInformation() 後新增 DogItemButton()。傳入回呼的 expanded 狀態及空白的 lambda。您將在後續步驟中定義 onClick 動作。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. 在「Design」窗格中查看 WoofPreview()

5bbf09cd2828b6.png

請注意,「顯示更多內容」按鈕不會對齊清單項目的末端。您將在下一個步驟予以修正。

對齊「顯示更多內容」按鈕

如要將「顯示更多內容」按鈕與清單項目末端對齊,您需要在版面配置中使用 Modifier.weight() 屬性加入空格字元。

Woof 應用程式中,每個清單項目列都有狗的圖片和資訊,以及「顯示更多內容」按鈕。您將在「顯示更多內容」按鈕前方使用 1f 權重加入 Spacer 可組合函式,適當對齊按鈕圖示。由於空格字元是列中唯一的加權子項元素,因此在測量其他未加權子項元素的寬度之後,空格字元就會填滿列中其餘空間。

733f6d9ef2939ab5.png

在清單項目列中加入空格字元

  1. DogItem()DogInformation()DogItemButton() 之間,新增 Spacer。使用 weight(1f) 傳入 ModifierModifier.weight() 會使空格字元填入列中剩餘空間。
import androidx.compose.foundation.layout.Spacer

Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. 在「Design」窗格中查看 WoofPreview()。請注意,「顯示更多內容」按鈕現已對齊清單項目末端。

8df42b9d85a5dbaa.png

4. 新增顯示興趣的可組合函式

在這項工作中,您將加入 Text 可組合函式,顯示狗的興趣資訊。

bba8146c6332cc37.png

  1. 建立名為 DogHobby() 的新可組合函式,用來接收狗的興趣字串資源 ID 和選用的 Modifier
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. DogHobby() 函式中建立 Column,並傳入傳遞至 DogHobby() 的修飾符。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) {

   }
}
  1. Column 區塊加入兩個 Text 可組合函式,一個在興趣資訊上方顯示「About」文字,另一個則顯示興趣資訊。

在「strings.xml」檔案中,將第一個函式的 text 設為 about,並將 style 設為 labelSmall。然後將第二個函式的 text 設為傳入的 dogHobbystyle 則設為 bodyLarge

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. DogItem() 中,DogHobby() 可組合函式會位於包含 DogIcon()DogInformation()Spacer()DogItemButton()Row 下方。若要如此設定,請使用 Column 納入 Row,這樣興趣就能新增至 Row 下方。
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}
  1. Row 後方加上 DogHobby(),做為 Column 的第二個子項。傳入包含所傳入狗獨特興趣的 dog.hobbies,並為 DogHobby() 可組合函式傳入具有邊框間距的 modifier
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}

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

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies,
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}
  1. 在「Design」窗格中查看 WoofPreview()。您會看到畫面上顯示狗的興趣。

Woof 預覽畫面中的清單項目已展開

5. 在點選按鈕時顯示或隱藏興趣

應用程式的每個清單項目都有一個「顯示更多內容」按鈕,但這個按鈕目前沒有作用!在本節中,您將加入選項,讓系統在使用者點選「顯示更多內容」按鈕時隱藏或顯示興趣資訊。

  1. DogItem() 可組合函式中,請於 DogItemButton() 函式呼叫中定義 onClick() lambda 運算式,將使用者點選按鈕時的 expanded 布林狀態值變更為 true,並將再次點選按鈕時的布林狀態值變更為 false
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItem() 函式中,使用 expanded 布林值的 if 檢查結果納入 DogHobby() 函式呼叫。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}

現在,只有 expanded 的值為 true 時,系統才會顯示狗的興趣資訊。

  1. 預覽畫面可以顯示 UI 的外觀,您也可以與預覽畫面互動。如要與 UI 預覽畫面互動,請將滑鼠游標懸停在「Design」窗格的 WoofPreview 文字上方,然後點選「Design」窗格右上角的「Interactive Mode」按鈕 42379dbe94a7a497.png。系統就會在互動模式中啟動預覽。

578d665f081e638e.png

  1. 點選「顯示更多內容」按鈕,就可以與預覽畫面互動。請注意,系統會先隱藏狗的興趣資訊,在點選「顯示更多內容」按鈕後才顯示出來。

Woof 清單項目展開和收合的動畫

請注意,展開清單項目時,「顯示更多內容」按鈕圖示不會改變。為了提升使用者體驗,您將變更圖示,讓 ExpandMore 顯示向下箭頭 c761ef298c2aea5a.pngExpandLess 則顯示向上箭頭 b380f933be0b6ff4.png

  1. DogItemButton() 函式中新增 if 陳述式,根據 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,
           ...
       )
   }
}

請注意您在先前的程式碼片段中撰寫 if-else 的方式。

if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore

這與下列程式碼中使用大括號 { } 的效果相同:

if (expanded) {

`Icons.Filled.ExpandLess`

} else {

`Icons.Filled.ExpandMore`

}

如果 if-else 陳述式只有一行程式碼,則不一定要使用大括號。

  1. 在裝置或模擬器上執行應用程式,或在預覽中再次使用互動模式。請注意,系統會切換顯示 ExpandMore c761ef298c2aea5a.pngExpandLess b380f933be0b6ff4.png 圖示。

de5dc4a953f11e65.gif

圖示更新成功!

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

6. 新增動畫

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

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

彈簧效果

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

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

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

彈簧放開效果

彈簧效果

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

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

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

彈簧效果高彈力

彈簧效果無彈力

高硬度

低硬度極低硬度

請查看 DogItem() 可組合函式中的 DogHobby() 函式呼叫。根據 expanded 布林值,狗的興趣資訊會包含在組合中。視興趣資訊是否顯示而定,清單項目高度會隨之變更。目前的轉場效果相當劇烈。在本節中,您將使用 animateContentSize 修飾符,讓已展開和未展開狀態之間的轉場效果更順暢。

// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies,
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}
  1. MainActivity.ktDogItem() 中,將 modifier 參數新增至 Column 版面配置。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. 使用 animateContentSize 修飾符鏈結修飾符,為大小變更 (清單項目高度變更) 加上動畫效果。
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

在目前的實作項目中,您會為應用程式中的清單項目高度加上動畫效果。但因為動畫不明顯,執行應用程式時會難以辨識。如要解決這個問題,您可以使用選用的 animationSpec 參數自訂動畫。

  1. 在 Woof 中,動畫會平緩進出,不加上彈跳效果。為此,請在 animateContentSize() 函式呼叫中加入 animationSpec 參數。請使用 DampingRatioNoBouncy 將該參數設為無彈力的彈簧效果,並設定 StiffnessMedium 參數,增加一些彈簧硬度。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)
  1. 在「Design」窗格中查看 WoofPreview(),然後使用互動模式,或是在模擬器或裝置上執行應用程式,即可看到彈簧效果的實際效果。

c0d0a52463332875.gif

您成功了!歡迎使用具有動畫效果的精美應用程式。

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

animate*AsState

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

Compose 提供 FloatColorDpSizeOffsetInt 等的 animate*AsState() 函式。您可以使用接受泛型類型的 animateValueAsState(),輕鬆支援其他資料類型。

展開清單項目時,請嘗試使用 animateColorAsState() 函式變更顏色。

  1. DogItem() 中宣告顏色,然後將其初始化作業委派給 animateColorAsState() 函式。
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState()
   ...
}
  1. 設定 targetValue 具名參數,具體設定方式取決於 expanded 布林值。如果清單項目已展開,請將清單項目設為 tertiaryContainer 的顏色,否則請設為 primaryContainer 的顏色。
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState(
       targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
       else MaterialTheme.colorScheme.primaryContainer,
   )
   ...
}
  1. color 設為 Column 的背景修飾符。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. 查看清單項目展開時的顏色變化。未展開的清單項目為 primaryContainer 顏色,已展開的清單項目為 tertiaryContainer 顏色。

animateAsState 動畫

8. 取得解決方案程式碼

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

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

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

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

9. 結語

恭喜!您加入了可隱藏和顯示狗狗資訊的按鈕,還利用彈簧效果提升了使用者體驗。您也已瞭解如何在「Design」窗格中使用互動模式。

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

瞭解詳情