高效動畫實現原理-Jetpack Compose 初探索

vivo網際網路技術發表於2021-10-19

一、簡介

Jetpack Compose是Google推出的用於構建原生介面的新Android 工具包,它可簡化並加快 Android上的介面開發。Jetpack Compose是一個宣告式的UI框架,隨著該框架的推出,標誌著Android 開始全面擁抱宣告式UI開發。Jetpack Compose存在很多優點:程式碼更加簡潔直觀、應用開發效率顯著提升、Kotlin API功能直觀、預覽工具強大等。

二、開發環境

為了獲得更好的開發體驗,筆者這裡使用的是Android Studio Canary版本,這樣可以無需配置一些設定和依賴。(下載地址

開啟工程,新建Empty Compose activity 模版,需要注意的是根目錄下的build.gradle,相關的依賴com.android.tools.build和org.jetbrains.kotlin版本需要對應,否則可能出現出錯的情形,這裡使用的是:

dependencies {
	classpath "com.android.tools.build:gradle:7.0.0-alpha15"
	classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30"
}

這樣就完成了專案的新建。

三、Jetpack Compose動畫

Jetpack Compose提供了一些功能強大且可擴充套件的 API,可用於在應用介面中輕鬆實現各種動畫效果。下文將會對Jetpack Compose Animations的常用方法進行介紹。

3.1 狀態驅動動畫:State

Jetpack Compose動畫是通過對狀態的監聽,即監聽狀態值的變化,使UI能實現自動更新。可組合函式可以使用 remember或者 mutableStateOf監聽狀態值的變化。如果狀態值是不變的,remember函式會在每次重新組合中保持該值;如果狀態是可變的,它會在值發生變化的時候觸發重組,mutableStateOf將得到一個MutableState物件,它是一個可觀察型別。

這種重組是建立狀態驅動動畫的關鍵。利用重組,它們會在可組合元件的狀態發生任何變化時被觸發。Compose動畫是由State驅動的,動畫相關的API也較容易上手,能比較容易創造出漂亮的宣告式動畫。

3.2 可見性動畫: AnimatedVisibility

首先看下函式定義:

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    initiallyVisible: Boolean = visible,
    content: @Composable () -> Unit
) {
    AnimatedVisibilityImpl(visible, modifier, enter, exit, initiallyVisible, content)
}

可以看出預設的動畫是淡入放大、淡出收縮,實際中通過傳入不同函式實現各種動效。

隨著可見值的變化,AnimatedVisibility可為其內容的出現和消失設定動畫。如下程式碼,可以通過點選Button,控制圖片的出現和消失。

@Composable
fun AinmationDemo() {

    //AnimatedVisibility 可見動畫
    var visible by remember { mutableStateOf(true) }

    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Button(
            onClick = { visible = !visible }
        ) {
            Text(text = if (visible) "Hide" else "Show")
        }

        Spacer(Modifier.height(16.dp))

        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Image(
                painter = painterResource(id = R.drawable.pikaqiu),
                contentDescription = null,
                Modifier.fillMaxSize()
            )
        }
    }
}

通過監聽visible的變化,可實現圖片的可見性動畫,效果如小圖所示;

3.3 佈局大小動畫:AnimateContentSize

先看下函式的定義:

fun Modifier.animateContentSize(
    animationSpec: FiniteAnimationSpec<IntSize> = spring(),
    finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
)

可以為佈局大小動畫設定動畫速度和監聽值。

由函式的定義可以看出這個函式本質上就Modefier的一個擴充套件函式。可以通過變數size監聽狀態變化實現佈局大小的動畫效果,程式碼如下:

//放大縮小動畫 animateContentSize
    var size by remember { mutableStateOf(Size(300F, 300F)) }

    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Spacer(Modifier.height(16.dp))

        Button(
            onClick = {
                size = if (size.height == 300F) {
                    Size(500F, 500F)
                } else {
                    Size(300F, 300F)
                }
            }
        ) {
            Text(if (size.height == 300F) "Shrink" else "Expand")
        }
        Spacer(Modifier.height(16.dp))

        Box(
            Modifier
                .animateContentSize()
        ) {
            Image(
                painter = painterResource(id = R.drawable.pikaqiu),
                contentDescription = null,
                Modifier
                    .animateContentSize()
                    .size(size = size.height.dp)
            )
        }
} //放大縮小動畫 animateContentSize    var size by remember { mutableStateOf(Size(300F, 300F)) }​    Column(        Modifier            .fillMaxWidth()            .fillMaxHeight(),        Arrangement.Top,        Alignment.CenterHorizontally    ) {        Spacer(Modifier.height(16.dp))​        Button(            onClick = {                size = if (size.height == 300F) {                    Size(500F, 500F)                } else {                    Size(300F, 300F)                }            }        ) {            Text(if (size.height == 300F) "Shrink" else "Expand")        }        Spacer(Modifier.height(16.dp))​        Box(            Modifier                .animateContentSize()        ) {            Image(                painter = painterResource(id = R.drawable.pikaqiu),                contentDescription = null,                Modifier                    .animateContentSize()                    .size(size = size.height.dp)            )        }}

通過Button的點選,監聽size值的變化,利用animateContentSize()實現動畫效果,具體動效如下圖所示:

3.4佈局切換動畫: Crossfade

Crossfade可以通過監聽狀態值的變化,使用淡入淡出的動畫在兩個佈局之間新增動畫效果,函式自身就是一個Composable,程式碼如下:

//Crossfade 淡入淡出動畫
    var fadeStatus by remember { mutableStateOf(true) }

    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Button(
            onClick = { fadeStatus = !fadeStatus }
        ) {
            Text(text = if (fadeStatus) "Fade In" else "Fade Out")
        }

        Spacer(Modifier.height(16.dp))

        Crossfade(targetState = fadeStatus, animationSpec = tween(3000)) { screen ->
            when (screen) {
                true -> Image(
                    painter = painterResource(id = R.drawable.pikaqiu),
                    contentDescription = null,
                    Modifier
                        .animateContentSize()
                        .size(300.dp)
                )
                false -> Image(
                    painter = painterResource(id = R.drawable.pikaqiu2),
                    contentDescription = null,
                    Modifier
                        .animateContentSize()
                        .size(300.dp)
                )
            }
        }

    }

同樣通過監聽fadeStatus的值,實現佈局切換的動畫,具體的動效如圖所示:

3.5單個值動畫:animate*AsState

為單個值新增動畫效果。只需提供結束值(或目標值),該 API 就會從當前值開始向指定值播放動畫。

Jetpack Compose 提供了很多內建函式,可以為不同型別的資料製作動畫,例如:animateColorAsState、animateDpAsState、animateOffsetAsState等,這裡將介紹下animateFooAsState的使用,程式碼如下:

//animate*AsState 單個值新增動畫
    var transparent by remember { mutableStateOf(true) }
    val alpha: Float by animateFloatAsState(if (transparent) 1f else 0.5f)

    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Button(
            onClick = { transparent = !transparent }
        ) {
            Text(if (transparent) "Light" else "Dark")
        }

        Spacer(Modifier.height(16.dp))

        Box {

            Image(
                painter = painterResource(id = R.drawable.pikaqiu),
                contentDescription = null,
                Modifier
                    .animateContentSize()
                    .graphicsLayer(alpha = alpha)
                    .size(300.dp)
            )
        }
}


動畫效果如下圖所示:

3.6 組合動畫:updateTransition

Transition 可同時追蹤一個或多個動畫,並在多個狀態之間同步這些動畫。具體的程式碼如下:

var imagePosition by remember { mutableStateOf(ImagePosition.TopLeft) }

    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Spacer(Modifier.height(16.dp))

        val transition = updateTransition(targetState = imagePosition, label = "")
        val boxOffset by transition.animateOffset(label = "") { position ->
            when (position) {
                ImagePosition.TopLeft -> Offset(-60F, 0F)
                ImagePosition.BottomRight -> Offset(60F, 120F)
                ImagePosition.TopRight -> Offset(60F, 0F)
                ImagePosition.BottomLeft -> Offset(-60F, 120F)
            }
        }
        Button(onClick = {
            imagePosition = ChangePosition(imagePosition)
        }) {
            Text("Change position")
        }
        Box {

            Image(
                painter = painterResource(id = R.drawable.pikaqiu),
                contentDescription = null,
                Modifier
                    .offset(boxOffset.x.dp, boxOffset.y.dp)
                    .animateContentSize()
                    .size(300.dp)
            )
        }
}

其中,ImagePosition、ChangePosition分別為定義的列舉類、自定義函式。

enum class ImagePosition {
    TopRight,
    TopLeft,
    BottomRight,
    BottomLeft
}

fun ChangePosition(position: ImagePosition) =
    when (position) {
        ImagePosition.TopLeft -> ImagePosition.BottomRight
        ImagePosition.BottomRight -> ImagePosition.TopRight
        ImagePosition.TopRight -> ImagePosition.BottomLeft
        ImagePosition.BottomLeft -> ImagePosition.TopLeft
    }

動畫的如下圖所示:

四、結語

Jetpack Compose 已將動畫簡化到只需在我們的可組合函式中建立宣告性程式碼的程度,只需編寫希望 UI 動畫的方式,其餘部分由 Compose 管理。最後,這也是是 Jetpack Compose 的主要目標:建立一個宣告式 UI 工具包來加速應用程式開發並提高程式碼可讀性和邏輯性。

Jetpack Compose提供的宣告式UI工具包,能做到使用更少的程式碼實現更多的功能,且程式碼的可讀性和邏輯性也大大提高了。

作者:vivo網際網路遊戲客戶端團隊-Ke Jie

相關文章