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 使用成本更高但是更加靈活,可以更精準地實現(xiàn) UI 元素個別屬性的動畫,多個低級別動畫還可以組合實現(xiàn)更復雜的動畫效果。最常見的低級別 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ū)動的動畫方式,即目標對象通過觀察狀態(tài)變化實現(xiàn)自身的動畫。
2. 長按點贊動畫分解
長按點贊的動畫乍看之下非常復雜,但是稍加分解后,不難發(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 基于當前狀態(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, //目標值 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è)置當前動畫的軌跡,后文會詳細介紹。
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ā)生增減時,可以被觀察到,而當我們觀察到新的 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ā)射給下游。當通過 Flow 收集到新加入的 AnimtaedRainbow
時,調(diào)用 startAnim
啟動動畫,這里充分發(fā)揮了掛起函數(shù)的優(yōu)勢,同步等待動畫執(zhí)行完畢,從 animatedRainbows
中移除 AnimtaedRainbow
即可。
值得一提的是,MutableStateList
的主要目的是在組合中觀察列表的狀態(tài)變化,本例子的動畫不發(fā)生在組合中(只發(fā)生在重繪中),完全可以使用普通的集合類型替代,這里使用 MutableStateList
有兩個好處:
- 可以響應(yīng)式地觀察列表變化
- 在 LaunchEffect 中響應(yīng)變化并啟動動畫,協(xié)程可以隨當前 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
之后,再次重復這個過程。
除了旋轉(zhuǎn)動畫之外,其他動畫都會在 duration
之后結(jié)束,它們分別在 async
中啟動并行執(zhí)行,awaitAll
等待它們?nèi)拷Y(jié)束。而由于旋轉(zhuǎn)動畫不會結(jié)束,因此不能放到 awaitAll 中,否則 startAnim 的調(diào)用方將永遠無法恢復執(zhí)行。
CubicBezierEasing
透明度動畫中的 easing
指定了一個 CubicBezierEasing
。easing 是動畫衰減效果,即動畫狀態(tài)以何種速率逼近目標值。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) //勻速接近目標值 val LinearEasing: Easing = Easing { fraction -> fraction }
上圖橫軸是時間,縱軸是逼近目標值的進度,可以看到除了 LinearEasing
之外,其它的的曲線變化都滿足 CubicBezierEasing
三階貝塞爾曲線,如果默認 Easing 不符合你的使用要求,可以使用 CubicBezierEasing
,通過參數(shù),自定義合適的曲線效果。比如例子中曲線如下:
這個曲線前半程狀態(tài)值進度非常緩慢,臨近時間結(jié)束才快速逼近最終狀態(tài)。因為我們希望表情動畫全程清晰可見,透明度的衰減盡量后置,默認 easiing 無法提供這種效果,因此我們自定義 CubicBezierEasing
拋物線動畫
再來看一下拋物線動畫的實現(xiàn)。通常我們可以借助拋物線公式,基于一些動畫狀態(tài)變量計算拋物線坐標來實現(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
繪制表情素材即可
//當前 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,//當前位置 alpha = alpha.value, //透明度 ) } }
注意旋轉(zhuǎn)動畫實際上是借助 DrawScope
的 rotate
方法實現(xiàn)的,在 block 內(nèi)部調(diào)用 drawImage
指定當前的 alpha
和 topLeft
即可。
5. 煙花動畫
5.1 狀態(tài)管理
煙花動畫緊跟在表情動畫結(jié)束時發(fā)生,動畫不涉及位置變化,主要是幾個花瓣不斷縮小的過程。花瓣用圓形繪制,動畫狀態(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
,相當于以初始值的 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 修飾符使用,這里就不贅述了。
最后,復習一下本文例子中的內(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ā)的基本思想,這之后再學習其他 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解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11Android自定義RecyclerView實現(xiàn)不固定刻度的刻度尺
這篇文章主要為大家詳細介紹了Android自定義RecyclerView實現(xiàn)不固定刻度的刻度尺,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-07-07ViewPager 與 Fragment相結(jié)合實現(xiàn)微信界面實例代碼
這篇文章主要介紹了ViewPager 與 Fragment相結(jié)合實現(xiàn)微信界面實例代碼的相關(guān)資料,需要的朋友可以參考下2016-07-07android 中ProgressDialog實現(xiàn)全屏效果的示例
本篇文章主要介紹了android 中ProgressDialog實現(xiàn)全屏效果的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11android webview中使用Java調(diào)用JavaScript方法并獲取返回值
這篇文章主要介紹了android webview中使用Java調(diào)用JavaScript方法并獲取返回值,本文直接給出代碼示例,需要的朋友可以參考下2015-03-03Android 添加系統(tǒng)設(shè)置屬性的實現(xiàn)及步驟
這篇文章主要介紹了Android 添加系統(tǒng)設(shè)置屬性的實現(xiàn)及步驟的相關(guān)資料,需要的朋友可以參考下2017-07-07