今天大概會聊到的範圍
- Animation
上一次有聊到,我們可以透過 Gesture 和 State 來與 user 互動。例如下面這個例子:
@Composable
fun ExpandText() {
var isExpanded by remember { mutableStateOf(false) } // <--- 1.
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxHeight()
) {
Row(modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { isExpanded = !isExpanded }, // <--- 2.
) {
Text("Expand")
}
}
Box {
Text(
text = contentText,
softWrap = true,
maxLines = if (isExpanded) Int.MAX_VALUE else 1, // <--- 3.
style = txtStyle,
modifier = Modifier
.background(color = Color.White)
)
}
}
}
ExpandText
是一個 Stateful 的 composable。在點擊2.
的按鈕後,會修改1.
的isExpanded
State。3.
這個Text
的maxLine
會依照isExpanded
的狀態調整
在點擊按鈕後,會直接修改 Text 的 maxLine
屬性。Text 也會再一次的 recompisition 後就調整大小。
若我們希望可以做的動態一點,我們可以加上 Animation。以這個例子,我們可以在變動大小的 Text 身上加上 animateContentSize
modifier。
@Composable
fun ExpandText() {
var isExpanded by remember { mutableStateOf(false) }
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxHeight()
) {
Row(modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { isExpanded = !isExpanded },
) {
Text("Expand")
}
}
Box {
Text(
text = contentText,
softWrap = true,
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = txtStyle,
modifier = Modifier
.background(color = Color.White)
.animateContentSize() // <--- 加上 animateContentSize
)
}
}
}
在研究 Compose 的 Animation 時,發現 compose 提供了許多 high-level 的 animation 用法(也就是較多預先包裝好、常用的情境 )。增加這些 high-level animation 的方法並不太統一,但大致可以分成三大類:
Modifier.animateContentSize
用法大致可以分成上述三類,但卻因為使用情境不同,而用到不同的 animator。官方文件上有一個不錯的說明,我將它簡化列在這邊:
此動畫是否有需要改動到元件內容(元件大小、顏色、內容 ... )?
AnimationVisibility
Modifier.animateContentSize
AnimatedContent
或 Crossfade
此動畫會連動某個狀態(例如某個數值、顏色 ...)?
rememberInfiniteTransition
取代 StateupdateTransition
取代 Stateanimate*AsState
取代 State其他情境
Animatable
來執行動畫animateContentSize
稍早已經有介紹過了,當元件的大小會變動時可以增加這個 modifier,在大小變動時會自動以動畫的方式變動。
AnimatedVisibility
, AnimatedContent
, Crossfade
這三者需要以 Composable 的形式包裝在要變動的元件之外。以 AnimatedVisibility
為例:
AnimatedVisibility(
visible = isExpanded
) {
Text(
text = contentText,
softWrap = true,
style = txtStyle,
modifier = Modifier
.background(color = Color.White)
)
}
除了提供 visible
依據的 State 外,還可以提供 enter
與 exit
兩個參數來設定進場與出場的動畫。
AnimatedContent
和 AnimatedVisibility
的用法類似,要再 parameter 中提供一個 state。在 lambda 中的 composable 若也透過對應的 state 進行改變的話,在變動的過程就會套上動畫。也和 AnimatedVisibility
類似,可以調整 transitionSpec
來改變動畫。
如果在改變內容時,只是希望進行簡單的 fadeIn + fadeOut,則可以用 Crossfade
來取代AnimatedContent
。
剛剛提到的,都是當 State A 改變到 State B 時,觸發一個動畫。動畫本身與 State A 和 B 的數值不相關。但有時候,我們需要以 State 作為動畫的數值,且需要將 State A ~ B 中間的間隔補齊。
舉個例,當我們需要改變一張卡片的顏色:
@Composable
fun AnimationScreen() {
var color by remember { mutableStateOf(Color.White) } // <--- 1.
val backgroundColor by animateColorAsState(targetValue = color) // <--- 2.
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxHeight()
) {
Row(modifier = Modifier.fillMaxWidth()) {
Button(onClick = { color = Color.Red }) { Text("Red") }
Button(onClick = { color = Color.Green }) { Text("Green") }
Button(onClick = { color = Color.Blue }) { Text("Blue") }
}
Text(
text = contentText,
softWrap = true,
style = txtStyle,
modifier = Modifier
.background(color = backgroundColor) // <--- 3.
)
}
}
animateXXXAsState
的 function 將這個 State 給包起來
Color
, Dp
, Size
, 以及標準數值,例如 Int
, Float
等不過我們的確要修改 content 的內容啊?那能不能用 AnimatedContent
來達到效果呢? 答案其實是可以的,只是 default 的行為可能不盡理想。
因為 AnimatedContent
會變動整個元件,但是 animateColorAsState
只會將那一個值 ( color ) 做逐步的變動。
@Composable
fun AnimationScreen() {
var color by remember { mutableStateOf(Color.White) }
val backgroundColor by animateColorAsState(targetValue = color)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxHeight()
) {
Row(modifier = Modifier.fillMaxWidth()) {
Button(onClick = { color = Color.Red }) { Text("Red") }
Button(onClick = { color = Color.Green }) { Text("Green") }
Button(onClick = { color = Color.Blue }) { Text("Blue") }
}
Text("animate with animateColorAsState",
style = titleStyle,
modifier = Modifier.background(color = Color.White))
Text(
text = contentText,
softWrap = true,
style = txtStyle,
modifier = Modifier
.background(color = backgroundColor)
)
Spacer(modifier = Modifier.size(16.dp))
Text("animate with AnimatedContent",
style = titleStyle,
modifier = Modifier.background(color = Color.White))
AnimatedContent(targetState = color) {
Text(
text = contentText,
softWrap = true,
style = txtStyle,
modifier = Modifier
.background(color = color)
)
}
}
}
最後,如果當某個 State 要變動時,需要改變多個物件的屬性的話。可以使用 updateTransition 將 State 包起來,在透過 updateTransition.animateXXX 來異動不同的屬性
@Composable
fun AnimationScreen() {
var isExpanded by remember { mutableStateOf(false) } // <-- 1. State
val transition = updateTransition(isExpanded) // <-- 2. 包成 traistion
// 3. 從 transition 中獨立出屬性
val color by transition.animateColor {
if(it) Color.Red else Color.Green
}
val maxLine by transition.animateInt {
if (it) Int.MAX_VALUE else 2
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxHeight()
) {
Row(modifier = Modifier.fillMaxWidth()) {
Button(onClick = { isExpanded = !isExpanded }) { Text("expand") }
}
Text(
text = contentText,
softWrap = true,
style = txtStyle,
maxLines = if (maxLine <= 0) 1 else maxLine, // <--- 使用從 transition 中獨立出的屬性
modifier = Modifier
.background(color = color) // <--
)
}
}
今天沒有提到更詳細的 Animatable,但在 High-Level 的 animation function 支持下,基本的互動與動畫就已經可以執行。 今天最後的 maxLine 還有點瑕疵,需要特別判斷當 MAX_VALUE ~ 2 時,會經過 0 的情況(同時動畫也會因此 delay,也許用 MAX_VALUE 是很糟點子) 。Animation 這邊,應該還有很多的東西可以挖出來。
Reference: