高效動畫實現(xiàn)原理-Jetpack Compose 初探索

一、簡介

Jetpack Compose是Google推出的用于構(gòu)建原生界面的新Android 工具包燕刻,它可簡化并加快 Android上的界面開發(fā)窥翩。Jetpack Compose是一個聲明式的UI框架,隨著該框架的推出宪哩,標(biāo)志著Android 開始全面擁抱聲明式UI開發(fā)常熙。Jetpack Compose存在很多優(yōu)點:代碼更加簡潔直觀纬乍、應(yīng)用開發(fā)效率顯著提升、Kotlin API功能直觀裸卫、預(yù)覽工具強大等仿贬。

二、開發(fā)環(huán)境

為了獲得更好的開發(fā)體驗墓贿,筆者這里使用的是Android Studio Canary版本茧泪,這樣可以無需配置一些設(shè)置和依賴。(下載地址

打開工程聋袋,新建Empty Compose activity 模版队伟,需要注意的是根目錄下的build.gradle,相關(guān)的依賴com.android.tools.build和org.jetbrains.kotlin版本需要對應(yīng)舱馅,否則可能出現(xiàn)出錯的情形,這里使用的是:

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,可用于在應(yīng)用界面中輕松實現(xiàn)各種動畫效果缠借。下文將會對Jetpack Compose Animations的常用方法進行介紹干毅。

3.1 狀態(tài)驅(qū)動動畫:State

Jetpack Compose動畫是通過對狀態(tài)的監(jiān)聽,即監(jiān)聽狀態(tài)值的變化泼返,使UI能實現(xiàn)自動更新硝逢。可組合函數(shù)可以使用 remember或者 mutableStateOf監(jiān)聽狀態(tài)值的變化。如果狀態(tài)值是不變的渠鸽,remember函數(shù)會在每次重新組合中保持該值叫乌;如果狀態(tài)是可變的,它會在值發(fā)生變化的時候觸發(fā)重組徽缚,mutableStateOf將得到一個MutableState對象憨奸,它是一個可觀察類型。

這種重組是創(chuàng)建狀態(tài)驅(qū)動動畫的關(guān)鍵凿试。利用重組排宰,它們會在可組合組件的狀態(tài)發(fā)生任何變化時被觸發(fā)。Compose動畫是由State驅(qū)動的那婉,動畫相關(guān)的API也較容易上手板甘,能比較容易創(chuàng)造出漂亮的聲明式動畫。

3.2 可見性動畫: AnimatedVisibility

首先看下函數(shù)定義:

@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)
}

可以看出默認(rèn)的動畫是淡入放大详炬、淡出收縮盐类,實際中通過傳入不同函數(shù)實現(xiàn)各種動效。

隨著可見值的變化痕寓,AnimatedVisibility可為其內(nèi)容的出現(xiàn)和消失設(shè)置動畫傲醉。如下代碼,可以通過點擊Button呻率,控制圖片的出現(xiàn)和消失硬毕。

@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()
            )
        }
    }
}

通過監(jiān)聽visible的變化,可實現(xiàn)圖片的可見性動畫礼仗,效果如小圖所示吐咳;

image

3.3 布局大小動畫:AnimateContentSize

先看下函數(shù)的定義:

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

可以為布局大小動畫設(shè)置動畫速度和監(jiān)聽值。

由函數(shù)的定義可以看出這個函數(shù)本質(zhì)上就Modefier的一個擴展函數(shù)元践【录梗可以通過變量size監(jiān)聽狀態(tài)變化實現(xiàn)布局大小的動畫效果,代碼如下:

//放大縮小動畫 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的點擊单旁,監(jiān)聽size值的變化沪羔,利用animateContentSize()實現(xiàn)動畫效果,具體動效如下圖所示:

image

3.4布局切換動畫: Crossfade

Crossfade可以通過監(jiān)聽狀態(tài)值的變化象浑,使用淡入淡出的動畫在兩個布局之間添加動畫效果蔫饰,函數(shù)自身就是一個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)
                )
            }
        }

    }

同樣通過監(jiān)聽fadeStatus的值愉豺,實現(xiàn)布局切換的動畫篓吁,具體的動效如圖所示:

image

3.5單個值動畫:animate*AsState

為單個值添加動畫效果。只需提供結(jié)束值(或目標(biāo)值)蚪拦,該 API 就會從當(dāng)前值開始向指定值播放動畫杖剪。

Jetpack Compose 提供了很多內(nèi)置函數(shù)冻押,可以為不同類型的數(shù)據(jù)制作動畫,例如: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)
            )
        }
}


動畫效果如下圖所示:

image

3.6 組合動畫:updateTransition

Transition 可同時追蹤一個或多個動畫狼渊,并在多個狀態(tài)之間同步這些動畫。具體的代碼如下:

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分別為定義的枚舉類、自定義函數(shù)蚤认。

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
    }

動畫的如下圖所示:

image

四米苹、結(jié)語

Jetpack Compose 已將動畫簡化到只需在我們的可組合函數(shù)中創(chuàng)建聲明性代碼的程度,只需編寫希望 UI 動畫的方式砰琢,其余部分由 Compose 管理蘸嘶。最后,這也是是 Jetpack Compose 的主要目標(biāo):創(chuàng)建一個聲明式 UI 工具包來加速應(yīng)用程序開發(fā)并提高代碼可讀性和邏輯性陪汽。

Jetpack Compose提供的聲明式UI工具包训唱,能做到使用更少的代碼實現(xiàn)更多的功能,且代碼的可讀性和邏輯性也大大提高了挚冤。

作者:vivo互聯(lián)網(wǎng)游戲客戶端團隊-Ke Jie

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末况增,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子训挡,更是在濱河造成了極大的恐慌澳骤,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澜薄,死亡現(xiàn)場離奇詭異为肮,居然都是意外死亡,警方通過查閱死者的電腦和手機肤京,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門颊艳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人忘分,你說我怎么就攤上這事棋枕。” “怎么了饭庞?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵戒悠,是天一觀的道長熬荆。 經(jīng)常有香客問我舟山,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任累盗,我火速辦了婚禮寒矿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘若债。我一直安慰自己符相,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布蠢琳。 她就那樣靜靜地躺著啊终,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傲须。 梳的紋絲不亂的頭發(fā)上蓝牲,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音泰讽,去河邊找鬼例衍。 笑死,一個胖子當(dāng)著我的面吹牛已卸,可吹牛的內(nèi)容都是我干的佛玄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼累澡,長吁一口氣:“原來是場噩夢啊……” “哼梦抢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起永乌,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惑申,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后翅雏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圈驼,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年望几,在試婚紗的時候發(fā)現(xiàn)自己被綠了绩脆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡橄抹,死狀恐怖靴迫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楼誓,我是刑警寧澤玉锌,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站疟羹,受9級特大地震影響主守,放射性物質(zhì)發(fā)生泄漏禀倔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一参淫、第九天 我趴在偏房一處隱蔽的房頂上張望救湖。 院中可真熱鬧,春花似錦涎才、人聲如沸鞋既。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邑闺。三九已至,卻和暖如春棕兼,著一層夾襖步出監(jiān)牢的瞬間检吆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工程储, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹭沛,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓章鲤,卻偏偏與公主長得像摊灭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子败徊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容