Android?Jetpack結(jié)構(gòu)運用Compose實現(xiàn)微博長按點贊彩虹效果
原版
效果高仿效果


1. Compose 動畫 API 概覽
Compose 動畫 API 在使用場景的維度上大體分為兩類:高級別 API 和低級別 API。就像編程語言分為高級語言和低級語言一樣,這列高級低級指 API 的易用性:
高級別 API 主打開箱即用,適用于一些 UI 元素的展現(xiàn)/退出/切換等常見場景,例如常見的 AnimatedVisibility 以及 AnimatedContent 等,它們被設(shè)計成 Composable 組件,可以在聲明式布局中與其他組件融為一體。
//Text通過動畫淡入
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
低級別 API 使用成本更高但是更加靈活,可以更精準(zhǔn)地實現(xiàn) UI 元素個別屬性的動畫,多個低級別動畫還可以組合實現(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 支撐實現(xiàn),TargetBasedAnimation 是開發(fā)者可直接使用的最低級 API。Animatable 也是一個相對低級的 API,它是一個動畫值的包裝器,在協(xié)程中完成狀態(tài)值的變化,向上提供對 animate*AsState 的支撐。它與其他 API 不同,是一個普通類而非一個 Composable 函數(shù),所以可以在 Composable 之外使用,因此更具靈活性。本例子的動畫主要也是依靠它完成的。
// Animtable 包裝了一個顏色狀態(tài)值
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
// animateTo 是個掛起函數(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)變化實現(xiàn)自身的動畫。
2. 長按點贊動畫分解
長按點贊的動畫乍看之下非常復(fù)雜,但是稍加分解后,不難發(fā)現(xiàn)它也是由一些常見的動畫形式組合而成,因此我們可以對其拆解后逐個實現(xiàn):
- 彩虹動畫:全屏范圍內(nèi)不斷擴散的彩虹效果??梢酝ㄟ^半徑不斷擴大的圓形圖案并依次疊加來實現(xiàn)
- 表情動畫:從按壓位置不斷拋出的表情??梢赃M一步拆解為三個動畫:透明度動畫,旋轉(zhuǎn)動畫以及拋物線軌跡動畫。
- 煙花動畫:拋出的表情在消失時會有一個煙花炸裂的效果。其實就是圍繞中心的八個圓點逐漸消失的過程,圓點的顏色提取自表情本身。
傳統(tǒng)視圖動畫可以作用在 View 上,通過動畫改變其屬性;也可以在 onDraw 中通過不斷重繪實現(xiàn)逐幀的動畫效果。 Compose 也同樣,我們可以在 Composable 中觀察動畫狀態(tài),通過重組實現(xiàn)動畫效果(本質(zhì)是改變 UI 組件的布局屬性),也可以在 Canvas 中觀察動畫狀態(tài),只在重繪中實現(xiàn)動畫(跳過組合)。這個例子的動畫效果也需要通過 Canvas 的不斷重繪來實現(xiàn)。
Compose 的 Canvas 也可以像 Composable 一樣聲明式的調(diào)用,基本寫法如下:
Canvas {
...
drawRainbow(rainbowState) //繪制彩虹
...
drawEmoji(emojiState) //繪制表情
...
drawFlow(flowState) //繪制煙花
...
}
State 的變化會驅(qū)動 Canvas 會自動重繪,無需手動調(diào)用 invalidate 之類的方法。那么接下來針對彩虹、表情、煙花等各種動畫的實現(xiàn),我們的工作主要有兩個:
- 狀態(tài)管理:定義相關(guān) State,并在在動畫中驅(qū)動其變化,如前所述這主要依靠 Animatable 實現(xiàn)。
- 內(nèi)容繪制:通過 Canvas API 基于當(dāng)前狀態(tài)繪制圖案
3. 彩虹動畫
3.1 狀態(tài)管理
對于彩虹動畫,唯一的動畫狀態(tài)就是圓的半徑,其值從 0F 過渡到 screensize,圓形面積鋪滿至整個屏幕。我們使用 Animatable 包裝這個狀態(tài)值,調(diào)用 animateTo 方法可以驅(qū)動狀態(tài)變化:
val raduis = Animatable(0f) //初始值 0f
radius.animateTo(
targetValue = screenSize, //目標(biāo)值
animationSpec = tween(
durationMillis = duration, //動畫時長
easing = FastOutSlowInEasing //動畫衰減效果
)
)animationSpec 用來指定動畫規(guī)格,不同的動畫規(guī)格決定了了狀態(tài)值變化的節(jié)奏。Compose 中常用的創(chuàng)建動畫規(guī)格的方法有以下幾種,它們創(chuàng)建不同類型的動畫規(guī)格,但都是 AnimationSpec 的子類:
- tween:創(chuàng)建補間動畫規(guī)格,補間動畫是一個固定時長動畫,比如上面例子中這樣設(shè)置時長 duration,此外,tween 還能通過 easiing 指定動畫衰減效果,后文詳細介紹。
- spring: 彈跳動畫:spring 可以創(chuàng)建基于物理特性的彈簧動畫,它通過設(shè)置阻尼比實現(xiàn)符合物理規(guī)律的動畫衰減,因此不需要也不能指定動畫時長
- Keyframes:創(chuàng)建關(guān)鍵幀動畫規(guī)格,關(guān)鍵幀動畫可以逐幀設(shè)置當(dāng)前動畫的軌跡,后文會詳細介紹。
AnimatedRainbow

要實現(xiàn)上面這樣多個彩虹疊加的效果,我們還需有多個 Animtable 同時運行,在 Canvas 中依次對它們進行繪制。繪制彩虹除了依靠 Animtable 的狀態(tài)值,還有 Color 等其他信息,因此我們定義一個 AnimatedRainbow 類保存包括 Animtable 在內(nèi)的繪制所需的的狀態(tài)
class AnimatedRainbow(
//屏幕尺寸(寬邊長邊大的一方)
private val screenSize: Float,
//RainbowColors是彩虹的候選顏色
private val color: Brush = RainbowColors.random(),
//動畫時長
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 列表
我們還需要一個集合來管理運行中的 AnimatedRainbow。這里我們使用 Compose 的 MutableStateList 作為集合容器,MutableStateList 中的元素發(fā)生增減時,可以被觀察到,而當(dāng)我們觀察到新的 AnimatedRainbow 被添加時,為它啟動動畫。關(guān)鍵代碼如下:
//MutableStateList 保存 AnimatedRainbow
val animatedRainbows = mutableStateListOf<AnimatedRainbow>()
//長按屏幕時,向列表加入 AnimtaedRainbow, 意味著增加一個新的彩虹
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,由于不是本文重點就不做深入介紹了,這里只需要知道 LaunchedEffect 是一個提供了執(zhí)行副作用的協(xié)程環(huán)境,而 snapshotFlow 可以將 animatedRainbows 中的變化轉(zhuǎn)化為 Flow 發(fā)射給下游。當(dāng)通過 Flow 收集到新加入的 AnimtaedRainbow 時,調(diào)用 startAnim 啟動動畫,這里充分發(fā)揮了掛起函數(shù)的優(yōu)勢,同步等待動畫執(zhí)行完畢,從 animatedRainbows 中移除 AnimtaedRainbow 即可。
值得一提的是,MutableStateList 的主要目的是在組合中觀察列表的狀態(tài)變化,本例子的動畫不發(fā)生在組合中(只發(fā)生在重繪中),完全可以使用普通的集合類型替代,這里使用 MutableStateList 有兩個好處:
- 可以響應(yīng)式地觀察列表變化
- 在 LaunchEffect 中響應(yīng)變化并啟動動畫,協(xié)程可以隨當(dāng)前 Composable 的生命周期結(jié)束而終止,避免泄露。
3.2 內(nèi)容繪制
我們在 Canvas 中遍歷 animatedRainbows 所有的 AnimtaedRainbow 完成彩虹的繪制。彩虹的圖形主要依靠 DrawScope 的 drawCircle 完成,比較簡單。一點需要特別注意,彩虹動畫結(jié)束時也要以一個圓形圖案逐漸退出直至漏出底部內(nèi)容,要實現(xiàn)這個效果,用到一個小技巧,我們的圓形繪制使用空心圓 (Stroke ) 而非 實心圓( Fill )

- 出現(xiàn)彩虹:圓環(huán)逐漸鋪滿屏幕卻不能漏出空心。這要求 StrokeWidth 寬度覆蓋 ScreenSize,且始終保持 CircleRadius 的兩倍
- 結(jié)束彩虹:圓環(huán)空心部分逐漸覆蓋屏幕。此時要求 CircleRadius 減去 StrokeWidth / 2 之后依然能覆蓋 ScreenSize
基于以上原則,我們?yōu)?AnimatedRainbow 添加單個 AnnimatedRainbow 的繪制方法:
fun DrawScope.draw() {
drawCircle(
brush = color, //圓環(huán)顏色
center = center, //圓心:點贊位置
radius = radius.value,// Animtable 中變化的 radius 值,
style = Stroke((radius.value * 2).coerceAtMost(_screenSize)),
)
}如上,StrokeWidth 覆蓋 ScreenSize 之后無需繼續(xù)增長,而 CircleRadius 的最終尺寸除去 ScreenSize 之外還要將 StrokeWidth 考慮進去,因此前面代碼中將 Animtable 的 targetValue 設(shè)置為 ScreenSize 的 1.6 倍。
4. 表情動畫
4.1 狀態(tài)管理
表情動畫又由三個子動畫組成:旋轉(zhuǎn)動畫、透明度動畫以及拋物線軌跡動畫。像 AnimtaedRainbow 一樣,我們定義 AnimatedEmoji 管理每個表情動畫的狀態(tài),AnimatedEmoji 中通過多個 Animatable 分別管理前面提到的幾個子動畫

AnimatedEmoji
class AnimatedEmoji(
private val start: Offset, //表情拋點位置,即長按的屏幕位置
private val screenWidth: Float, //屏幕寬度
private val screenHeight: Float, //屏幕高度
private val duration: Int = 1500 //動畫時長
) {
//拋出距離(x方向移動終點),在左右一個屏幕之間取隨機數(shù)
private val throwDistance by lazy {
((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random()
}
//拋出高度(y方向移動終點),在屏幕頂端到拋點之間取隨機數(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)建了一個無限循環(huán)的動畫,RepeatMode.Restart 表示它的從 0F 過渡到 360F 之后,再次重復(fù)這個過程。
除了旋轉(zhuǎn)動畫之外,其他動畫都會在 duration 之后結(jié)束,它們分別在 async 中啟動并行執(zhí)行,awaitAll 等待它們?nèi)拷Y(jié)束。而由于旋轉(zhuǎn)動畫不會結(jié)束,因此不能放到 awaitAll 中,否則 startAnim 的調(diào)用方將永遠無法恢復(fù)執(zhí)行。
CubicBezierEasing
透明度動畫中的 easing 指定了一個 CubicBezierEasing。easing 是動畫衰減效果,即動畫狀態(tài)以何種速率逼近目標(biāo)值。Compose 提供了幾個默認的 Easing 類型可供使用,分別是:
//默認的 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 }
上圖橫軸是時間,縱軸是逼近目標(biāo)值的進度,可以看到除了 LinearEasing 之外,其它的的曲線變化都滿足 CubicBezierEasing 三階貝塞爾曲線,如果默認 Easing 不符合你的使用要求,可以使用 CubicBezierEasing,通過參數(shù),自定義合適的曲線效果。比如例子中曲線如下:

這個曲線前半程狀態(tài)值進度非常緩慢,臨近時間結(jié)束才快速逼近最終狀態(tài)。因為我們希望表情動畫全程清晰可見,透明度的衰減盡量后置,默認 easiing 無法提供這種效果,因此我們自定義 CubicBezierEasing
拋物線動畫
再來看一下拋物線動畫的實現(xiàn)。通常我們可以借助拋物線公式,基于一些動畫狀態(tài)變量計算拋物線坐標(biāo)來實現(xiàn)動畫,但這個例子中我們借助 Easing 更加巧妙的實現(xiàn)了拋物線動畫。
我們將拋物線動畫拆解為 x 軸和 y 軸兩個方向兩個并行執(zhí)行的位移動畫,x 軸位移通過 LinearEasing 勻速完成,y 軸又拆分成兩個過程
上升到最高點,使用 LinearOutSlowInEasing 上升時速度加速衰減
下落到屏幕底端,使用 FastOutLinearInEasing 下落時速度加速增加
上升和下降的 Easing 曲線互相對稱,符合拋物線規(guī)律
animatedEmojis 列表
像彩虹動畫一樣,我們同樣使用一個 MutableStateList 集合管理 AnimatedEmoji 對象,并在 LaunchedEffect 中監(jiān)聽新元素的插入,并執(zhí)行動畫。只是表情動畫每次會批量增加多個
//MutableStateList 保存 animatedEmojis
val animatedEmojis = mutableStateListOf<AnimatedEmoji>()
//一次增加 EmojiCnt 個表情
animatedEmojis.addAll(buildList {
repeat(EmojiCnt) {
add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res))
}
})
//監(jiān)聽 animatedEmojis 變化
LaunchedEffect(Unit) {
//監(jiān)聽到新加入的 EmojiCnt 個表情
snapshotFlow { animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()//啟動表情動畫,等待除了旋轉(zhuǎn)動畫外的所有動畫結(jié)束
animatedEmojis.remove(it) //從列表移除
}
}
}
}4.2 內(nèi)容繪制
單個 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)動畫實際上是借助 DrawScope 的 rotate 方法實現(xiàn)的,在 block 內(nèi)部調(diào)用 drawImage 指定當(dāng)前的 alpha 和 topLeft 即可。
5. 煙花動畫
5.1 狀態(tài)管理
煙花動畫緊跟在表情動畫結(jié)束時發(fā)生,動畫不涉及位置變化,主要是幾個花瓣不斷縮小的過程?;ò暧脠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 可以更精確指定時間區(qū)間內(nèi)的動畫進度。比如代碼中 radius / 3 at 0 表示 0 秒時狀態(tài)值達到 intial / 3 ,相當(dāng)于以初始值的 1/3 尺寸出現(xiàn),這是一般的 tween 難以實現(xiàn)的。另外我們希望花瓣可以持久可見,所以使用 keyframe 確保時間進行到 95% 時,radius 的尺寸仍然清晰可見。

animatedFlower 列表
由于煙花動畫設(shè)計是表情動畫的延續(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) //添加進列表
anim.startAnim() //執(zhí)行煙花動畫
animatedFlowers.remove(anim) //移除動畫
}5.2 內(nèi)容繪制
煙花的內(nèi)容繪制,需要計算每個花瓣的位置,一共8個花瓣,各自位置計算如下:
//計算 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點方向
center.copy(center.x + d2, center.y - d2),
center.copy(x = center.x + d1),//3點方向
center.copy(center.x + d2, center.y + d2),
center.copy(y = center.y + d1),//6點方向
center.copy(center.x - d2, center.y + d2),
center.copy(x = center.x - d1),//9點方向
center.copy(center.x - d2, center.y - d2),
)
}center 是煙花的中心位置,隨著花瓣的變小,同時越來越遠離中心位置,因此 d1 和 d2 就是偏離 center 的距離,與 radius 大小成反比。
最后在 Canvas 中繪制這些 points 即可:
fun DrawScope.draw() {
points.forEachIndexed { index, point ->
drawCircle(color = color[index % 2], center = point, radius = radius.value)
}
}
6. 合體效果
最后我們定義一個 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 實現(xiàn)的,我們也可以將 AnimatedLike 改為 Modifier 修飾符使用,這里就不贅述了。
最后,復(fù)習(xí)一下本文例子中的內(nèi)容:
Animatable:包裝動畫狀態(tài)值,并且在協(xié)程中執(zhí)行動畫,同步返回動畫結(jié)果AnimationSpec:動畫規(guī)格,可以配置動畫時長、Easing 等,例子中用到了 tween,keyframes,infiniteRepeatable 等多個動畫規(guī)格Easing:動畫狀態(tài)值隨時間變化的趨勢,通常使用默認類型即可, 也可以基于 CubicBezierEasing 定制。
一個例子不可能覆蓋到 Compose 所有的動畫 API ,但是借由這個例子我們可以掌握一些基礎(chǔ) API 的使用,了解 Compose 動畫開發(fā)的基本思想,這之后再學(xué)習(xí)其他 API 就是水到渠成的事情了。
到此這篇關(guān)于Android Jetpack結(jié)構(gòu)運用Compose實現(xiàn)微博長按點贊彩虹效果的文章就介紹到這了,更多相關(guān)Android Jetpack Compose內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解關(guān)于AndroidQ獲取不到imsi解決方案
這篇文章主要介紹了詳解關(guān)于AndroidQ獲取不到imsi解決方案,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Android自定義RecyclerView實現(xiàn)不固定刻度的刻度尺
這篇文章主要為大家詳細介紹了Android自定義RecyclerView實現(xiàn)不固定刻度的刻度尺,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-07-07
ViewPager 與 Fragment相結(jié)合實現(xiàn)微信界面實例代碼
這篇文章主要介紹了ViewPager 與 Fragment相結(jié)合實現(xiàn)微信界面實例代碼的相關(guān)資料,需要的朋友可以參考下2016-07-07
android 中ProgressDialog實現(xiàn)全屏效果的示例
本篇文章主要介紹了android 中ProgressDialog實現(xiàn)全屏效果的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
android webview中使用Java調(diào)用JavaScript方法并獲取返回值
這篇文章主要介紹了android webview中使用Java調(diào)用JavaScript方法并獲取返回值,本文直接給出代碼示例,需要的朋友可以參考下2015-03-03
Android 添加系統(tǒng)設(shè)置屬性的實現(xiàn)及步驟
這篇文章主要介紹了Android 添加系統(tǒng)設(shè)置屬性的實現(xiàn)及步驟的相關(guān)資料,需要的朋友可以參考下2017-07-07

