首頁 > 軟體

Android Compose狀態改變動畫animateXxxAsState使用詳解

2022-12-01 14:00:58

前言

上一篇文章我們探索了 Compose 中屬性動畫的使用,發現屬性動畫確實是可以在 Compose 中使用的,雖然使用方式跟傳統 Android 開發中有所區別,但也不難用,甚至對於已經熟悉了屬性動畫的我們來說學習成本更低,那麼為什麼 Compose 又要單獨搞一套 動畫 Api 呢?為了搞清楚這個問題,首先我們得先學會 Compose 動畫的使用。

實踐是檢驗真理的唯一標準,我們將從這一篇開始一步步深入學習 Compose 動畫的使用,看看它到底好不好用。本篇將首先從animateXxxAsState這一組動畫 Api 開始進入 Compose 的動畫世界。

animateXxxAsState

在 Compose 中提供了一系列動畫 API,其中有一類 API 跟屬性動畫非常類似,它就是 animateXxxAsState,我翻譯成狀態改變動畫,其中 Xxx對應的是 DpFloatIntSizeOffsetRectIntOffsetIntSizeColor等資料型別,即當狀態改變時觸發對應資料型別的值的發生改變,從而執行資料從當前值到目標值變化的動畫。

對應 Api 如圖:

接下來就看看這些 Api 到底是如何使用的。

基礎使用

我們首先以 animateDpAsState為例來看一下 animateXxxAsState 動畫到底如何使用。

Dp是 Compose 提供的一個封裝資料型別,作用跟在傳統 Android 開發中 xml 使用的 dp單位是一樣的,是與螢幕畫素密度相關的抽象單位。Compose 中為其提供了基礎資料型別的擴充套件,可以直接使用數值.dp進行使用,如:10.dp12.5.dp等。

在 Compose 中跟長度相關的引數型別基本上都是 Dp,如寬高、圓角、間距等等。

animateDpAsState的定義如下:

fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp>

引數說明:

  • targetValue:目標值
  • animationSpec:動畫規格
  • finishedListener:動畫完成監聽

返回值是一個 State 物件,即當其內部 value 值發生改變時會觸發 Compose 的重組,從而重新整理介面。

前面說了 animateXxxAsState 跟屬性動畫類似,但是好像不對呀,這裡引數只有一個 targetValue即目標值,熟悉屬性動畫的都知道,屬性動畫的數值引數是一個可變引數,當為 1 個的時候,初始值為屬性當前值,目標值為傳入引數值,多個引數時初始值為第一個引數值,那這裡只有一個 targetValue引數是不是也是初始值是從元件中獲取呢?

我們來試試,建立一個 Box 通過改變其左邊距實現向右移動的動畫:

val startPadding = animateDpAsState(10.dp)
Box(Modifier
    .padding(start = startPadding.value, top = 10.dp)
    .size(100.dp, 100.dp)
    .background(Color.Blue)
)

因為需要對左邊距進行改變,所以將 padding 的 start 提取為 startPadding 變數,如上面的程式碼,但是這樣的話那初始值就是 animateDpAsState傳入的值,也就是這裡的 10.dp ,先執行一下看看是不是這樣:

執行效果確實是這樣,那怎麼實現動畫效果呢?是修改 startPadding 的值麼?我們給 Box 新增一個點選事件修改 startPadding 的值看看:

val startPadding = animateDpAsState(10.dp)
Box(Modifier
    .padding(start = startPadding.value, top = 10.dp)
    .size(100.dp, 100.dp)
    .background(Color.Blue)
    // 新增點選事件
    .clickable { 
        // 修改值 報錯
        startPadding.value = 100.dp
    }
)

這樣寫編輯器直接報錯了,錯誤資訊如下:

說 val 變數不能重新賦值,是因為 startPadding 變數定義成了 val 所以不能修改麼?並不是,因為我們重新賦值的不是 startPadding 變數,而是其內部的 value,而 startPadding 是 State 型別,State 內部的 value 是 val 的,定義如下:

interface State<out T> {
    val value: T
}

所以並不能通過重新賦值修改 animateDpAsState建立的 State 的 value 值,那麼怎麼修改這個值讓其產生動畫呢?

前面說了 animateXxxAsState 是依賴狀態改變而產生值的變化,所以實際上我們這裡還需要定義一個額外的狀態變數,targetValue 引數根據這個狀態傳入不同的值,修改上面程式碼如下:

@Composable
fun DpAnimationBox(){
    // 是否移動到右邊
    var moveToRight by remember { mutableStateOf(false) }
    //根據 moveToRight 變數傳入引數,true 代表在右邊則傳入 100.dp,false 在左邊則傳入 10.dp
    val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp)
    Box(Modifier
        .padding(start = startPadding.value, top = 10.dp)
        .size(100.dp, 100.dp)
        .background(Color.Blue)
        .clickable {
            // 改變 moveToRight 狀態,這裡直接取反
            moveToRight = !moveToRight
        }
    )
}

修改點如下:

  • 使用 mutableStateOf 建立 moveToRight 變數,內部值為 Boolean 型別,即 MutableState,因為是在 Compose 函數中使用,需要用 remember 函數包裹,防止重組時重複建立
  • 修改 animateDpAsState 傳入引數的固定值為根據 moveToRight 傳入,即 if (moveToRight) 100.dp else 10.dp
  • 修改點選事件處理,修改 moveToRight 的值

執行看一下效果:

終於有效果了。所以實際是根據 moveToRight 的值改變導致傳入 animateDpAsState 的 targetValue 引數的值發生改變,而動畫執行的就是之前舊的值到當前設定最新值的動畫效果。

上面的 moveToRight 是 MutableState 型別, 內部的 value 是 Boolean 型別,那是不是隻能是 Boolean 型別呢,當然不是,可以是任何型別,只要在傳入 animateDpAsState 的引數值時根據這個型別的值進行自定義條件判斷傳入不同的資料即可,比如定義一個列舉型別,根據不同型別傳入不同的引數,如下:

enum class CustomState{
    STATE1,
    STATE2,
    STATE3,
}
var customState by remember { mutableStateOf(CustomState.STATE1) }
val paddingValue = when(customState){
    CustomState.STATE1 -> 0.dp
    CustomState.STATE2 -> 100.dp
    CustomState.STATE3 -> 200.dp
}
val startPadding = animateDpAsState(paddingValue)

甚至你可以直接建立一個跟動畫值相同的資料型別,比如這裡可以直接建立一個 Dp 型別的狀態變數,然後在點選時直接改變其值來驅動動畫執行,如下:

@Composable
fun DpAnimationBox(){
    // 動畫目標值
    var startPaddingValue by remember { mutableStateOf(10.dp) }
    // 蔣其設定給 animateDpAsState
    val startPadding = animateDpAsState(startPaddingValue)
    Box(Modifier
        .padding(start = startPadding.value, top = 10.dp)
        .size(100.dp)
        .background(Color.Blue)
        .clickable {
            // 改變動畫目標值
            if(startPaddingValue == 10.dp){
                startPaddingValue = 100.dp
            }else{
                startPaddingValue = 10.dp
            }
        }
    )
}

上面程式碼同樣能實現跟之前一樣的效果。使用還是相當靈活的,開發中可以根據實際的需求定義不同的狀態來完成我們想要的動畫效果。

動畫監聽

animateXxxAsState提供了動畫完成時的監聽 finishedListener,可以通過監聽動畫完成進行自定義的業務處理,比如修改介面的顯示狀態或者開啟下一個動畫等。

比如 animateDpAsStatefinishedListener 定義如下:

(Dp) -> Unit

有一個 Dp 型別的引數,即動畫完成時的目標值,使用如下:

val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) {
   //TODO: do something
}

比如我們想在上面的動畫結束時再讓方塊移動回去,那我們可以這麼寫:

val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) {
    if(it == 100.dp){
        moveToRight = false
    }
}

效果如下:

或者我們想讓這個方塊往返重複執行,可以這麼寫:

val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) {
    moveToRight = !moveToRight
}

效果如下:

通過對 animateXxxAsState動畫的監聽我們可以實現介面狀態的重新整理或進行動畫的組合等自定義操作。

使用範例

前面講了 animateDpAsState動畫的使用,其他 animateXxxAsStateapi 的使用基本一樣,只是動畫作用的資料型別不一樣,下面將通過一個個簡單範例來看看其他幾個 api 的使用。

animateFloatAsState

animateFloatAsState作用於 Float 型別資料的動畫,比如 alpha 值,通過改變控制元件的 alpha 值可實現元素的顯示與隱藏,使用範例如下:

@Composable
fun FloatAnimationBox() {
    var show by remember { mutableStateOf(true) }
    val alpha by animateFloatAsState(if (show) 1f else 0f)
    Box(Modifier
        .padding(10.dp)
        .size(100.dp)
        .alpha(alpha)
        .background(Color.Blue)
        .clickable {
            show = !show
        }
    )
}

動畫效果:

animateIntAsState

animateIntAsState作用於 Int 資料型別,上面的 animateDpAsState實現的動畫也可以使用 animateIntAsState實現,如下:

@Composable
fun IntAnimationBox() {
    var moveToRight by remember { mutableStateOf(false) }
    val startPadding by animateIntAsState(if (moveToRight) 100 else 10)
    Box(Modifier
        .padding(start = startPadding.dp, top = 10.dp)
        .size(100.dp)
        .background(Color.Blue)
        .clickable {
            moveToRight = !moveToRight
        }
    )
}

效果跟使用 animateDpAsState 實現的一樣:

animateColorAsState

animateColorAsState是作用於 Color 上,可實現顏色的過渡動畫,比如將上面的方塊顏色從藍色變為紅色,程式碼如下:

@Composable
fun ColorAnimationBox() {
    var toRed by remember { mutableStateOf(false) }
    val color by animateColorAsState(if (toRed) Color.Red else Color.Blue)
    Box(Modifier
        .padding(10.dp)
        .size(100.dp)
        .background(color)
        .clickable {
            toRed = !toRed
        }
    )
}

效果如下:

animateSizeAsState/animateIntSizeAsState

animateSizeAsState作用於 Size 上,看到這個我們一下就想到了用於控制元件的 size 上,比如上面的 Modifier.size()上,但實際上 Modifier.size()的引數並不是 Size 型別,而是 Dp 型別或者 DpSize,而 DpSize 並不是 Size 的子類,所以不能直接將 Size 型別的資料直接傳入 Modifier.size()中,而是需要轉換一下:

@Composable
fun SizeAnimationBox() {
    var changeSize by remember { mutableStateOf(false) }
    // 定義 Size 動畫
    val size by animateSizeAsState(if (changeSize) Size(200f, 50f) else Size(100f, 100f))
    Box(Modifier
        .padding(10.dp)
        // 設定 Size 值
        .size(size.width.dp, size.height.dp)
        .background(Color.Blue)
        .clickable {
            changeSize = !changeSize
        }
    )
}

效果如下:

animateIntSizeAsStateanimateSizeAsState幾乎一樣,只是它作用於 IntSize,跟 Size 的唯一區別就是引數是 Int 型別而不是 Float 型別,如下:

val size by animateIntSizeAsState(if (changeSize) IntSize(200, 50) else IntSize(100, 100))

animateOffsetAsState/animateIntOffsetAsState

animateOffsetAsState作用於 Offset 型別資料,用於控制偏移量,同樣的它不能直接用於 Modifier.offset()上,因為 Modifier.offset()接收的也是 Dp 型別引數,所以也需要進行轉換,如下:

@Composable
fun OffsetAnimationBox() {
    var changeOffset by remember { mutableStateOf(false) }
    // 定義 offset 動畫
    val offset by animateOffsetAsState(if (changeOffset) Offset(100f, 100f) else Offset(0f, 0f))
    Box(Modifier
        // 設定 offset 數值
        .offset(offset.x.dp, offset.y.dp)
        .padding(10.dp)
        .size(100.dp)
        .background(Color.Blue)
        .clickable {
            changeOffset = !changeOffset
        }
    )
}

效果如下:

animateIntOffsetAsState則作用於 IntOffset型別資料,使用方法與上面一致,只是將 Float 型別換成 Int 型別:

val intOffset by animateIntOffsetAsState(if (changeOffset) IntOffset(100, 100) else IntOffset(0, 0))

Modifier.offset()提供了一個返回 IntOffset 的函數引數,可以如下使用:

Modifier.offset { intOffset } 

animateRectAsState

animateRectAsState作用於 Rect資料,即可以同時控制位置和大小,通過 animateRectAsState可實現上面方塊的位置和大小變化的動畫,使用如下:

@Composable
fun RectAnimationBox() {
    var changeRect by remember { mutableStateOf(false) }
	// 定義 rect
    val rect by animateRectAsState(if (changeRect) Rect(100f, 100f, 310f, 150f) else Rect(10f, 10f, 110f, 110f))
    Box(Modifier
        // 設定位置偏移
        .offset(rect.left.dp, rect.top.dp)
        // 設定大小
        .size(rect.width.dp, rect.height.dp)
        .background(Color.Blue)
        .clickable {
            changeRect = !changeRect
        }
    )
}

效果如下:

實戰

上面講了 animateXxxAsState動畫 api 的基本使用,下面就用這些 api 來完成一個實戰效果,還是上一篇《Compose 中屬性動畫的使用》的效果:

前面說了animateXxxAsState是依賴於狀態的動畫,分析上面的動畫一共存在 4 個狀態:

  • 預設狀態:顯示藍色矩形按鈕,文字為 Upload
  • 開始上傳狀態:按鈕變為圓形且中間為白色,邊框為灰色,文字消失
  • 上傳中狀態:邊框根據進度變為藍色
  • 上傳完成狀態:按鈕從圓形回到圓角矩形,且顏色變為紅色,文字變為 Success

實現原理如下:

首先通過一個列舉定義上述四種狀態:

enum class UploadState {
    Normal,
    Start,
    Uploading,
    Success
}

然後實現預設狀態的介面展示:

@Composable
fun UploadAnimation() {
    val originWidth = 180.dp
    val circleSize = 48.dp
    var uploadState by remember { mutableStateOf(UploadState.Normal) }
    var text by remember { mutableStateOf("Upload") }
    val textAlpha by animateFloatAsState(1f)
    val backgroundColor by animateColorAsState(Color.Blue)
    val boxWidth by animateDpAsState(originWidth)
    val progressAlpha by animateFloatAsState(0f)
    val progress by animateIntAsState(0)
    // 介面佈局
    Box(
        modifier = Modifier
            .padding(start = 10.dp, top = 10.dp)
            .width(originWidth),
        contentAlignment = Alignment.Center
    ) {
        // 按鈕
        Box(
            modifier = Modifier
                .clip(RoundedCornerShape(circleSize / 2))
                .background(backgroundColor)
                .size(boxWidth, circleSize)
                .clickable {
                    // 點選時修改狀態為開始上傳
                    uploadState = UploadState.Start
                },
            contentAlignment = Alignment.Center,
        ) {
            // 進度
            Box(
                modifier = Modifier.size(circleSize).clip(ArcShape(progress))
                    .alpha(progressAlpha).background(Color.Blue)
            )
            // 白色蒙版
            Box(
                modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp))
                    .alpha(progressAlpha).background(Color.White)
            )
            // 文字
            Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha))
        }
    }
}

然後根據上傳按鈕的狀態定義不同狀態時的資料值:

var textAlphaValue = 1f
var backgroundColorValue = Color.Blue
var boxWidthValue = originWidth
var progressAlphaValue = 0f
var progressValue = 0
when (uploadState) {
    // 預設狀態不處理
    UploadState.Normal -> {}
    // 開始上傳
    UploadState.Start -> {
        // 文字透明度變為0
        textAlphaValue = 0f
        // 按鈕背景顏色變為灰色
        backgroundColorValue = Color.Gray
        // 按鈕寬度變為圓的寬度
        boxWidthValue = circleSize
        // 中間進度的透明度變為 1
        progressAlphaValue = 1f
    }
    // 上傳中狀態
    UploadState.Uploading -> {
        textAlphaValue = 0f
        backgroundColorValue = Color.Gray
        boxWidthValue = circleSize
        progressAlphaValue = 1f
        // 進度值變為 100
        progressValue = 100
    }
    // 上傳完成
    UploadState.Success -> {
        // 文字透明度變為 1
        textAlphaValue = 1f
        // 顏色變為紅色
        backgroundColorValue = Color.Red
        // 按鈕寬度變化預設時的原始寬度
        boxWidthValue = originWidth
        // 進度透明度變為 0f
        progressAlphaValue = 0f
    }
}
val textAlpha by animateFloatAsState(textAlphaValue)
val backgroundColor by animateColorAsState(backgroundColorValue)
val boxWidth by animateDpAsState(boxWidthValue)
val progressAlpha by animateFloatAsState(progressAlphaValue)
val progress by animateIntAsState(progressValue)

此時執行後點選按鈕效果如下:

點選後只有開始上傳的動畫,沒有後續的動畫效果,這是因為我們在點選的時候只是將狀態變為了 UploadState.Start 而沒有進行後續狀態的改變,所以需要監聽動畫完成然後繼續改變按鈕的狀態來實現完整的動畫效果,程式碼修改如下:

    val boxWidth by animateDpAsState(boxWidthValue){
        // 按鈕寬度變化完成監聽,當狀態為 Start 則修改為 Uploading
        if(uploadState == UploadState.Start){
            uploadState = UploadState.Uploading
        }
    }
    val progress by animateIntAsState(progressValue){
        // 進度完成監聽,當狀態為 Uploading 則修改為 Success
        if(uploadState == UploadState.Uploading){
            uploadState = UploadState.Success
            // 文字內容修改為 Success
            text = "Success"
        }
    }

分別給按鈕寬度變化動畫和進度動畫進行監聽並修改其狀態,這樣就將整個動畫串聯起來了,最終效果如下:

最後

關於 animateXxxAsState的基本使用就講得差不多了,並通過一系列 api 完成了上一篇使用屬性動畫實現的效果,細心的同學會發現關於 animateXxxAsState 其實還有兩個知識點是沒有介紹到的:

  • animateXxxAsState還有一個 api animateValueAsState
  • animateXxxAsState的引數 animationSpec引數

其中 animateValueAsStateanimateXxxAsState 的底層 api,上面介紹的一系列 animateXxxAsState 最終都是呼叫 animateValueAsState 來實現,關於 animateValueAsState 我們將在下一篇進行詳細介紹。animationSpec是對動畫進行更詳細的設定,比如動畫的時間、速度曲線等,將在後續文章中詳細介紹

以上就是Android Compose狀態改變動畫animateXxxAsState使用詳解的詳細內容,更多關於Android Compose狀態改變動畫的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com