1. 事前準備
在這個程式碼研究室中,您將瞭解如何在 Android 應用程式中加入簡單的動畫。動畫可為應用程式增添互動性和趣味,並讓使用者更容易理解。在提供許多資訊的畫面中以動畫呈現個別最新資訊,可協助使用者瞭解變更的內容。
在應用程式使用者介面中,可以使用多種類型的動畫。項目能以淡入或淡出的方式顯示或消失,也能移入或移出畫面,或以有趣的方式轉換。這都可以讓應用程式的 UI 更生動易懂。
動畫還能讓應用程式看起來更精緻、提升應用程式的外觀與風格,同時為使用者帶來助益。
必要條件
- 對 Kotlin 的瞭解,包括函式、lambda 和無狀態可組合項。
- 對如何在 Jetpack Compose 中建構版面配置有基本瞭解。
- 對如何在 Jetpack Compose 建立清單有基本瞭解。
- 對 Material Design 有基本瞭解。
課程內容
- 如何使用 Jetpack Compose 建構簡易的彈簧效果。
建構項目
- 您將在 Jetpack Compose 程式碼研究室利用 Material Design 主題設定建構 Woof 應用程式,並加入簡單的動畫來確認使用者的動作。
軟硬體需求
- 最新的 Android Studio 穩定版。
- 連上網際網路,可下載範例程式碼。
2. 應用程式總覽
在程式碼研究室「使用 Jetpack Compose 進行 Material Design 主題設定」中,您已使用 Material Design 建立 Woof 應用程式,並顯示犬隻及其資訊的清單。
在本程式碼研究室中,您將在 Woof 應用程式中加入動畫,並新增可在展開清單項目時顯示的興趣資訊。您還要加入彈簧效果,以動畫呈現清單項目展開的樣子:
取得範例程式碼
如要開始使用,請先下載範例程式碼:
或者,您也可以複製 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. 新增「顯示更多內容」圖示
在本節中,您將在應用程式新增「顯示更多內容」圖示 和「顯示較少內容」 圖示。
圖示
圖示是一種符號,可透過視覺呈現的方式協助使用者瞭解使用者介面的預定功能,而且通常會以使用者預期在實體世界遇到的物體為靈感。圖示設計往往會將詳細資料精細程度降至供使用者熟悉所需的最低程度。舉例來說,實體世界中的鉛筆代表寫字,因此對應的圖示通常表示建立或編輯。
相片來源:Angelina Litvin 發表於 Unsplash 網站上 |
Material Design 提供多種圖示,並依常見類別排列,方便您視需求選擇使用。
新增 Gradle 依附元件
在專案中加入 material-icons-extended
程式庫依附元件。您將使用此程式庫中的 Icons.Filled.ExpandLess
和 Icons.Filled.ExpandMore
圖示。
- 在「Project」窗格中,依序開啟「Gradle Scripts」>「build.gradle.kts (Module :app)」。
- 捲動至
build.gradle.kts (Module :app)
檔案的結尾。在dependencies{}
區塊中,加入以下這行程式碼:
implementation("androidx.compose.material:material-icons-extended")
新增圖示可組合項
請新增函式,顯示來自 Material Design 圖示庫的「顯示更多內容」圖示,並設為按鈕。
- 在
MainActivity.kt
中的DogItem()
函式後,建立名為DogItemButton()
的新可組合函式。 - 為展開狀態傳入
Boolean
、為 onClick 處理常式傳入 lambda 運算式,並視需要傳入Modifier
,如下所示:
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
}
- 在
DogItemButton()
函式中,新增接受onClick
具名參數的IconButton()
可組合函式、使用結尾 lambda 語法的 lambda (會在按下圖示時叫用),並視需要新增modifier
。請將IconButton's onClick
和modifier value parameters
設為等於傳入DogItemButton
的項目。
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
){
IconButton(
onClick = onClick,
modifier = modifier
) {
}
}
- 在
IconButton()
lambda 區塊中,加入Icon
可組合函式,並將imageVector value-parameter
設為Icons.Filled.ExpandMore
。這會顯示在清單項目末端 。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
)
}
- 加入價值參數
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()
可組合函式,即可顯示該函式。
- 在
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) }
- 在清單項目中顯示圖示按鈕。請在
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*/ }
)
}
- 在「Design」窗格中查看
WoofPreview()
。
請注意,「顯示更多內容」按鈕不會對齊清單項目的末端。您將在下一個步驟予以修正。
對齊「顯示更多內容」按鈕
如要將「顯示更多內容」按鈕與清單項目末端對齊,您需要在版面配置中使用 Modifier.weight()
屬性加入空格字元。
在 Woof 應用程式中,每個清單項目列都有狗的圖片和資訊,以及「顯示更多內容」按鈕。您將在「顯示更多內容」按鈕前方使用 1f
權重加入 Spacer
可組合函式,適當對齊按鈕圖示。由於空格字元是列中唯一的加權子項元素,因此在測量其他未加權子項元素的寬度之後,空格字元就會填滿列中其餘空間。
在清單項目列中加入空格字元
- 在
DogItem()
的DogInformation()
和DogItemButton()
之間,新增Spacer
。使用weight(1f)
傳入Modifier
。Modifier.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*/ }
)
}
- 在「Design」窗格中查看
WoofPreview()
。請注意,「顯示更多內容」按鈕現已對齊清單項目末端。
4. 新增顯示興趣的可組合函式
在這項工作中,您將加入 Text
可組合函式,顯示狗的興趣資訊。
- 建立名為
DogHobby()
的新可組合函式,用來接收狗的興趣字串資源 ID 和選用的Modifier
。
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
) {
}
- 在
DogHobby()
函式中建立Column
,並傳入傳遞至DogHobby()
的修飾符。
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
){
Column(
modifier = modifier
) {
}
}
- 在
Column
區塊加入兩個Text
可組合函式,一個在興趣資訊上方顯示「About」文字,另一個則顯示興趣資訊。
在「strings.xml」檔案中,將第一個函式的 text
設為 about
,並將 style
設為 labelSmall
。然後將第二個函式的 text
設為傳入的 dogHobby
,style
則設為 bodyLarge
。
Column(
modifier = modifier
) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.labelSmall
)
Text(
text = stringResource(dogHobby),
style = MaterialTheme.typography.bodyLarge
)
}
- 在
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*/ }
)
}
}
- 在
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)
)
)
}
}
}
- 在「Design」窗格中查看
WoofPreview()
。您會看到畫面上顯示狗的興趣。
5. 在點選按鈕時顯示或隱藏興趣
應用程式的每個清單項目都有一個「顯示更多內容」按鈕,但這個按鈕目前沒有作用!在本節中,您將加入選項,讓系統在使用者點選「顯示更多內容」按鈕時隱藏或顯示興趣資訊。
- 在
DogItem()
可組合函式中,請於DogItemButton()
函式呼叫中定義onClick()
lambda 運算式,將使用者點選按鈕時的expanded
布林狀態值變更為true
,並將再次點選按鈕時的布林狀態值變更為false
。
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded }
)
- 在
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
時,系統才會顯示狗的興趣資訊。
- 預覽畫面可以顯示 UI 的外觀,您也可以與預覽畫面互動。如要與 UI 預覽畫面互動,請將滑鼠游標懸停在「Design」窗格的 WoofPreview 文字上方,然後點選「Design」窗格右上角的「Interactive Mode」按鈕 。系統就會在互動模式中啟動預覽。
- 按一下「顯示更多內容」按鈕,就可以與預覽畫面互動。請注意,系統會先隱藏狗的興趣資訊,在點選「顯示更多內容」按鈕後才顯示出來。
請注意,展開清單項目時,「顯示更多內容」按鈕圖示不會改變。為了提升使用者體驗,您將變更圖示,讓 ExpandMore
顯示向下箭頭 ,ExpandLess
則顯示向上箭頭 。
- 在
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
陳述式只有一行程式碼,則不一定要使用大括號。
- 在裝置或模擬器上執行應用程式,或在預覽中再次使用互動模式。請注意,系統會切換顯示
ExpandMore
和ExpandLess
圖示。
圖示更新成功!
展開清單項目時,有發現高度突然改變了嗎?高度會突然改變,就表示應用程式設計不完善。為解決這個問題,接下來您將在應用程式中加入動畫。
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)
)
)
}
}
- 在
MainActivity.kt
的DogItem()
中,將modifier
參數新增至Column
版面配置。
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
){
...
}
}
}
- 使用
animateContentSize
修飾符鏈結修飾符,為大小變更 (清單項目高度變更) 加上動畫效果。
import androidx.compose.animation.animateContentSize
Column(
modifier = Modifier
.animateContentSize()
)
在目前的實作項目中,您會為應用程式中的清單項目高度加上動畫效果。但因為動畫不明顯,執行應用程式時會難以辨識。如要解決這個問題,您可以使用選用的 animationSpec
參數自訂動畫。
- 在 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
)
)
)
- 在「Design」窗格中查看
WoofPreview()
,然後使用互動模式,或是在模擬器或裝置上執行應用程式,即可看到彈簧效果的實際效果。
您成功了!歡迎使用具有動畫效果的精美應用程式。
7. (選用) 嘗試使用其他動畫
animate*AsState
animate*AsState()
函式是 Compose 中最簡單的動畫 API 之一,可用於建立單一值。您只需提供結束值 (或目標值),API 就會從目前的值開始動畫,直到指定的結束值為止。
Compose 提供 Float
、Color
、Dp
、Size
、Offset
和 Int
等的 animate*AsState()
函式。您可以使用接受泛型類型的 animateValueAsState()
,輕鬆支援其他資料類型。
展開清單項目時,請嘗試使用 animateColorAsState()
函式變更顏色。
- 在
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()
...
}
- 設定
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,
)
...
}
- 將
color
設為Column
的背景修飾符。
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
.animateContentSize(
...
)
)
.background(color = color)
) {...}
}
- 查看清單項目展開時的顏色變化。未展開的清單項目為
primaryContainer
顏色,已展開的清單項目為tertiaryContainer
顏色。
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,透過社群媒體分享您的作品!
瞭解詳情
- Jetpack Compose 動畫
- 程式碼研究室:Jetpack Compose 的動畫元素
- 影片:重新詮釋動畫
- 影片:Jetpack Compose:動畫