Android?Jetpack結(jié)構(gòu)運(yùn)用Compose實(shí)現(xiàn)微博長按點(diǎn)贊彩虹效果
原版
效果高仿效果
1. Compose 動畫 API 概覽
Compose 動畫 API 在使用場景的維度上大體分為兩類:高級別 API 和低級別 API。就像編程語言分為高級語言和低級語言一樣,這列高級低級指 API 的易用性:
高級別 API 主打開箱即用,適用于一些 UI 元素的展現(xiàn)/退出/切換等常見場景,例如常見的 AnimatedVisibility
以及 AnimatedContent
等,它們被設(shè)計(jì)成 Composable 組件,可以在聲明式布局中與其他組件融為一體。
//Text通過動畫淡入 var editable by remember { mutableStateOf(true) } AnimatedVisibility(visible = editable) { Text(text = "Edit") }
低級別 API 使用成本更高但是更加靈活,可以更精準(zhǔn)地實(shí)現(xiàn) UI 元素個(gè)別屬性的動畫,多個(gè)低級別動畫還可以組合實(shí)現(xiàn)更復(fù)雜的動畫效果。最常見的低級別 animateFloatAsState
系列了,它們也是 Composable 函數(shù),可以參與 Composition 的組合過程。
//動畫改變 Box 透明度 val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f) Box( Modifier.fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
處于上層的 API 由底層 API 支撐實(shí)現(xiàn),TargetBasedAnimation
是開發(fā)者可直接使用的最低級 API。Animatable 也是一個(gè)相對低級的 API,它是一個(gè)動畫值的包裝器,在協(xié)程中完成狀態(tài)值的變化,向上提供對 animate*AsState
的支撐。它與其他 API 不同,是一個(gè)普通類而非一個(gè) Composable 函數(shù),所以可以在 Composable 之外使用,因此更具靈活性。本例子的動畫主要也是依靠它完成的。
// Animtable 包裝了一個(gè)顏色狀態(tài)值 val color = remember { Animatable(Color.Gray) } LaunchedEffect(ok) { // animateTo 是個(gè)掛起函數(shù),驅(qū)動狀態(tài)之變化 color.animateTo(if (ok) Color.Green else Color.Gray) } Box(Modifier.fillMaxSize().background(color.value))
無論高級別 API 還是低級別 API ,它們都遵循狀態(tài)驅(qū)動的動畫方式,即目標(biāo)對象通過觀察狀態(tài)變化實(shí)現(xiàn)自身的動畫。
2. 長按點(diǎn)贊動畫分解
長按點(diǎn)贊的動畫乍看之下非常復(fù)雜,但是稍加分解后,不難發(fā)現(xiàn)它也是由一些常見的動畫形式組合而成,因此我們可以對其拆解后逐個(gè)實(shí)現(xiàn):
- 彩虹動畫:全屏范圍內(nèi)不斷擴(kuò)散的彩虹效果??梢酝ㄟ^半徑不斷擴(kuò)大的圓形圖案并依次疊加來實(shí)現(xiàn)
- 表情動畫:從按壓位置不斷拋出的表情??梢赃M(jìn)一步拆解為三個(gè)動畫:透明度動畫,旋轉(zhuǎn)動畫以及拋物線軌跡動畫。
- 煙花動畫:拋出的表情在消失時(shí)會有一個(gè)煙花炸裂的效果。其實(shí)就是圍繞中心的八個(gè)圓點(diǎn)逐漸消失的過程,圓點(diǎn)的顏色提取自表情本身。
傳統(tǒng)視圖動畫可以作用在 View 上,通過動畫改變其屬性;也可以在 onDraw
中通過不斷重繪實(shí)現(xiàn)逐幀的動畫效果。 Compose 也同樣,我們可以在 Composable 中觀察動畫狀態(tài),通過重組實(shí)現(xiàn)動畫效果(本質(zhì)是改變 UI 組件的布局屬性),也可以在 Canvas 中觀察動畫狀態(tài),只在重繪中實(shí)現(xiàn)動畫(跳過組合)。這個(gè)例子的動畫效果也需要通過 Canvas 的不斷重繪來實(shí)現(xiàn)。
Compose 的 Canvas 也可以像 Composable 一樣聲明式的調(diào)用,基本寫法如下:
Canvas {
...
drawRainbow(rainbowState) //繪制彩虹
...
drawEmoji(emojiState) //繪制表情
...
drawFlow(flowState) //繪制煙花
...
}
State 的變化會驅(qū)動 Canvas 會自動重繪,無需手動調(diào)用 invalidate
之類的方法。那么接下來針對彩虹、表情、煙花等各種動畫的實(shí)現(xiàn),我們的工作主要有兩個(gè):
- 狀態(tài)管理:定義相關(guān) State,并在在動畫中驅(qū)動其變化,如前所述這主要依靠 Animatable 實(shí)現(xiàn)。
- 內(nèi)容繪制:通過 Canvas API 基于當(dāng)前狀態(tài)繪制圖案
3. 彩虹動畫
3.1 狀態(tài)管理
對于彩虹動畫,唯一的動畫狀態(tài)就是圓的半徑,其值從 0F 過渡到 screensize,圓形面積鋪滿至整個(gè)屏幕。我們使用 Animatable
包裝這個(gè)狀態(tài)值,調(diào)用 animateTo
方法可以驅(qū)動狀態(tài)變化:
val raduis = Animatable(0f) //初始值 0f radius.animateTo( targetValue = screenSize, //目標(biāo)值 animationSpec = tween( durationMillis = duration, //動畫時(shí)長 easing = FastOutSlowInEasing //動畫衰減效果 ) )
animationSpec
用來指定動畫規(guī)格,不同的動畫規(guī)格決定了了狀態(tài)值變化的節(jié)奏。Compose 中常用的創(chuàng)建動畫規(guī)格的方法有以下幾種,它們創(chuàng)建不同類型的動畫規(guī)格,但都是 AnimationSpec
的子類:
- tween:創(chuàng)建補(bǔ)間動畫規(guī)格,補(bǔ)間動畫是一個(gè)固定時(shí)長動畫,比如上面例子中這樣設(shè)置時(shí)長 duration,此外,tween 還能通過 easiing 指定動畫衰減效果,后文詳細(xì)介紹。
- spring: 彈跳動畫:spring 可以創(chuàng)建基于物理特性的彈簧動畫,它通過設(shè)置阻尼比實(shí)現(xiàn)符合物理規(guī)律的動畫衰減,因此不需要也不能指定動畫時(shí)長
- Keyframes:創(chuàng)建關(guān)鍵幀動畫規(guī)格,關(guān)鍵幀動畫可以逐幀設(shè)置當(dāng)前動畫的軌跡,后文會詳細(xì)介紹。
AnimatedRainbow
要實(shí)現(xiàn)上面這樣多個(gè)彩虹疊加的效果,我們還需有多個(gè) Animtable
同時(shí)運(yùn)行,在 Canvas 中依次對它們進(jìn)行繪制。繪制彩虹除了依靠 Animtable 的狀態(tài)值,還有 Color 等其他信息,因此我們定義一個(gè) AnimatedRainbow
類保存包括 Animtable 在內(nèi)的繪制所需的的狀態(tài)
class AnimatedRainbow( //屏幕尺寸(寬邊長邊大的一方) private val screenSize: Float, //RainbowColors是彩虹的候選顏色 private val color: Brush = RainbowColors.random(), //動畫時(shí)長 private val duration: Int = 3000 ) { private val radius = Animatable(0f) suspend fun startAnim() = radius.animateTo( targetValue = screenSize * 1.6f, // 關(guān)于 1.6f 后文說明 animationSpec = tween( durationMillis = duration, easing = FastOutSlowInEasing ) ) }
animatedRainbows 列表
我們還需要一個(gè)集合來管理運(yùn)行中的 AnimatedRainbow
。這里我們使用 Compose 的 MutableStateList
作為集合容器,MutableStateList
中的元素發(fā)生增減時(shí),可以被觀察到,而當(dāng)我們觀察到新的 AnimatedRainbow
被添加時(shí),為它啟動動畫。關(guān)鍵代碼如下:
//MutableStateList 保存 AnimatedRainbow val animatedRainbows = mutableStateListOf<AnimatedRainbow>() //長按屏幕時(shí),向列表加入 AnimtaedRainbow, 意味著增加一個(gè)新的彩虹 animatedRainbows.add( AnimatedRainbow( screenHeightPx.coerceAtLeast(screenWidthPx), RainbowColors.random() ) )
我們使用 LaunchedEffect
+ snapshotFlow
觀察 animatedRainbows 的變化,代碼如下:
LaunchedEffect(Unit) { //監(jiān)聽到新添加的 AnimatedRainbow snapshotFlow { animatedRainbows.lastOrNull() } .filterNotNull() .collect { launch { //啟動 AnimatedRainbow 動畫 val result = it.startAnim() //動畫結(jié)束后,從列表移除,避免泄露 if (result.endReason == AnimationEndReason.Finished) { animatedRainbows.remove(it) } } } }
LaunchedEffect
和 snapshotFlow
都是 Compose 處理副作用的 API,由于不是本文重點(diǎn)就不做深入介紹了,這里只需要知道 LaunchedEffect
是一個(gè)提供了執(zhí)行副作用的協(xié)程環(huán)境,而 snapshotFlow
可以將 animatedRainbows
中的變化轉(zhuǎn)化為 Flow 發(fā)射給下游。當(dāng)通過 Flow 收集到新加入的 AnimtaedRainbow
時(shí),調(diào)用 startAnim
啟動動畫,這里充分發(fā)揮了掛起函數(shù)的優(yōu)勢,同步等待動畫執(zhí)行完畢,從 animatedRainbows
中移除 AnimtaedRainbow
即可。
值得一提的是,MutableStateList
的主要目的是在組合中觀察列表的狀態(tài)變化,本例子的動畫不發(fā)生在組合中(只發(fā)生在重繪中),完全可以使用普通的集合類型替代,這里使用 MutableStateList
有兩個(gè)好處:
- 可以響應(yīng)式地觀察列表變化
- 在 LaunchEffect 中響應(yīng)變化并啟動動畫,協(xié)程可以隨當(dāng)前 Composable 的生命周期結(jié)束而終止,避免泄露。
3.2 內(nèi)容繪制
我們在 Canvas 中遍歷 animatedRainbows 所有的 AnimtaedRainbow 完成彩虹的繪制。彩虹的圖形主要依靠 DrawScope
的 drawCircle
完成,比較簡單。一點(diǎn)需要特別注意,彩虹動畫結(jié)束時(shí)也要以一個(gè)圓形圖案逐漸退出直至漏出底部內(nèi)容,要實(shí)現(xiàn)這個(gè)效果,用到一個(gè)小技巧,我們的圓形繪制使用空心圓 (Stroke ) 而非 實(shí)心圓( Fill )
- 出現(xiàn)彩虹:圓環(huán)逐漸鋪滿屏幕卻不能漏出空心。這要求 StrokeWidth 寬度覆蓋 ScreenSize,且始終保持 CircleRadius 的兩倍
- 結(jié)束彩虹:圓環(huán)空心部分逐漸覆蓋屏幕。此時(shí)要求 CircleRadius 減去 StrokeWidth / 2 之后依然能覆蓋 ScreenSize
基于以上原則,我們?yōu)?AnimatedRainbow 添加單個(gè) AnnimatedRainbow 的繪制方法:
fun DrawScope.draw() { drawCircle( brush = color, //圓環(huán)顏色 center = center, //圓心:點(diǎn)贊位置 radius = radius.value,// Animtable 中變化的 radius 值, style = Stroke((radius.value * 2).coerceAtMost(_screenSize)), ) }
如上,StrokeWidth 覆蓋 ScreenSize 之后無需繼續(xù)增長,而 CircleRadius 的最終尺寸除去 ScreenSize 之外還要將 StrokeWidth 考慮進(jìn)去,因此前面代碼中將 Animtable 的 targetValue 設(shè)置為 ScreenSize 的 1.6 倍。
4. 表情動畫
4.1 狀態(tài)管理
表情動畫又由三個(gè)子動畫組成:旋轉(zhuǎn)動畫、透明度動畫以及拋物線軌跡動畫。像 AnimtaedRainbow 一樣,我們定義 AnimatedEmoji
管理每個(gè)表情動畫的狀態(tài),AnimatedEmoji 中通過多個(gè) Animatable 分別管理前面提到的幾個(gè)子動畫
AnimatedEmoji
class AnimatedEmoji( private val start: Offset, //表情拋點(diǎn)位置,即長按的屏幕位置 private val screenWidth: Float, //屏幕寬度 private val screenHeight: Float, //屏幕高度 private val duration: Int = 1500 //動畫時(shí)長 ) { //拋出距離(x方向移動終點(diǎn)),在左右一個(gè)屏幕之間取隨機(jī)數(shù) private val throwDistance by lazy { ((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random() } //拋出高度(y方向移動終點(diǎn)),在屏幕頂端到拋點(diǎn)之間取隨機(jī)數(shù) private val throwHeight by lazy { (0..start.y.toInt()).random() } private val x = Animatable(start.x)//x方向移動動畫值 private val y = Animatable(start.y)//y方向移動動畫值 private val rotate = Animatable(0f)//旋轉(zhuǎn)動畫值 private val alpha = Animatable(1f)//透明度動畫值 suspend fun CoroutineScope.startAnim() { async { //執(zhí)行旋轉(zhuǎn)動畫 rotate.animateTo( 360f, infiniteRepeatable( animation = tween(_duration / 2, easing = LinearEasing), repeatMode = RepeatMode.Restart ) ) } awaitAll( async { //執(zhí)行x方向移動動畫 x.animateTo( throwDistance.toFloat(), animationSpec = tween(durationMillis = duration, easing = LinearEasing) ) }, async { //執(zhí)行y方向移動動畫(上升) y.animateTo( throwHeight.toFloat(), animationSpec = tween( duration / 2, easing = LinearOutSlowInEasing ) ) //執(zhí)行y方向移動動畫(下降) y.animateTo( screenHeight, animationSpec = tween( duration / 2, easing = FastOutLinearInEasing ) ) }, async { //執(zhí)行透明度動畫,最終狀態(tài)是半透明 alpha.animateTo( 0.5f, tween(duration, easing = CubicBezierEasing(1f, 0f, 1f, 0.8f)) ) } ) }
infiniteRepeatable
上面代碼中,旋轉(zhuǎn)動畫的 AnimationSpec 使用 infiniteRepeatable
創(chuàng)建了一個(gè)無限循環(huán)的動畫,RepeatMode.Restart
表示它的從 0F
過渡到 360F
之后,再次重復(fù)這個(gè)過程。
除了旋轉(zhuǎn)動畫之外,其他動畫都會在 duration
之后結(jié)束,它們分別在 async
中啟動并行執(zhí)行,awaitAll
等待它們?nèi)拷Y(jié)束。而由于旋轉(zhuǎn)動畫不會結(jié)束,因此不能放到 awaitAll 中,否則 startAnim 的調(diào)用方將永遠(yuǎn)無法恢復(fù)執(zhí)行。
CubicBezierEasing
透明度動畫中的 easing
指定了一個(gè) CubicBezierEasing
。easing 是動畫衰減效果,即動畫狀態(tài)以何種速率逼近目標(biāo)值。Compose 提供了幾個(gè)默認(rèn)的 Easing 類型可供使用,分別是:
//默認(rèn)的 Easing 類型,以加速度起步,減速度收尾 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) //勻速接近目標(biāo)值 val LinearEasing: Easing = Easing { fraction -> fraction }
上圖橫軸是時(shí)間,縱軸是逼近目標(biāo)值的進(jìn)度,可以看到除了 LinearEasing
之外,其它的的曲線變化都滿足 CubicBezierEasing
三階貝塞爾曲線,如果默認(rèn) Easing 不符合你的使用要求,可以使用 CubicBezierEasing
,通過參數(shù),自定義合適的曲線效果。比如例子中曲線如下:
這個(gè)曲線前半程狀態(tài)值進(jìn)度非常緩慢,臨近時(shí)間結(jié)束才快速逼近最終狀態(tài)。因?yàn)槲覀兿M砬閯赢嬋糖逦梢?,透明度的衰減盡量后置,默認(rèn) easiing 無法提供這種效果,因此我們自定義 CubicBezierEasing
拋物線動畫
再來看一下拋物線動畫的實(shí)現(xiàn)。通常我們可以借助拋物線公式,基于一些動畫狀態(tài)變量計(jì)算拋物線坐標(biāo)來實(shí)現(xiàn)動畫,但這個(gè)例子中我們借助 Easing 更加巧妙的實(shí)現(xiàn)了拋物線動畫。
我們將拋物線動畫拆解為 x 軸和 y 軸兩個(gè)方向兩個(gè)并行執(zhí)行的位移動畫,x 軸位移通過 LinearEasing 勻速完成,y 軸又拆分成兩個(gè)過程
上升到最高點(diǎn),使用 LinearOutSlowInEasing 上升時(shí)速度加速衰減
下落到屏幕底端,使用 FastOutLinearInEasing 下落時(shí)速度加速增加
上升和下降的 Easing 曲線互相對稱,符合拋物線規(guī)律
animatedEmojis 列表
像彩虹動畫一樣,我們同樣使用一個(gè) MutableStateList 集合管理 AnimatedEmoji 對象,并在 LaunchedEffect 中監(jiān)聽新元素的插入,并執(zhí)行動畫。只是表情動畫每次會批量增加多個(gè)
//MutableStateList 保存 animatedEmojis val animatedEmojis = mutableStateListOf<AnimatedEmoji>() //一次增加 EmojiCnt 個(gè)表情 animatedEmojis.addAll(buildList { repeat(EmojiCnt) { add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res)) } }) //監(jiān)聽 animatedEmojis 變化 LaunchedEffect(Unit) { //監(jiān)聽到新加入的 EmojiCnt 個(gè)表情 snapshotFlow { animatedEmojis.takeLast(EmojiCnt) } .flatMapMerge { it.asFlow() } .collect { launch { with(it) { startAnim()//啟動表情動畫,等待除了旋轉(zhuǎn)動畫外的所有動畫結(jié)束 animatedEmojis.remove(it) //從列表移除 } } } }
4.2 內(nèi)容繪制
單個(gè) AnimatedEmoji 繪制代碼很簡單,借助 DrawScope
的 drawImage
繪制表情素材即可
//當(dāng)前 x,y 位移的位置 val offset get() = Offset(x.value, y.value) //圖片topLeft相對于offset的距離 val d by lazy { Offset(img.width / 2f, img.height / 2f) } //繪制表情 fun DrawScope.draw() { rotate(rotate.value, pivot = offset) { drawImage( image = img, //表情素材 topLeft = offset - dCenter,//當(dāng)前位置 alpha = alpha.value, //透明度 ) } }
注意旋轉(zhuǎn)動畫實(shí)際上是借助 DrawScope
的 rotate
方法實(shí)現(xiàn)的,在 block 內(nèi)部調(diào)用 drawImage
指定當(dāng)前的 alpha
和 topLeft
即可。
5. 煙花動畫
5.1 狀態(tài)管理
煙花動畫緊跟在表情動畫結(jié)束時(shí)發(fā)生,動畫不涉及位置變化,主要是幾個(gè)花瓣不斷縮小的過程?;ò暧脠A形繪制,動畫狀態(tài)值就是圓形半徑,使用 Animatable 包裝。
AnimatedFlower
煙花的繪制還要用到顏色等信息,我們定義 AnimatedFlower 保存包括 Animtable 在內(nèi)的相關(guān)狀態(tài)。
class AnimatedFlower( private val intial: Float, //花瓣半徑初始值,一般是表情的尺寸 private val duration: Int = 2500 ) { //花瓣半徑 private val radius = Animatable(intial) suspend fun startAnim() { radius.animateTo(0f, keyframes { durationMillis = duration intial / 3 at 0 with FastOutLinearInEasing intial / 5 at (duration * 0.95f).toInt() }) }
keyframes
這里又出現(xiàn)了一種 AnimationSpec,即幀動畫 keyframes
,相對于 tween ,keyframes
可以更精確指定時(shí)間區(qū)間內(nèi)的動畫進(jìn)度。比如代碼中 radius / 3 at 0
表示 0 秒時(shí)狀態(tài)值達(dá)到 intial / 3
,相當(dāng)于以初始值的 1/3
尺寸出現(xiàn),這是一般的 tween 難以實(shí)現(xiàn)的。另外我們希望花瓣可以持久可見,所以使用 keyframe
確保時(shí)間進(jìn)行到 95% 時(shí),radius 的尺寸仍然清晰可見。
animatedFlower 列表
由于煙花動畫設(shè)計(jì)是表情動畫的延續(xù),所以它緊跟表情動畫執(zhí)行,共享 CoroutienScope ,不需要借助 LaunchedEffect ,所以使用普通列表定義 animatedFlower 即可:
//animatedFlowers 使用普通列表創(chuàng)建 val animatedFlowers = mutableListOf<AnimatedFlower>() launch { with(it) {//表情動畫執(zhí)行 startAnim() animatedEmojis.remove(it) } //創(chuàng)建 AnimatedFlower 動畫 val anim = AnimatedFlower( center = it.offset, //使用 Palette 從表情圖片提取煙花顏色 color = Palette.from(it.img.asAndroidBitmap()).generate().let { arrayOf( Color(it.getDominantColor(Color.Transparent.toArgb())), Color(it.getVibrantColor(Color.Transparent.toArgb())) ) }, initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat() ) animatedFlowers.add(anim) //添加進(jìn)列表 anim.startAnim() //執(zhí)行煙花動畫 animatedFlowers.remove(anim) //移除動畫 }
5.2 內(nèi)容繪制
煙花的內(nèi)容繪制,需要計(jì)算每個(gè)花瓣的位置,一共8個(gè)花瓣,各自位置計(jì)算如下:
//計(jì)算 sin45 的值 val sin by lazy { sin(Math.PI / 4).toFloat() } val points get() = run { val d1 = initial - radius.value val d2 = (initial - radius.value) * sin arrayOf( center.copy(y = center.y - d1), //0點(diǎn)方向 center.copy(center.x + d2, center.y - d2), center.copy(x = center.x + d1),//3點(diǎn)方向 center.copy(center.x + d2, center.y + d2), center.copy(y = center.y + d1),//6點(diǎn)方向 center.copy(center.x - d2, center.y + d2), center.copy(x = center.x - d1),//9點(diǎn)方向 center.copy(center.x - d2, center.y - d2), ) }
center
是煙花的中心位置,隨著花瓣的變小,同時(shí)越來越遠(yuǎn)離中心位置,因此 d1
和 d2
就是偏離 center 的距離,與 radius 大小成反比。
最后在 Canvas 中繪制這些 points 即可:
fun DrawScope.draw() { points.forEachIndexed { index, point -> drawCircle(color = color[index % 2], center = point, radius = radius.value) } }
6. 合體效果
最后我們定義一個(gè) AnimatedLike
的 Composable ,整合上面代碼
@Composable fun AnimatedLike(modifier: Modifier = Modifier, state: LikeAnimState = rememberLikeAnimState()) { LaunchedEffect(Unit) { //監(jiān)聽新增表情 snapshotFlow { state.animatedEmojis.takeLast(EmojiCnt) } .flatMapMerge { it.asFlow() } .collect { launch { with(it) { startAnim() state.animatedEmojis.remove(it) } //添加煙花動畫 val anim = AnimatedFlower( center = it.offset, color = Palette.from(it.img.asAndroidBitmap()).generate().let { arrayOf( Color(it.getDominantColor(Color.Transparent.toArgb())), Color(it.getVibrantColor(Color.Transparent.toArgb())) ) }, initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat() ) state.animatedFlowers.add(anim) anim.startAnim() state.animatedFlowers.remove(anim) } } } LaunchedEffect(Unit) { //監(jiān)聽新增彩虹 snapshotFlow { state.animatedRainbows.lastOrNull() } .filterNotNull() .collect { launch { val result = it.startAnim() if (result.endReason == AnimationEndReason.Finished) { state.animatedRainbows.remove(it) } } } } //繪制動畫 Canvas(modifier.fillMaxSize()) { //繪制彩虹 state.animatedRainbows.forEach { animatable -> with(animatable) { draw() } } //繪制表情 state.animatedEmojis.forEach { animatable -> with(animatable) { draw() } } //繪制煙花 state.animatedFlowers.forEach { animatable -> with(animatable) { draw() } } } }
我們使用 AnimatedLike
布局就可以為頁面添加動畫效果了,由于 Canvas 本身是基于 modifier.drawBehind
實(shí)現(xiàn)的,我們也可以將 AnimatedLike 改為 Modifier 修飾符使用,這里就不贅述了。
最后,復(fù)習(xí)一下本文例子中的內(nèi)容:
Animatable
:包裝動畫狀態(tài)值,并且在協(xié)程中執(zhí)行動畫,同步返回動畫結(jié)果AnimationSpec
:動畫規(guī)格,可以配置動畫時(shí)長、Easing 等,例子中用到了 tween,keyframes,infiniteRepeatable 等多個(gè)動畫規(guī)格Easing
:動畫狀態(tài)值隨時(shí)間變化的趨勢,通常使用默認(rèn)類型即可, 也可以基于 CubicBezierEasing 定制。
一個(gè)例子不可能覆蓋到 Compose 所有的動畫 API ,但是借由這個(gè)例子我們可以掌握一些基礎(chǔ) API 的使用,了解 Compose 動畫開發(fā)的基本思想,這之后再學(xué)習(xí)其他 API 就是水到渠成的事情了。
到此這篇關(guān)于Android Jetpack結(jié)構(gòu)運(yùn)用Compose實(shí)現(xiàn)微博長按點(diǎn)贊彩虹效果的文章就介紹到這了,更多相關(guān)Android Jetpack Compose內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 融會貫通Android?Jetpack?Compose中的Snackbar
- Android?Jetpack?Compose開發(fā)實(shí)用小技巧
- Android開發(fā)Jetpack?Compose元素Modifier特性詳解
- Android JetpackCompose使用教程講解
- Android中分析Jetpack?Compose動畫內(nèi)部的實(shí)現(xiàn)原理
- Android Jetpack Compose實(shí)現(xiàn)列表吸頂效果
- Android Jetpack Compose無限加載列表
- Android使用Jetpack Compose開發(fā)零基礎(chǔ)起步教程
相關(guān)文章
Android異步消息處理機(jī)制實(shí)現(xiàn)原理詳解
這篇文章主要介紹了Android異步消息處理機(jī)制實(shí)現(xiàn)原理詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09詳解關(guān)于AndroidQ獲取不到imsi解決方案
這篇文章主要介紹了詳解關(guān)于AndroidQ獲取不到imsi解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Android自定義RecyclerView實(shí)現(xiàn)不固定刻度的刻度尺
這篇文章主要為大家詳細(xì)介紹了Android自定義RecyclerView實(shí)現(xiàn)不固定刻度的刻度尺,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07ViewPager 與 Fragment相結(jié)合實(shí)現(xiàn)微信界面實(shí)例代碼
這篇文章主要介紹了ViewPager 與 Fragment相結(jié)合實(shí)現(xiàn)微信界面實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-07-07android 中ProgressDialog實(shí)現(xiàn)全屏效果的示例
本篇文章主要介紹了android 中ProgressDialog實(shí)現(xiàn)全屏效果的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11android webview中使用Java調(diào)用JavaScript方法并獲取返回值
這篇文章主要介紹了android webview中使用Java調(diào)用JavaScript方法并獲取返回值,本文直接給出代碼示例,需要的朋友可以參考下2015-03-03Android 添加系統(tǒng)設(shè)置屬性的實(shí)現(xiàn)及步驟
這篇文章主要介紹了Android 添加系統(tǒng)設(shè)置屬性的實(shí)現(xiàn)及步驟的相關(guān)資料,需要的朋友可以參考下2017-07-07