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