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:


<<:  day26 : k8s backup/restore/migrate with velero(上)

>>:  GitHub Project Board - 看板方法

Day 16 - CSS Text Shadow Mouse Move Effect

前言 JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用...

档案服务器架构设计备忘

档案服务器架构设计备忘.docx ...

[Day11] SSTI (Server Side Template Injection)

前言 系列完成3分之1了,今天来谈谈SSTI吧 正文 简介 SSTI,全称Server Side T...

007 2021线上看

007 2021线上看 世界局势波诡云谲,再度出山的邦德(丹尼尔·克雷格饰)面临有史以来空前的危机。...

Day2 用python写UI-聊聊tkinter的基本操作~

今天要介绍视窗设定,会分成三个部份来讲,建立视窗、设定视窗大小跟视窗的其他基本设定,那我们不多说就直...