日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Android?Compose?屬性動畫使用探索詳解_Android

作者:loongwind ? 更新時間: 2022-11-17 編程語言

前言

Jetpack Compose(簡稱 Compose )是 Google 官方推出的基于 Kotlin 語言的 Android 新一代 UI 開發框架,其采用聲明式的 UI 編程特性使得 Android 應用界面的編寫和維護變得更加簡單。

本專欄將詳細介紹在使用 Compose 進行 UI 開發中如何實現炫酷的動畫效果。動畫效果在 App 使用中至關重要,它使得 App 的交互更加自然流暢,用戶使用體驗更加良好。

在傳統的 Android 開發中有古老的 View 動畫和目前流行的屬性動畫,如今 View 動畫幾乎已被廣大開發者所拋棄,屬性動畫因其可以作用于任何對象的靈活和強大特性而被開發者所擁抱。既然屬性動畫這么強大,那么它是否能用在 Compose 開發中呢?如果能那跟傳統 UI 開發中使用又有什么區別呢?本篇就帶領你來探索一下在 Compose 中屬性動畫的使用。

使用探索

在傳統 Android 開發中,屬性動畫使用得最多的是 ObjectAnimatorValueAnimator,接下來就探索一下在 Compose 中如何使用它們來實現動畫效果。

ObjectAnimator 使用探索

首先看一下在傳統 Android 開發中如何使用屬性動畫,比如使用屬性動畫實現豎直方向向下移動的動畫:

val animator = ObjectAnimator.ofFloat(view, "translationY", 10f, 100f)
animator.start()

通過 ObjectAnimator作用于 View 的 translationY屬性,不斷改變 translationY 的值從而實現動畫效果,一個很簡單的屬性動畫,這里就不貼運行效果了。

那在 Compose 中能否使用 ObjectAnimator 呢?

下面使用 Compose 在界面上顯示一個 100dp*100dp 的藍色正方形方塊,代碼如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Box(Modifier.padding(start = 10.dp, top = 10.dp)
                .size(100.dp)
                .background(Color.Blue)
            )
        }
    }
}

運行效果如下:

現在要同樣實現一個豎直方向移動的動畫效果,讓方塊從上往下移動。在上面的屬性動畫實現中 ObjectAnimator是作用于 View 組件上的,按照這個思路在這里 ObjectAnimator 就應該作用于 Box 上,但實際上我們這里壓根拿不到 Box 的實例,因為這里的 Box 實際是一個函數且沒有返回值,看一下 Box 的源碼:

@Composable
fun Box(modifier: Modifier) {
    Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}

既然作用于 Box 上不行,那能不能作用于 State 上呢,Compose 是數據驅動 UI 刷新,通過數據狀態改變重組 UI 實現界面的刷新,把上面的 top 提取為一個 State 再通過 ObjectAnimator 去改變是否可行呢?改造代碼實驗一下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val topPadding:MutableState<Int> = mutableStateOf(10)
        
        val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
        animator.duration = 1000
        
        setContent {
            Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
                .size(100.dp)
                .background(Color.Blue)
                // 添加點擊事件
                .clickable {
                    // 啟動動畫
                    animator.start()
                }
            )
        }
    }
}

改造如下:

  • 將之前 top 的固定值提取成了一個 State 變量 topPadding,當 topPadding 的值發生改變時會重組界面從而讓界面刷新
  • 聲明了 ObjectAnimator 的 animator 變量,作用于 topPadding 的 value 屬性上,并設置動畫值從 10 到 100,動畫時長 1000ms
  • 給 Box 添加點擊監聽事件啟動動畫

實際上寫完這段代碼,編輯器就已經有報錯提示了,提示如下:

說沒有找到帶 Int 參數的 setValue方法,那來看看 MutableState是否有 setValue 方法:

interface MutableState<T> : State<T> {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}

可以發現 MutableState 中是有一個 var 修飾的 value 變量的,說明是有 setValue 方法的,但是錯誤提示是找不到帶 Int 參數的 setValue 方法,實際上 MutableState 的 setValue 的定義應該是這樣的:

fun setValue(value:T){
    this.value = value
}

這里參數類型是泛型 T,而 ObjectAnimator 找的是明確的 Int 類型參數的方法,所以找不到。那怎么辦呢?是不是就意味著在 Compose 中無法使用 ObjectAnimator 了呢?

直接使用確實是不行,那我們能不能對其進行封裝,不是找不到對應的 setValue 方法嘛,那我封裝一下提供一個 setValue 方法不就行了。定義一個 IntState類,再提供一個 mutableIntStateOf方法:

class IntState(private val state: MutableState<Int>){
    var value : Int = state.value
        get() = state.value
        set(value) {
            field = value
            state.value = value
        }
}
fun mutableIntStateOf(value: Int, policy: SnapshotMutationPolicy<Int> = structuralEqualityPolicy()) : IntState{
    val state = mutableStateOf(value, policy)
    return IntState(state)
}

IntState構造方法傳入一個 MutableState 類型的 state 參數,然后提供一個 value 變量,get 方法返回 state.value ,set 方法將傳入值設置給 state.value,這樣 IntState 就有了一個明確的 setValue(value:Int) 的方法。

為了便于使用,封裝一個 mutableIntStateOf方法,實現里先采用 Compose 提供的 mutableStateOf 方法獲取一個 MutableState ,然后用其構建一個 IntState 進行返回。

再改造一下上面動畫實現代碼將 mutableStateOf替換成 mutableIntStateOf

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 替換為 mutableIntStateOf
        val topPadding = mutableIntStateOf(10)
        // 創建 ObjectAnimator 目標為 topPadding,作用屬性為 value,值從 10 變化到 100
        val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
        // 設置動畫時長 1s
        animator.duration = 1000
        
        setContent {
            Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
                .size(100.dp)
                .background(Color.Blue)
                // 添加點擊事件
                .clickable {
                    // 啟動動畫
                    animator.start()
                }
            )
        }
    }
}

現在不報錯了,運行一下看看是否有動畫效果:

效果符合預期,說明這種辦法是可行,也說明 ObjectAnimator 在 Compose 中也是可以使用的,只是不能像傳統 Android 開發那樣直接作用于 View 組件上,而是需要進行二次封裝后使用。

ValueAnimator 使用探索

ObjectAnimator 使用探索完了,那么 ValueAnimator能否使用呢?Compose 以聲明式的方式通過數據驅動界面刷新,而ValueAnimator主要用于數據的改變,好像很契合的樣子,使用 ValueAnimator 不斷改變 State 的值理論上就可以實現動畫效果。還是上面的例子,改造成使用 ValueAnimator來實現:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 使用 mutableStateOf 創建 topPadding 的 State
        var topPadding by mutableStateOf(10)
        // 創建 ValueAnimator 從 10 變化到 100
        val animator = ValueAnimator.ofInt(10, 100)
        // 動畫時長 1s
        animator.duration = 1000
        // 設置監聽,當動畫改變時動態修改 topPadding 的值
        animator.addUpdateListener {
            topPadding = it.animatedValue as Int
        }
        setContent {
            Box(Modifier.padding(start = 10.dp, top = topPadding.dp)
                .size(100.dp)
                .background(Color.Blue)
                .clickable {
                    animator.start()
                }
            )
        }
    }
}

是否有效果呢?運行一下看看效果:

跟上面使用 ObjectAnimator 實現的效果一致,說明 ValueAnimator 在 Compose 中實現動畫是可行的,只是需要手動去監聽 ValueAnimator 值的變化然后去動態更新 State 的值,稍微麻煩了一點,實際上我們也可以對其進行封裝簡化其使用。

通過上面的代碼發現,如果要在 Compose 中使用 ValueAnimator 來實現動畫,對動畫數值的改變進行監聽并動態更新 State 的值是必不可少的一步,那么我們就可以將其提取進行封裝。

/**
 * @param state 動畫作用的目標 State
 * @param values 動畫的變化值,可變參數
 */
fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
    // 創建 ValueAnimator ,參數為傳入的 values
    val animator = ValueAnimator.ofInt(*values)
    // 添加監聽
    animator.addUpdateListener {
        // 更新 state 的 value 值
        state.value = it.animatedValue as Int
    }
    return animator
}

然后將上面的創建動畫替換成使用 animatorOfInt 創建:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val topPadding = mutableStateOf(10)
        // 使用封裝的 animatorOfInt 方法創建動畫
        val animator = animatorOfInt(topPadding, 10, 100)
        animator.duration = 1000
        setContent {
            Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
                .size(100.dp)
                .background(Color.Blue)
                .clickable {
                    animator.start()
                }
            )
        }
    }
}

使用是不是要簡單很多,不需要手動去處理動畫值變化的監聽了,有點使用 ObjectAnimator 的感覺,只是不需要指定目標屬性。運行效果跟上面一致就不貼圖了。

Compose 函數中使用屬性動畫

前面在 Compose 中使用的動畫都是創建在 Compose 函數外面的,如果我們想把這個組件封裝成一個獨立的 Compose 組件就需要將動畫的創建放到 Compose 函數里面,比如將上面的效果封裝成一個 AnimationBox組件:

@Composable
fun AnimationBox(){
    val topPadding = mutableStateOf(10) 
    val animator = animatorOfInt(topPadding, 10, 100)
    animator.duration = 1000
    Box(modifier = Modifier.padding(start = 10.dp, top = topPadding.value.dp)
        .size(100.dp)
        .background(Color.Blue)
        .clickable {
            animator.start()
        })
}

首先 mutableStateOf 會報錯:

意思是在組合過程中創建 state 需要使用 remember,原因是當 state 里的值發生變化時 Compose 會進行重組導致函數重新執行,如果 mutableStateOf 不加 remember則會每次重組都重新創建 state,導致 UI 上使用的值每次都是初始值而得不到刷新。

既然報錯那就給他加上 remember:

@Composable
fun AnimationBox(){
    val topPadding = remember { mutableStateOf(10) }
    ...
}

然后在使用的地方直接使用 AnimationBox() 即可:

setContent {
    AnimationBox()
}

運行后發現效果跟之前一樣,那是不是就可以了呢?

實際上上面的代碼是還存在問題的,前面說在 Compose 重組時會重新執行 Compose 組件的代碼,也就是在界面刷新時會多次重復創建動畫對象,我們在 animatorOfInt 函數里添加一個日志再看看運行時的日志輸出:

fun  animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
    println("-------call animatorOfInt--------")
  ...
}

輸出結果:

I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------

日志確實輸出了多次,意味著動畫確實創建了多次,那怎么解決呢?

前面說了 remember可以解決重組時重復創建的問題,所以只需在創建動畫上套上 remember即可,如下:

val animator = remember { animatorOfInt(topPadding, 10, 100) }

修改后再看日志,發現就只在第一次進行了創建,動畫執行過程中并沒有再次創建。

為了方便使用,可以再封裝一個 rememberAnimatorOfInt方法:

@Composable
fun rememberAnimatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
   return remember { animatorOfInt(state, *values) }
}

在 animatorOfInt 上套了一個 remember,這樣使用時就可以直接使用 rememberAnimatorOfInt 方法:

val animator = rememberAnimatorOfInt(topPadding, 10, 100)

remember 是 Compose 提供的在 Compose 函數中緩存狀態的方法,解決在 Compose 重組時重復創建的問題,關于 remember 更多使用大家可以自行查詢相關資料,本專欄主要講解動畫的使用就不過多贅述。

實戰

前面介紹了屬性動畫在 Compose 中的運用,那在實際開發中到底好不好用呢?接下來我們通過一個實例來看看。

先看一下最終實現的效果:

一個上傳按鈕的動畫效果,動畫主要分為三階段:

  • 上傳開始
  • 按鈕從圓角矩形變成圓形
  • 按鈕顏色從藍色變成中間白色,邊框灰色
  • 文字逐漸消失
  • 上傳進度
  • 邊框根據進度變為藍色
  • 上傳完成
  • 按鈕從圓形變成圓角矩形
  • 按鈕顏色變成紅色
  • 文字逐漸顯示,且文字變為 “Success”

上傳開始動畫

先把按鈕的初始狀態使用 Compose 實現:

@Composable
fun UploadButton() {
    Box(
        modifier = Modifier
            .padding(start = 10.dp, top = 10.dp)
            .width(180.dp),
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .clip(RoundedCornerShape(24.dp))
                .background(Color.Blue)
                .size(180.dp, 48.dp),
            contentAlignment = Alignment.Center,
        ) {
            Text("Upload", color = Color.White)
        }
    }
}

運行效果如下:

下面就為這按鈕添加動畫,前面講了動畫主要作用于 State 上,所以需要先將使用到的數據提取成對應的狀態:

@Composable
fun UploadButton() {
    val originWidth = 180.dp
    val circleSize = 48.dp
    var text by remember { mutableStateOf("Upload") }
    val textAlpha = remember { mutableStateOf(1.0f) }
    val backgroundColor = remember { mutableStateOf(Color.Blue) }
    val boxWidth = remember { mutableStateOf(originWidth) }
    Box(
        modifier = Modifier
            .padding(start = 10.dp, top = 10.dp)
            .width(originWidth),
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .clip(RoundedCornerShape(height/2))
                .background(backgroundColor.value)
                .size(boxWidth.value, height),
            contentAlignment = Alignment.Center,
        ) {
            Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
        }
    }
}

創建開始上傳的動畫:

@Composable
fun UploadButton() {
  ...
    val uploadStartAnimator = remember {
        // 創建 AnimatorSet
        val animatorSet = AnimatorSet()
        // 按鈕寬度變化動畫
        val widthAnimator = animatorOfDp(boxWidth, arrayOf(originWidth, circleSize))
        // 文字消失動畫
        val textAnimator = animatorOfFloat(textAlpha, 1f, 0.0f)
        // 按鈕顏色動畫
        val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Gray))
        // 動畫添加到 AnimatorSet
        animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator)
        animatorSet
    }
    Box(...) {
        Box(
            modifier = Modifier
                ...
                .clickable {
                    // 點擊執行動畫
                    uploadStartAnimator.start()
                },
            ...
        )
    }
}

分別創建按鈕寬度、按鈕顏色和文字 alpha 值變化的動畫,因需同時執行多個動畫,這里使用 AnimatorSet 進行同時執行,然后在按鈕上添加點擊事件進行動畫執行。

上面的 animatorOfDpanimatorOfFloatanimatorOfColor都是自定義封裝的函數,封裝方法與上面介紹的 animatorOfInt基本相同,源碼可通過文章最后附的源碼地址進行查看。

運行效果如下:

好像還差點,中間應該是白色的,在 Box 下再添加一個白色圓形的 Box,默認 alpha 是 0,上傳開始時 alpha 從 0 變成 1 :

@Composable
fun UploadButton() {
    ...
    val progressAlpha = remember { mutableStateOf(0.0f) }
    val uploadStartAnimator = remember {
        ...
        // 中間白色透明度變化動畫
        val centerAlphaAnimator = animatorOfFloat(progressAlpha, 0.0f, 1f)
        animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator, centerAlphaAnimator)
        animatorSet
    }
    Box(...) {
        Box(...) {
            // 白色圓形
            Box(
                modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp))
                    .alpha(progressAlpha.value).background(Color.White)
            )
            Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
        }
    }
}

運行效果如下:

上傳進度動畫

這里通過自定義 clip 的一個弧形的 shape 來實現進度,自定義代碼如下:

class ArcShape(private val progress: Int) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path().apply {
            moveTo(size.width / 2f, size.height / 2f)
            arcTo(Rect(0f, 0f, size.width, size.height), -90f, progress / 100f * 360f, false)
            close()
        }
        return Outline.Generic(path)
    }
}

傳入一個進度值(0-100),然后根據進度值算出一個繪制的弧度,使用這個自定義的 ArcShape 代碼如下:

 Box(Modifier.size(48.dp).clip(ArcShape(30)).background(Color.Blue))

效果:

所以只需動態改變 ArcShape 的 progress 參數的值就能實現上傳進度效果,修改代碼如下:

@Composable
fun PreviewUploadButton() {
    ...
    val progress = remember { mutableStateOf(0) }
    //上傳進度動畫
    val progressAnimator = remember {
        val animator = animatorOfInt(progress, 0, 100)
        animator.duration = 1000
        animator
    }
    val uploadStartAnimator = remember {
        ...
        // 添加動畫監聽,完成后執行進度動畫
        animatorSet.addListener(onEnd = {
            progressAnimator.start()
        })
        animatorSet
    }
    Box(...) {
        Box(...) {
            // 進度 Box
            Box(
                modifier = Modifier.size(height).clip(ArcShape(progress.value))
                    .alpha(progressAlpha.value).background(Color.Blue)
            )
            ...
        }
    }
}

運行效果:

上傳完成動畫

最后是上傳完成動畫就很簡單了,基本就是開始動畫的反向,只是按鈕顏色從藍色變成了紅色,動畫在上傳進度動畫完成時執行:

@Composable
fun PreviewUploadButton() {
    ...
    
    val endAnimatorSet = remember {
        val animatorSet = AnimatorSet()
        val widthAnimator = animatorOfDp(boxWidth, arrayOf(circleSize, originWidth))
        val centerAnimator = animatorOfFloat(progressAlpha, 1f, 0f)
        val textAnimator = animatorOfFloat(textAlpha, 0f, 1f)
        val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Red))
        animatorSet.playTogether(widthAnimator, centerAnimator, textAnimator, colorAnimator)
        animatorSet.addListener(onStart = {
            text = "Success"
        })
        animatorSet
    }
    val progressAnimator = remember {
        val animator = animatorOfInt(progress, 0, 100)
        animator.duration = 1000
        animator.addListener(onEnd = {
            endAnimatorSet.start()
        })
        animator
    }
    ...
}

最終效果:

最后

通過本篇文章的探索可以發現屬性動畫在 Compose 中確實是可以使用的,雖然跟傳統 UI 開發中使用屬性動畫有所區別,但確實能用,而且通過一個簡單的實戰示例發現好像還挺好用的。好了,我已經學會 Compose 的動畫開發了,什么?Compose 還單獨提供了一套動畫 API ?

屬性動畫這不是挺好使的么,這不是多此一舉么,難道 Compose 的動畫 API 比屬性動畫還好用、還強大?如果感興趣請關注本專欄,從下一篇開始帶你真正走進 Compose 的動畫世界。

源碼地址:ComposeAnimationDemo

原文鏈接:https://juejin.cn/post/7147323811824664590

欄目分類
最近更新