iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 12

D12/ 我要怎麼用動畫改變中的資料? - Animations

今天大概會聊到的範圍

  • 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. 這個 TextmaxLine 會依照 isExpanded 的狀態調整

在點擊按鈕後,會直接修改 Text 的 maxLine 屬性。Text 也會再一次的 recompisition 後就調整大小。

附圖在 https://imgur.com/QeaTc2r

若我們希望可以做的動態一點,我們可以加上 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 
            )
        }        
    }
}

附圖在 https://imgur.com/Nz71o0V

不同情況用不同的方式加上 Animation

在研究 Compose 的 Animation 時,發現 compose 提供了許多 high-level 的 animation 用法(也就是較多預先包裝好、常用的情境 )。增加這些 high-level animation 的方法並不太統一,但大致可以分成三大類:

  • animator as modifier:
    • 這一類的 animator 可以直接透過 modifier 的方式加在某個 composable 身上。就如同上面舉例提到的 Modifier.animateContentSize
  • animator as composable
    • 這一類的 animator 需要將原先的 composable 用代表 animation 的 composable 包起來
  • animator as state
    • 這一類的 animator 是直接將 state 用 animator 包起來。在 animate 的過程會變動 state 來達成動畫的效果

用法大致可以分成上述三類,但卻因為使用情境不同,而用到不同的 animator。官方文件上有一個不錯的說明,我將它簡化列在這邊:

  • 此動畫是否有需要改動到元件內容(元件大小、顏色、內容 ... )?

    • 此動畫目標是元件的顯示 or 消失 ( 元件進場、離場 )? => 使用 AnimationVisibility
    • 此動畫會改變元件的大小? => 使用 Modifier.animateContentSize
    • 此動畫會讓元件內容變動? => 使用 AnimatedContentCrossfade
  • 此動畫會連動某個狀態(例如某個數值、顏色 ...)?

    • 此動畫是否會持續執行(不會停 ) => 用 rememberInfiniteTransition 取代 State
    • 此動畫需要一次改變多個參數 => 用 updateTransition 取代 State
    • 此動畫需要一次改變一個參數 => 用 animate*AsState 取代 State
  • 其他情境

    • 用 low-level 的 Animatable 來執行動畫

High-Level animator

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 外,還可以提供 enterexit 兩個參數來設定進場與出場的動畫。

AnimatedContentAnimatedVisibility 的用法類似,要再 parameter 中提供一個 state。在 lambda 中的 composable 若也透過對應的 state 進行改變的話,在變動的過程就會套上動畫。也和 AnimatedVisibility 類似,可以調整 transitionSpec 來改變動畫。

如果在改變內容時,只是希望進行簡單的 fadeIn + fadeOut,則可以用 Crossfade 來取代AnimatedContent

Animator as State

剛剛提到的,都是當 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.
        )
        
    }
}
  1. 首先我們一樣要建立一個 State
  2. 再來,我們需要將 State 用 animateXXXAsState 的 function 將這個 State 給包起來
    • 目前 Compose 有提供常用的數值。例如 Color, Dp, Size, 以及標準數值,例如 Int, Float
  3. 在數值需求的地方,提供 animtate 包起來的 State

附圖在 https://imgur.com/5UArR3c

不過我們的確要修改 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)
            )
        }
        
    }
}

附圖在 https://imgur.com/q58oO4Q

最後,如果當某個 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:


上一篇
D11/ 要怎麼顯示動態資料的畫面 - State
下一篇
D13/ 怎麼做翻卡片的動畫 - Animation Part 2 & GraphicsLayer
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30

尚未有邦友留言

立即登入留言