Jetpack Compose(6)——動畫

来源:https://www.cnblogs.com/joy99/p/18160974
-Advertisement-
Play Games

目錄一、低級別動畫 API1.1 animate*AsState1.2 Animatable1.3 Transition 動畫1.3.1 updateTransition1.3.2 createChildTransition1.3.3 封裝並復用 Transition 動畫1.4 remeberIn ...


目錄

本文介紹 Jetpack Compose 動畫。
官方文檔
關於動畫這塊,第一次看官網,覺得內容很雜,很難把握住整個框架結構,很難去對動畫進行分類。參考了很多文獻資料,大多數都是從高級別 API 開始講解,包括官網也是如此。我發現這樣不太容易理解,因為高級別 API 中可能會涉及到低級別 API 中的一些方法,術語等。所以本文從低級別 API 講起。

一、低級別動畫 API

1.1 animate*AsState

animate*AsState 函數是 Compose 動畫中最常用的低級別 API 之一,它類似於傳統 View 中的屬性動畫,你只需要提供結束值(或者目標值),API 就會從當前值到目標值開始動畫。
看一個改變 Composable 組件大小的例子:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    // I'm here
    val boxSize by animateDpAsState(targetValue = if (bigBox) 200.dp else 50.dp, label = "")
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(boxSize) // I'm here
        .clickable {
            bigBox = !bigBox
        })
}

運行一下看看效果:

上述示例中我們使用了 animateDpAsState 這個函數,定義了一個 “Dp” 相關的動畫。
其實 animate*AsState 並不是只某一個具體方法,而是只形如 animate*AsState 的一系列方法,具體如下:

是的,你沒有看錯,甚至可以使用 animateColorAsState 方法對顏色做動畫。
聰明的你,肯定會有一個疑問,這個方法是從當前值到設定的目標值啟動動畫,但是動畫具體執行過程是怎樣的,比如持續時間等等,這個有辦法控制嗎?還是以 AnimateDpAsState 為例,看看這個參數的完整簽名:

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}

實際上這個方法有4個參數。

  • targetValue 是沒有預設值的,表示動畫的目標值。
  • animationSpec 動畫規格,這裡有一個預設值,實際上就是這個參數決定了動畫的執行邏輯。
  • lable 這個參數是為了區別在 Android Studio 中進行動畫預覽時,區別其它動畫的。
  • finishedListener 可以用來監聽動畫的結束。

關於動畫規格 AnimationSpec, 此處不展開,後面會詳細講解。

再延伸一點,看看該方法的實現,實際上是調用了 animateValueAsState 方法。事實上前面展示的 animate*AsState 的系列方法都是調用的 animateValueAsState

看看源碼:

@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember { spring() },
    visibilityThreshold: T? = null,
    label: String = "ValueAnimation",
    finishedListener: ((T) -> Unit)? = null
): State<T> {

    val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
    val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
    val listener by rememberUpdatedState(finishedListener)
    val animSpec: AnimationSpec<T> by rememberUpdatedState(
        animationSpec.run {
            if (visibilityThreshold != null && this is SpringSpec &&
                this.visibilityThreshold != visibilityThreshold
            ) {
                spring(dampingRatio, stiffness, visibilityThreshold)
            } else {
                this
            }
        }
    )
    val channel = remember { Channel<T>(Channel.CONFLATED) }
    SideEffect {
        channel.trySend(targetValue)
    }
    LaunchedEffect(channel) {
        for (target in channel) {
            // This additional poll is needed because when the channel suspends on receive and
            // two values are produced before consumers' dispatcher resumes, only the first value
            // will be received.
            // It may not be an issue elsewhere, but in animation we want to avoid being one
            // frame late.
            val newTarget = channel.tryReceive().getOrNull() ?: target
            launch {
                if (newTarget != animatable.targetValue) {
                    animatable.animateTo(newTarget, animSpec)
                    listener?.invoke(animatable.value)
                }
            }
        }
    }
    return toolingOverride.value ?: animatable.asState()
}

稍微看下源碼,大致能發現,實際上是啟動了一個協程,然後協程內部不斷調用了 animatable.animateTo() 這樣一個方法。下一節講 animatable, 收回來,這裡我想要表達的意思是,使用接受通用類型的 animateValueAsState() 可以輕鬆添加對其他數據類型的支持只需要自行實現一個 TwoWayConverter。具體如何實現,下文第四節會詳細講解。

1.2 Animatable

前面的 animate*AsState 只需要指定目標值,無需指定初始值,而 Animatable 則是一個能夠指定初始值的更基礎的 API。 animate*AsState 調用了AnimateValueAsState, 而 AnimateValueAsState 內部使用 Animatable 定製完成。

對於 Animatable 而言,動畫數值更新需要在協程中完成,也就是調用 animateTo 方法。此時我們需要確保 Animatable 的初始狀態與 LaunchedEffect 代碼塊首次執行時狀態保持一致。

接下來,我們使用 animatable 實現前面的例子。

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val customSize = remember {
        Animatable(50.dp, Dp.VectorConverter)
    }
    
    LaunchedEffect(key1 = bigBox) {
        customSize.animateTo(if (bigBox) 200.dp else 50.dp)
    }

    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize.value)
        .clickable {
            bigBox = !bigBox
        })
}

值得註意的是,當我們在 Composable 使用 Animatable 時,其必須包裹在 rememebr 中,如果你沒有這麼做,編譯器會貼心的提示你添加 rememeber 。

同樣,與 animate*AsState 一樣, animateTo 方法接收 AnimationSpec 參數用來指定動畫的規格。

suspend fun animateTo(
        targetValue: T,
        animationSpec: AnimationSpec<T> = defaultSpringSpec,
        initialVelocity: T = velocity,
        block: (Animatable<T, V>.() -> Unit)? = null
    )

再看看增加一個顏色變化的動畫:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val customSize = remember {
        androidx.compose.animation.core.Animatable(50.dp, Dp.VectorConverter)
    }

    val customColor = remember {
        androidx.compose.animation.Animatable(Color.Blue)
    }

    LaunchedEffect(key1 = bigBox) {
        customSize.animateTo(if (bigBox) 200.dp else 50.dp)
        customColor.animateTo(if (bigBox) Color.Red else Color.Blue)
    }

    Box(modifier = Modifier
        .background(customColor.value)
        .size(customSize.value)
        .clickable {
            bigBox = !bigBox
        })
}

我們看到,前面使用 Dp 類型時,我們使用的是 androidx.compose.animation.core.Animatable.kt 文件中的方法, 此時使用的是 androidx.compose.animation.SingleValueAnimation.kt 文件中的方法, 且沒有傳入 TwoWayConverter 參數。這裡說明一下:對與 ColorFloat 類型,Compose 已經進行了封裝,不需要我們傳入 TwoWayConverter 參數,對於其它的常用數據類型,Compose 也提供了對應的 TwoWayConverter 實現方法。比如 Dp.VectorConverter, 直接傳入即可。

另外 Launched 會在 onAtive 時執行,此時要確保, animateTo 的 targetValue 與 Animatable 的預設值相同。否則在頁面首次渲染時,便會發生動畫,可能與預期結果不相符。

最後我們看一下執行效果:

可以看到,實際上 size 和 Color 並不是同時執行的,而是先執行 size 的動畫, 後執行 Color 的動畫。我們做如下修改,讓兩個動畫併發執行。

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val customSize = remember {
        androidx.compose.animation.core.Animatable(50.dp, Dp.VectorConverter)
    }

    val customColor = remember {
        androidx.compose.animation.Animatable(Color.Blue)
    }

    LaunchedEffect(key1 = bigBox) {
        launch{
            customSize.animateTo(if (bigBox) 200.dp else 50.dp)
        }
        launch {
            customColor.animateTo(if (bigBox) Color.Red else Color.Blue)
        }
    }

    Box(modifier = Modifier
        .background(customColor.value)
        .size(customSize.value)
        .clickable {
            bigBox = !bigBox
        })
}

此時的執行效果,如下:

對比看還是能看到其中差距,如果感覺不明顯,可以在開發者模式裡面將動畫執行時間調整為 5 倍,則能更清晰地觀察動畫執行過程。這樣看起來,size 和 color 動畫是在非同步併發執行。

1.3 Transition 動畫

Animate*AsState 和 Animatable 都是針對單個目標值的動畫,而 Transition 可以面向多個目標值應用動畫,並保持它們同步結束。這聽起來是不是類似傳統 View 中的 AnimationSet ?

1.3.1 updateTransition

我們可以使用 updateTransition 創建一個 Transition 動畫,還是先上代碼,看看 updateTransition 的用法:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val transition = updateTransition(targetState = bigBox, label = "")
    val customSize by transition.animateDp(label = "") {
        when(it) {
            true -> 200.dp
            false -> 50.dp
        }
    }

    val customColor by transition.animateColor(label = "") {
        when(it) {
            true -> Color.Red
            false -> Color.Blue
        }
    }

    Box(modifier = Modifier
        .background(customColor)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}

Transition 也需要依賴狀態執行,需要枚舉出所有可能的狀態。然後基於這個狀態,通過 updateTransition 創建了一個 Transition 對象,然後使用 transiton 動畫,依次創建出 Size 和 Color。效果如下:

咋一看,怎麼感覺跟前面實現的 Size 和 Color 的調整後的這個動畫是一樣的?這裡可以這樣理解,Transition 動畫是其依賴的狀態變化後,會同步改變由它創建出來的其它多個屬性,能保證各個屬性同時變化,動畫真正的同時啟動,並同時結束。

1.3.2 createChildTransition

通過 createChildTransition 可以將一種類型的 Transition 轉換為其它的 Transition。
舉例:

var bigBox by remember {
    mutableStateOf(false)
}

val transition = updateTransition(targetState = bigBox, label = "")

val sizeTransition = transition.createChildTransition(label = "") {
    when(it) {
        true -> 200.dp
        false -> 50.dp
    }
}


val colorTransition = transition.createChildTransition(label = "") {
    when(it) {
        true -> Color.Red
        false -> Color.Blue
    }
}

將一個 Transition 類型轉換為了 Transition 和 Transition, 實際使用中,子動畫的動畫數值來自於父動畫,某種程度上說,createChildTransition 更像是一種 map 操作。

1.3.3 封裝並復用 Transition 動畫

使用 updateTransition 方法操作動畫,沒有問題,現在假設某個動畫效果很複雜,我們不希望每次用的時候都去重新實現一遍,我們希望將上述動畫效果封裝起來,並可以復用。如何做呢?還是以上面的動畫效果為例.
首先把動畫涉及到的屬性做一個封裝:

class TransitionData(
    size: State<Dp>,
    color: State<Color>
) {
    val size by size
    val color by color
}

然後定義動畫,並返回對應的值:

@Composable
fun ChangeBoxSizeAndColor(bigBox: Boolean): TransitionData {
    val transition = updateTransition(targetState = bigBox, label = "")
    val size = transition.animateDp(label = "") {
        when(it) {
            true -> 200.dp
            false -> 50.dp
        }
    }
    val color = transition.animateColor(label = "") {
        when(it) {
            true -> Color.Red
            false -> Color.Blue
        }
    }
    return remember (transition) {
        TransitionData(size, color)
    }
}

最後使用封裝的動畫:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val sizeAndColor = ChangeBoxSizeAndColor(bigBox)
    Box(modifier = Modifier
        .background(sizeAndColor.color)
        .size(sizeAndColor.size)
        .clickable {
            bigBox = !bigBox
        })
}

執行結果和前面一致。這裡就不在貼圖了。

1.4 remeberInfiniteTransition —— 無限迴圈的 transition 動畫

顧名思義,remeberInfiniteTransition 就是一個無限迴圈的 transition 動畫。一旦動畫開始便會無限迴圈下去,直到 Composable 進入 onDispose。
看下用法:

@Composable
fun Demo() {
    val infiniteTransition = rememberInfiniteTransition(label = "")
    val color by infiniteTransition.animateColor(
        initialValue = Color.Blue,
        targetValue = Color.Red,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        ),
        label = ""
    )
    val size by infiniteTransition.animateValue(
        initialValue = 50.dp,
        targetValue = 200.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        ),
        label = ""
    )

    Box(modifier = Modifier
        .background(color)
        .size(size)
    )
}

效果如下:

首先使用 remeberInfiniteTransition 創建一個 InfiniteTransition 對象,然後通過該對象,創建出具體的要進行動畫的屬性,這裡使用到了 animationSpec 動畫規格,關於動畫規格,不著急,下文馬上講解。需要註意到的是,infiniteTransition.animateColorinfiniteTransition.animateFloat 方法是不需要傳入 typeConverter 參數的,其它類型,我們需要實現 TwoWayConverter。

1.5 小結

關於 Compose 低級別的動畫 API ,我們介紹差不多了,主要是 animtion*AsStateAnimatableTransition,比如上面的例子中,我們用三種動畫都實現了相同的效果。那這三者我們到底應該怎麼理解?

首先 animtion*AsState 底層實現實際上就是使用的 Animatable,我們可以把 animtion*AsState 理解為 Animatable 的一種更簡便更直接的用法。這兩個實際上可以歸為同一類。
而 Transition 的核心思想與 Animatable 不一樣。Animatable 的核心思想是面向值的,在多個動畫,多個狀態的情況下,存在不方便管理的問題。比如針對 size 和 color,我們需要創建出兩個 Animatable 對象,並且需要啟動兩個協程,如果有更多的還要同時執行更多的動畫,則會更複雜。Transition 的核心思想是面向狀態的,如果多個動畫依賴一個共同的狀態,則可以做到統一管理。updateTransition 只會創建一次協程,根據一種狀態的變化,控制不同的動畫效果。很明顯,transition 在代碼結構上以及邏輯上更清晰。

另外,Transition 還支持一個非常牛叉的功能:支持 Compose 動畫預覽調節!!!

二、Android Studio 對 Compose 動畫調試的支持

為了方便後面在體驗不同動畫規格時,感受效果差異,在講解動畫規格之前,這裡插入一節,將一下 Android Studio 對與 Compose 動畫的預覽調試,這個是真正的生產力。我們不需要在編譯 apk 在真機或者模擬器上運行,就可以預覽動畫效果,這個是不是大大提高了效率。怎麼用?—— @Preview 註解。
首先,不論是 Animatable 動畫還是 Transition 動畫, Android Studio 都支持預覽功能。
還是上面的例子:

@Preview
@Composable
fun Demo1() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val customSize = remember {
        androidx.compose.animation.core.Animatable(50.dp, Dp.VectorConverter, label = "demo1Size")
    }

    val customColor = remember {
        androidx.compose.animation.Animatable(Color.Blue)
    }

    LaunchedEffect(key1 = bigBox) {
        launch {
            customSize.animateTo(if (bigBox) 200.dp else 50.dp)
        }
        launch {
            customColor.animateTo(if (bigBox) Color.Red else Color.Blue)
        }
    }

    Box(modifier = Modifier
        .background(customColor.value)
        .size(customSize.value)
        .clickable {
            bigBox = !bigBox
        })
}

@Preview
@Composable
fun Demo2() {
    var bigBox by remember {
        mutableStateOf(false)
    }

    val transition = updateTransition(targetState = bigBox, label = "demo2SizeAndColor")
    val customSize by transition.animateDp(label = "demo2Size") {
        when(it) {
            true -> 200.dp
            false -> 50.dp
        }
    }

    val customColor by transition.animateColor(label = "demo2Color") {
        when(it) {
            true -> Color.Red
            false -> Color.Blue
        }
    }

    Box(modifier = Modifier
        .background(customColor)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}
  1. 給需要預覽調節的 Composable 加上 @Preview 註解。這裡我們還對動畫函數中的 label 參數重新賦值了。這個 label 參數就是方便預覽調節的。
  2. 進入 Android Studio 預覽模式

然後將滑鼠移動到對應的 Composable 名稱上,可以看到有多種模式:

一般會有如下預覽模式:
start UI check mode 用來檢查 UI 狀態,比如橫豎屏,暗黑模式,亮色模式,rtl 佈局,不同尺寸的設備,不同主題等 UI 適配問題,非常方便。
run preview 是在真機或者模擬器中運行當前的 Composable,進行預覽,切合實際場景。
start interact mode 啟動交互模式,顧名思義,比如點擊、長按,拖拽等等事件交互進行預覽,當然動畫也是一種交互形式,可以在這個模式下進行動畫預覽。
start animation preview 啟動動畫預覽模式,這個就是上一節提到的,Transition 動畫特有的預覽調節模式。

我們可以看到只有 Demo2 有 start animation preview 選項。啟動該模式:

①區域,可以播放動畫,是否迴圈,播放速度,跳轉到動畫起始位置或者結束位置。
②區域,可以設置動畫依賴的狀態變化。
③區域,可以展開或者收起具體的動畫,展開後,④區域會顯示當前具體每個動畫執行過程,前面個動畫設置 label, 就是用作在此做區別的。並且有一個時間軸,可以自行調節。

雖然 animate*AsState 也支持添加 label,但是該類型動畫只支持預覽,不支持調節。但即便如此,是不是比傳統 View 方便了一萬倍。更多預覽調試功能大家可以自行探索。

三、AnimationSpec 動畫規格

終於講到動畫規格了。動畫規格實際上就是控制動畫如何執行的。前面出現的代碼中多次提到 animationSpec 參數,大多數 Compose 動畫 API 都支持設置 animationSpec 參數定義動畫效果。前面我們沒有傳入這個參數,是因為使用到的 API 該參數都有一個預設值。
比如:

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}

animateDpAsState 方法對於 animationSpec 參數賦了預設值 dpDefaultSpring。我們先看看這個 AnimationSpec 定義:

interface AnimationSpec<T> {
    fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedAnimationSpec<V>
}

AnimationSpec 是一個介面,只有一個方法,泛型 T 是當前動畫的數值類型,vectorize 方法用來創建一個 VectorizedAnimationSpec,這是一個矢量動畫的配置。AnimationVector 其實就是一個函數,用來參與計算動畫矢量,TwoWayConverter 用來將 T 類型轉換成參與動畫計算的矢量數據。
AnimationSpec 的實現類如下:

下麵按照類別介紹:

3.1 SpringSpec 彈跳動畫

Spring 彈性動畫。可以使用 spring() 方法進行創建。

@Stable
fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
): SpringSpec<T> =
    SpringSpec(dampingRatio, stiffness, visibilityThreshold)

spring 方法接收三個參數,都有預設值。

  1. dampingRatio: 彈簧的阻尼比,阻尼比可以定義震動從一次彈跳到下一次彈跳所衰減的速度有多快。當阻尼比 < 1 時,阻尼比越小,彈簧越有彈性,預設值為 Spring.DampingRatioNoBouncy = 1f
  • 當 dampingRatio > 1 時會出現過阻尼現象,這會使彈簧快速地返回到靜止狀態。
  • 當 dampingRatio = 1 時,沒有彈性的阻尼比,會使得彈簧在最短的時間內返回到靜止狀態。
  • 當 0 < dampingRatio < 1 時, 彈簧會圍繞最終靜止為止多次反覆振動。
    註意 dampingRatio 不能小於0。
    Compose 為 spring 提供了一組常用的阻尼比常量。
const val DampingRatioHighBouncy = 0.2f

const val DampingRatioMediumBouncy = 0.5f

const val DampingRatioLowBouncy = 0.75f

const val DampingRatioNoBouncy = 1f
  1. stiffness: 彈簧的剛度,剛度值越大,彈簧到靜止的速度就越快。預設值為Spring.StiffnessMedium = 1500f
    註意 stiffness 必須大於 0 。
    Compose 為 spring 提供了一組常量值:
const val StiffnessHigh = 10_000f

const val StiffnessMedium = 1500f

const val StiffnessMediumLow = 400f

const val StiffnessLow = 200f

const val StiffnessVeryLow = 50f
  1. visibilityThreshold: 可見性閾值。這個參數是一個泛型,次泛型與 targetValue 的類型保持一致,又開發者指定一個閾值,當動畫達到這個閾值時,動畫會立即停止。預設值為 null。

示例:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val customSize by animateDpAsState(
        targetValue = if (bigBox) 200.dp else 50.dp,
        animationSpec = spring(dampingRatio = -0.2f),
        label = ""
    )
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })

}

這個 Gif 錄製效果著實不太好,大家可以試一下,實際效果很明顯,像彈框一樣反覆振動直到靜止。

3.2 TweenSpec 補間動畫

TweenSpec 是 DurationBasedAnimationSpec 的子類。TweenSpec 的動畫必須在規定的時間內完成,它的動畫效果是基於時間參數計算的,可以使用 Easing 來指定不同的時間曲線動畫效果。可以使用 tween() 方法進行創建。

@Stable
fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)

tween 方法接收三個參數:

  1. durationMillis: 動畫的持續時間,預設值 300ms。
  2. delayMillis: 動畫延遲時間,預設值 0, 即立即執行。
  3. easing: 動畫曲線變化,預設值為 FastOutSlowInEasing。

Easing

下麵介紹一下 Easing,

@Stable
fun interface Easing {
    fun transform(fraction: Float): Float
}

Easing 是一個介面,只有一個方法, transform 用來計算變化曲線,允許過度元素加速或者減速,而不是以恆定的速度變化。參數 fraction 是一個 [0.0f, 1.0f] 區間的值。其中 0.0f 表示 開始,1.0f 表示結束。
Compose 給出了 Easing 的兩個實現類:

PathEasing

class PathEasing(path: Path) : Easing

PathEasing 需要創建一個 Path 對象傳入。關於 Path 的使用,和 View 中的 Path API 有一些類似,具體可以查看源碼,需要註意的是這個 Path 必須從 (0,0) 開始,到 (1, 1) 結束,並且 x 方向上不得有間隙,也不得自行迴圈,避免有兩個點共用相同的 x 坐標。

CubicBezierEasing

class CubicBezierEasing(
    private val a: Float,
    private val b: Float,
    private val c: Float,
    private val d: Float
) : Easing

實際開發中,使用更多的是三階貝塞爾曲線。a、b, 是第一個控制的 x 坐標和 y 坐標,c、d 是第二個控制點的 x 坐標和 y 坐標。
關於三階貝塞爾曲線的知識,本文就展開了,如不清楚的可以查閱相關資料。Compose 提供了一些常用的實現。

val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)

val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)

val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)

如需自定義控制點坐標,可以到一些線上網站上預覽曲線變化效果。比如:

LinearEasing

除此之外,還有一個線性變化曲線。

val LinearEasing: Easing = Easing { fraction -> fraction }

自定義 Easing

自定義 Easing 實際上就是自己顯示 Easing 介面。
例如:

val CustomEasing = Easing { fraction -> fraction * fraction }

3.3 KeyframesSpec 關鍵幀動畫

KeyframesSpec 也是 DurationBasedAnimationSpec,基於時間的動畫規格,在不同的時間戳定義值,更精細地來實現關鍵幀的動畫。可以使用 keyframes() 方法來創建 KeyframesSpec。

@Stable
fun <T> keyframes(
    init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
): KeyframesSpec<T> {
    return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init))
}

keyframes 方法只有一個參數。直接看用法:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val customSize by animateDpAsState(
        targetValue = if (bigBox) 200.dp else 50.dp,
        animationSpec = keyframes {
            durationMillis = 10000
            50.dp at 0 with LinearEasing
            100.dp at 1000 with FastOutLinearInEasing
            150.dp at 9000 with LinearEasing
            200.dp at 10000
        },
        label = ""
    )
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}

解釋:指定在什麼時間,值應該是多少,是以怎樣的曲線變化到該值。

註意,這個示例有個奇怪的點,由小變到大是符合邏輯的,而由大變大小,這個效果會顯得很奇怪。

3.4 SnapSpec 跳切動畫

SnapSpec 表示跳切動畫,它立即將動畫值捕捉到最終值。它的 targetValue 發生變化時,當前值會立即更新為 targetValue, 沒有中間過渡,動畫會瞬間完成,常用於跳過過場動畫的場景。使用 snap() 方法創建。

@Stable
fun <T> snap(delayMillis: Int = 0) = SnapSpec<T>(delayMillis)

接收一個參數,表示延遲多久後執行。

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val customSize by animateDpAsState(
        targetValue = if (bigBox) 200.dp else 50.dp,
        animationSpec = snap(),
        label = ""
    )
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}

3.5 RepeatableSpec 迴圈動畫

使用 repeatable() 方法可以創建一個 RepeatableSpec 示例,前面介紹的動畫規格都是單次執行的動畫,而 RepeatableSpec 是一個可循播放的動畫。

@Stable
fun <T> repeatable(
    iterations: Int,
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode = RepeatMode.Restart,
    initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
    RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)

repeatable 有四個參數:

  1. iterations: 迴圈次數,理論上應該大於 1 ,等於 1 表示不迴圈。那也就沒有必要使用 RepeatableSpec 了。
  2. animation: 該參數是一個 DurationBasedAnimationSpec 類型。可以使用
    TweenSpecKeyframesSpec SnapSpec 。SpringSpec 不支持迴圈播放,這個可以理解,迴圈的彈性,違背物理定律。
  3. repeatMode: 重覆模式,枚舉類型。
enum class RepeatMode {
    Restart,

    Reverse
}

示例:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val customSize by animateDpAsState(
        targetValue = if (bigBox) 200.dp else 50.dp,
        animationSpec = repeatable(
            iterations = 10,
            animation = tween()
        ),
        label = ""
    )
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}
  1. initialStartOffset: 動畫開始的偏移。可用於延遲動畫的開始或將動畫快進到給定的播放時間。此起始偏移量不會重覆,而動畫中的延遲(如果有)將重覆。 預設情況下,偏移量為 0。

3.6 InfiniteRepeatableSpec 無限迴圈動畫

InfiniteRepeatableSpec 表示無限迴圈的動畫,使用 infiniteRepeatable() 方法創建。

@Stable
fun <T> infiniteRepeatable(
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode = RepeatMode.Restart,
    initialStartOffset: StartOffset = StartOffset(0)
): InfiniteRepeatableSpec<T> =
    InfiniteRepeatableSpec(animation, repeatMode, initialStartOffset)

與 repeatable() 方法相比少了一個參數 iterations。無限迴圈動畫自然是不需要指定重覆次數的,其餘參數一樣。
示例:

@Composable
fun Demo() {
    var bigBox by remember {
        mutableStateOf(false)
    }
    val customSize by animateDpAsState(
        targetValue = if (bigBox) 200.dp else 50.dp,
        animationSpec = infiniteRepeatable(
            animation = tween()
        ),
        label = ""
    )
    Box(modifier = Modifier
        .background(Color.LightGray)
        .size(customSize)
        .clickable {
            bigBox = !bigBox
        })
}

這裡註意和前面講到的 remeberInfiniteTransition 用法做一個區分。
remeberInfiniteTransition 是一種動畫類型,需要結合 infiniteRepeatable 一起使用,是在 transition 動畫中使用,而 infiniteRepeatableSpec 是一種動畫規格,可以使用在任何需要 animationSpec 參數的方法中。

3.7 FloatAnimationSpec

FloatAnimationSpec 是一個介面,有兩個實現類,FloatTweenSpec 僅針對 Float 類型做 TweenSpec 動畫,FloatSpringSpec 僅針對 Float 類型做 SpringSpec 動畫。官方沒有提供可以直接進行使用的方法,因為 tween() 和 spring() 支持全量數據類型,FloatAnimationSpec 是底層做更精細的計算的時候才會去使用。

3.8 KeyframesWithSplineSpec

KeyframesWithSplineSpec 是基於三次埃爾米特樣條曲線的變化,截止目前,該類是一個實驗性質的 API, 目前官方文檔沒有說明其使用方法。等待後續穩定可用。

關於動畫規格的介紹這些了。

四、TwoWayConverter

4.1 TwoWayConterver 是什麼

TwoWayConterver 是一個介面:

interface TwoWayConverter<T, V : AnimationVector> {
    val convertToVector: (T) -> V

    val convertFromVector: (V) -> T
}

它可以需要實現將任意 T 類型的數值轉換成標準的 AnimationVector 類型。以及將標準的 AnimationVector 類型轉換為任意的 T 類型數值。

這個 AnimationVerction 有如下子類:

分別表示 1 維到 4 維的的矢量值。

我們看看幾個 Compose 實現的方式。

private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> = TwoWayConverter(
    convertToVector = { AnimationVector1D(it.value) },
    convertFromVector = { Dp(it.value) }
)
private val SizeToVector: TwoWayConverter<Size, AnimationVector2D> =
    TwoWayConverter(
        convertToVector = { AnimationVector2D(it.width, it.height) },
        convertFromVector = { Size(it.v1, it.v2) }
    )
private val OffsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
    TwoWayConverter(
        convertToVector = { AnimationVector2D(it.x, it.y) },
        convertFromVector = { Offset(it.v1, it.v2) }
    )

可以看到,常用類型都已經提供了 TwoWayConverter 的拓展實現。可以在這些類型的半生對象中找到,並可以直接使用。

4.2 自定義 TwoWayConterver

對於沒有提供預設支持的數據類型,可以自定義 TwoWayConterver。
示例:

data class HealthData(val height: Float, val weight: Float)

@Composable
fun MyAnimation(targetHealthData: HealthData) {
    val healthDataToVector: TwoWayConverter<HealthData, AnimationVector2D> = TwoWayConverter(
        convertToVector = { AnimationVector2D(it.height, it.weight) },
        convertFromVector = { HealthData(it.v1, it.v2) }
    )

    val animationHeath by animateValueAsState(
        targetValue = targetHealthData,
        typeConverter = healthDataToVector, 
        label = ""
    )
}

我們定義了一個健康數據類,包含身高和體重。然後實現一個 TwoWayConterver。並基於這個 TwoWayConterver 實現了一個自定義動畫。

五、高級動畫 API

所謂高級別動畫 API ,是指這些 API 是基於前面講到的低級別動畫 API 進行封裝的,使用起來更方便。
很多 Compose 教程都是先介紹高級別動畫 API ,再介紹低級別動畫 API,因為高級別的 API 使用起來更簡單,服務於常見的業務,開箱即用。我先介紹了低級別動畫 API ,是因為我覺得這樣更好理解。明白了背後的原理,再去看上層實現,才能心中的脈絡框架。下麵逐一介紹高級別的動畫 API。

5.1、AnimatedVisibility

5.1.1 基本使用

AnimatedVisibility 是一個用於可組合項出現/消失的過渡動畫效果。藉助 AnimatedVisibility 可以輕鬆實現隱藏和顯示可組合項。
看一下效果:

使用方法如下:

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

AnimatedVisibility 本身就是一個 Composable。定義如下:

@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedVisibilityImpl(transition, { it }, modifier, enter, exit, content = content)
}

可以看到,AnimatedVisibility 是基於 Transition 動畫實現的。關鍵參數如下:

  • visible: 表示可組合項出現或者消失。
  • content: 表示要添加出現/消失動畫的可組合項內容。
  • enter: 表示 content 出現的動畫。EnterTransition 類型。
  • exit: 表示 content 消失的動畫。ExitTransition 類型。

對於動畫,可以使用 + 運算符,組合多個 EnterTransition 或者 ExitTransition 對象。預設情況下內容以淡入和擴大的方式出現,以淡出和縮小的方式消失。
Compose 提供了多種 EnterTransition 或者 ExitTransition 的實例。
fadeInfadeOutslideInslideOutslideInHorizontallyslideOutHorizontallyslideInVerticallyslideOutVerticallyscaleInscaleOutexpandInshrinkOutexpandHorizontallyshrinkHorizontallyexpandVerticallyshrinkVertically
具體效果大家可以動手試一下,也可以到官網查看:animatedvisibility

示例:

@Composable
fun Demo() {
    var visible by remember {
        mutableStateOf(false)
    }

    Column {
        AnimatedVisibility(visible) {
            Box(modifier = Modifier.background(Color.LightGray).size(200.dp))
        }
        Button(onClick = {
            visible = !visible
        }) {
            Text(text = "click me")
        }
    }
}

效果如下:

5.1.2 MutableTransitionState 監聽動畫執行狀態

AnimatedVisibility 還提供了另一個重載方法。它接受一個 MutableTransitionState 類型的參數。

@Composable
fun AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visibleState, label)
    AnimatedVisibilityImpl(transition, { it }, modifier, enter, exit, content = content)
}

它與上一小節的那個方法,只有第一個參數不一樣,它支持監聽動畫狀態。看一下 MutableTransitionState 的定義:

class MutableTransitionState<S>(initialState: S) : TransitionState<S>() {

    override var currentState: S by mutableStateOf(initialState)
        internal set

    override var targetState: S by mutableStateOf(initialState)

    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning

    override fun transitionConfigured(transition: Transition<S>) {
    }
}

MutableTransitionState 有兩個關鍵成員:currentStatetargetState,表示當前狀態和目標狀態。兩個狀態的不同驅動了動畫的執行。用法如下:

@Composable
fun Demo() {
    val visible = remember {
        MutableTransitionState<Boolean>(false)
    }

    Column {
        AnimatedVisibility(visible) {
            Box(modifier = Modifier.background(Color.LightGray).size(200.dp))
        }
        Button(onClick = {
            visible.targetState = !visible.currentState
        }) {
            Text(text = "click me")
        }
    }
}

當需要執行動畫時,只需要改變 MutableTransitionState 對象的 targetState,讓它與 currentState 不同。效果與上一小節完全一致,這裡就不貼圖了。
另外 MutableTransitionState 還可以方便實現 AnimatedVisibility 首次添加到組合樹中,就立即觸發動畫。只需在初始化 MutableTransitionState 對象是,讓 targetState 和 currentState 不同即可。可以用此來實現一些開屏動畫的效果。

val visible = remember {
    MutableTransitionState<Boolean>(false).apply {
        targetState = true
    }
}

此外,MutableTransitionState 的意義還在於可以通過 currentState 和 isIdle 的值,獲取動畫的執行狀態。

@Composable
fun Demo() {
    val visible = remember {
        MutableTransitionState<Boolean>(false)
    }

    Column {
        AnimatedVisibility(visible) {
            Box(
                modifier = Modifier
                    .background(Color.LightGray)
                    .size(200.dp)
            )
        }
        Button(onClick = {
            visible.targetState = !visible.currentState
        }) {
            Text(text = "click me")
        }
        Text(
            text = when {
                visible.isIdle && visible.currentState -> "Visible"
                !visible.isIdle && visible.currentState -> "Disappearing"
                visible.isIdle && !visible.currentState -> "Invisible"
                else -> "Appearing"
            }
        )
    }
}

效果如下:

5.1.3 為子可組合項添加進入和退出的動畫

AnimatedVisibility 中直接或者間接的子可組合項,可以使用 Modifier.animateEnterExit 修飾符單獨設置進入和退出的過渡動畫。這樣子項的動畫就是 AnimatedVisibility 中設置的動畫與子項自己設置的動畫結合在一起構成的。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Demo() {
    var visible by remember {
        mutableStateOf(false)
    }
    Column {
        AnimatedVisibility(
            visible = visible,
        ) {
            Box(
                modifier = Modifier
                    .background(Color.LightGray)
                    .size(200.dp)
                    .animateEnterExit(
                        enter = slideInHorizontally(),
                        exit = slideOutHorizontally(),
                        label = ""
                    )
            )
        }
        Button(onClick = {
            visible = !visible
        }) {
            Text(text = "click me")
        }
    }
}

如果希望每個子項完全自己定義不同的動畫效果,則可以將 AnimatedVisibility 中的動畫設置為 EnterTransition.NoneExitTransition.None

5.1.4 自定義 AnimatedVisibility 動畫

除了使用 Compose 提供的 EnterTransitionExitTransition 動畫以外,AnimatedVisibility 還支持自定義動畫效果。通過在 AnimatedVisibility 的 AnimatedVisibilityScope 中的 transition 訪問底層的 Transition 實例。添加到 transition 的動畫會和 AnimatedVisibility 中設置的動畫同時運行,AnimatedVisibility 會等到 Transition 中的所有動畫都完成後,再移除其內容。對於獨立於 Transition 創建的動畫(比如使用 animate*AsState 創建的動畫), AnimatedVisibility 將無法解釋這些動畫。因此可能會在動畫完成之前移除內容可組合項。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Demo() {
    var visible by remember {
        mutableStateOf(false)
    }
    Column {
        AnimatedVisibility(
            visible = visible,
        ) {
            val bgColor by transition.animateColor(label = "") { state ->
                if (state == EnterExitState.Visible) Color.Red else Color.Blue
            }
            Box(
                modifier = Modifier
                    .background(bgColor)
                    .size(200.dp)
            )
        }
        Button(onClick = {
            visible = !visible
        }) {
            Text(text = "click me")
        }
    }
}

直白點理解就是 AnimatedVisibilityScope 裡面提供了一個 trasition 對象,然後通過 transition 對象做動畫處理。
這也是我為什麼先講解低級別動畫 API 的原因,不然在這裡自定義動畫,可能有些術語就會造成困擾。
我們是否也可以自己使用前面講解的 updateTransition() 方法創建一個 trasition 對象,然後使用這個 trasition 做動畫處理呢?可以是可以,但是與使用 animate*AsState 一樣,AnimatedVisibility 將無法解釋這些動畫。可能會在動畫完成之前移除內容可組合項。
如下代碼做了一個對比:

@Preview
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Demo1() {
    var visible by remember {
        mutableStateOf(false)
    }
    Column {
        AnimatedVisibility(
            visible = visible,
        ) {
            val bgColor by transition.animateColor(label = "bgColor",
                transitionSpec = {
                    tween(durationMillis = 3000, delayMillis = 0, easing = LinearEasing)
                }
            ) { state ->
                if (state == EnterExitState.Visible) Color.Red else Color.Blue
            }

            Box(
                modifier = Modifier
                    .background(bgColor)
                    .size(200.dp)
            )
        }
        Button(onClick = {
            visible = !visible
        }) {
            Text(text = "click me")
        }
    }
}

@Preview
@Composable
fun Demo2() {
    var visible by remember {
        mutableStateOf(false)
    }
    Column {
        AnimatedVisibility(
            visible = visible,
        ) {
            val myTransition = updateTransition(targetState = visible, label = "myColor")
            val myColor by myTransition.animateColor(
                label = "",
                transitionSpec = {
                    tween(durationMillis = 3000, delayMillis = 0, easing = LinearEasing)
                }
            ) {
                if (it) Color.Red else Color.Blue
            }

            Box(
                modifier = Modifier
                    .background(myColor)
                    .size(200.dp)
            )
        }
        Button(onClick = {
            visible = !visible
        }) {
            Text(text = "click me")
        }
    }
}

此處特意將動畫持續時間設置為了 3000ms, 最終的效果就是,Demo1 中,在 enter 時,我們能看到顏色漸變動畫的整個過程。而 Demo2 就感覺不明顯。
使用動畫調節器可以更清晰看到區別:
Demo1:

當我們使用 AnimatedVisibilityScope 裡面提供的 transition 對象做動畫,所有的動畫都包含在 AnimatedVisility 這個動畫之內。以最長的時間為動畫執行時間。
Demo2:

當我們通過 updateTransition() 創建 transition 對象做動畫,這個動畫和 AnimatedVisility 動畫是獨立的。

5.2 AnimatedContent

AnimatedContent 可組合項會在內容根據目標狀態發生變化時,為內容添加動畫效果。

5.2.1 基本用法

用法:

@Composable
fun Demo() {
    var typeState by remember {
        mutableStateOf(true)
    }
    Column {
        Button(onClick = {
            typeState = !typeState
        }) {
            Text(text = "Change Type")
        }
        AnimatedContent(targetState = typeState, label = "") {
            if (it) {
                Text(
                    text = "Hello, Animation",
                    modifier = Modifier
                        .background(Color.Yellow)
                        .wrapContentSize()
                )
            } else {
                Icon(imageVector = Icons.Filled.Face, contentDescription = "")
            }
        }
    }
}

為了清楚看到效果,我在開發者模式,把動畫持續時間改為 5 倍了。

看一下定義:

@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
        (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
            scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
            .togetherWith(fadeOut(animationSpec = tween(90)))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    label: String = "AnimatedContent",
    contentKey: (targetState: S) -> Any? = { it },
    content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {
    val transition = updateTransition(targetState = targetState, label = label)
    transition.AnimatedContent(
        modifier,
        transitionSpec,
        contentAlignment,
        contentKey,
        content = content
    )
}

簡單理解 AnimatedContent 這個可組合函數,AnimatedContent 內部維護著 targetState 到 content 的映射表,當 targetState 發生變化了, AnimatedContentScope 中的可組合函數 content 就會發生重組,在 content 重組時附加的動畫效果就會執行。

預設情況下,動畫效果是初始內容淡出,目標內容淡入。可以從 AnimatedContent 的聲明中看到。自定義動畫效果,即給 transitionSpec 參數指定一個 ContentTransform 對象。ContentTransform 也是由 EnterTransition 和 ExitTranstion 組合的,可以使用中綴運算符 with 將 EnterTransition 和 ExitTransition 組合起來以常見 ContentTransform。

infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)

我當前是 Compose 1.6.5 版本,比較新,顯示 with 已經過時了。現在使用 togetherWith,用法一樣。

infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)

下麵看下例子:

@Composable
fun Demo() {
    var typeState by remember {
        mutableStateOf(true)
    }
    Column {
        Button(onClick = {
            typeState = !typeState
        }) {
            Text(text = "Change Type")
        }
        AnimatedContent(targetState = typeState, label = "", transitionSpec = {
            slideInVertically { fullHeight -> fullHeight }.togetherWith(slideOutVertically { fullHeight -> -fullHeight })
        }) {
            if (it) {
                Text(
                    text = "Hello, Animation",
                    modifier = Modifier
                        .background(Color.Yellow)
                        .wrapContentSize()
                )
            } else {
                Icon(imageVector = Icons.Filled.Face, contentDescription = "")
            }
        }
    }
}

效果:

另外,AnimatedContent 提供了 slideIntoContainerslideOutOfContainer,可以作為 slideInHorizontally/VerticallyslideOutHorizontally/Vertically 的便捷替代方案。它們可以根據 另外,AnimatedContent 的初始內容的大小和目標內容的大小來計算滑動距離。

5.2.2 SizeTransform 定義大小動畫

AnimatedContent 中,還可以使用中綴函數 usingSizeTransform 應用於 ContentTransform 來定義大小動畫。
看下 SizeTransform :

fun SizeTransform(
    clip: Boolean = true,
    sizeAnimationSpec: (initialSize: IntSize, targetSize: IntSize) -> FiniteAnimationSpec<IntSize> =
        { _, _ ->
            spring(
                stiffness = Spring.StiffnessMediumLow,
                visibilityThreshold = IntSize.VisibilityThreshold
            )
        }
): SizeTransform = SizeTransformImpl(clip, sizeAnimationSpec)

SizeTransform 定義了大小應如何在初始內容與目標內容之間添加動畫效果。在創建動畫時,可以訪問初始大小和目標大小。SizeTransform 還可控制在動畫播放期間是否應將內容裁剪為組件大小。

示例:

@Composable
fun Demo() {
    var typeState by remember {
        mutableStateOf(true)
    }
    Column {
        Button(onClick = {
            typeState = !typeState
        }) {
            Text(text = "Change Type")
        }
        AnimatedContent(targetState = typeState, label = "", transitionSpec = {
            fadeIn(animationSpec = tween(150, 150))
                .togetherWith(fadeOut(animationSpec = tween(150)))
                .using(
                    SizeTransform(
                        clip = true,
                        sizeAnimationSpec = { initSize, targetSize ->
                            if (targetState) {
                                keyframes {
                                    // 展開時,先水平方向展開.
                                    IntSize(targetSize.width, initSize.height) at 150
                                    durationMillis = 300
                                }
                            } else {
                                keyframes {
                                    // 收起時,先垂直方向收起.
                                    IntSize(initSize.width, targetSize.height) at 150
                  

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 隨著企業數據規模的增長和業務多元化發展,海量數據實時、多維地靈活查詢變成業務常見訴求。同時多套資料庫系統成為常態,這既帶來了數據管理的複雜性,又加大了數據使用的難度,面對日益複雜的數據環境和嚴格的數據安全要求,需要解決多資料庫系統並存、數據孤島嚴重、許可權管理混亂和數據查詢提取困難等問題。與此同時,企 ...
  • Spark 是一個快速、通用、可擴展的大數據計算引擎,具有高性能、易用、容錯、可以與 Hadoop 生態無縫集成、社區活躍度高等優點。在實際使用中,具有廣泛的應用場景: · 數據清洗和預處理:在大數據分析場景下,數據通常需要進行清洗和預處理操作以確保數據質量和一致性,Spark 提供了豐富的 API ...
  • 目錄一、什麼是MongoDB二、MongoDB 與關係型資料庫對比三、數據類型四、部署MongoDB1、下載二進位包2、下載安裝包並解壓3、創建用於存放數據和日誌的目錄,並修改許可權4、啟動MongoDB4.1前臺啟動4.2後臺啟動4.3、配置文件啟動服務4.4、配置systemd服務4.5、syst ...
  • 本文介紹基於Microsoft SQL Server軟體,實現資料庫表的創建、修改、複製、刪除與表數據處理的方法。 目錄1 互動式創建資料庫表T2 互動式創建資料庫表S3 T-SQL創建資料庫表C4 T-SQL創建資料庫表SC5 T-SQL創建資料庫表TC6 互動式向資料庫表S中添加新列NATIVE ...
  • 字元編碼和排序規則 下麵的討論用到W、王和三個字元,以下是這三個字元的各種編碼 先看看不帶N和帶N的字元字面量各用什麼編碼,用Microsoft SQL Server Management Studio連接SQL SERVER 2022執行下麵SQL語句: select N'W' charact ...
  • 引言 在數據驅動的世界中,企業正在尋求可靠且高性能的解決方案來管理其不斷增長的數據需求。本系列博客從一個重視數據安全和合規性的 B2C 金融科技客戶的角度來討論雲上雲下混合部署的情況下如何利用亞馬遜雲科技雲原生服務、開源社區產品以及第三方工具構建無伺服器數據倉庫的解耦方法。 Apache EMR(E ...
  • 問題:Jetpack Compose 中使用 Material 包中的控制項,點擊預設會有水波紋效果。如何去除這個點擊水波紋效果呢? 看下 Modifier.clickable 的簽名: fun Modifier.clickable( interactionSource: MutableInterac ...
  • 看問題本質,設置全面屏,是系統視窗的行為,與 View 和 Compose 有什麼關係呢? 所以,原理和傳統 View 視圖是一樣的,甚至 Api 都是一模一樣的,不熟悉的可以看我之前的文章。傳送門: Android 全面屏體驗 那為什麼還要寫這篇文章呢?主要是在 Compose 中寫法上的一些區別 ...
一周排行
    -Advertisement-
    Play Games
  • 基於.NET Framework 4.8 開發的深度學習模型部署測試平臺,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等應用場景,同時支持圖像與視頻檢測。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runti... ...
  • 十年沉澱,重啟開發之路 十年前,我沉浸在開發的海洋中,每日與代碼為伍,與演算法共舞。那時的我,滿懷激情,對技術的追求近乎狂熱。然而,隨著歲月的流逝,生活的忙碌逐漸占據了我的大部分時間,讓我無暇顧及技術的沉澱與積累。 十年間,我經歷了職業生涯的起伏和變遷。從初出茅廬的菜鳥到逐漸嶄露頭角的開發者,我見證了 ...
  • C# 是一種簡單、現代、面向對象和類型安全的編程語言。.NET 是由 Microsoft 創建的開發平臺,平臺包含了語言規範、工具、運行,支持開發各種應用,如Web、移動、桌面等。.NET框架有多個實現,如.NET Framework、.NET Core(及後續的.NET 5+版本),以及社區版本M... ...
  • 前言 本文介紹瞭如何使用三菱提供的MX Component插件實現對三菱PLC軟元件數據的讀寫,記錄了使用電腦模擬,模擬PLC,直至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1. PLC開發編程環境GX Works2,GX Works2下載鏈接 https:// ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • 1、jQuery介紹 jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝 ...
  • 前言 之前的文章把js引擎(aardio封裝庫) 微軟開源的js引擎(ChakraCore))寫好了,這篇文章整點js代碼來測一下bug。測試網站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻譯js逆向(MD5加密,AES加密)附完整源碼 ...
  • 引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
  • 掌握使用Python進行文本英文統計的基本方法,並瞭解如何進一步優化和擴展這些方法,以應對更複雜的文本分析任務。 ...
  • 背景 Redis多數據源常見的場景: 分區數據處理:當數據量增長時,單個Redis實例可能無法處理所有的數據。通過使用多個Redis數據源,可以將數據分區存儲在不同的實例中,使得數據處理更加高效。 多租戶應用程式:對於多租戶應用程式,每個租戶可以擁有自己的Redis數據源,以確保數據隔離和安全性。 ...