Jetpack Compose 波浪進度載入
theme: smartblue highlight: androidstudio
我正在參加中秋創意投稿大賽,詳情請看:中秋創意投稿大賽
受到 波浪動畫很常見,但這個波浪元件絕對不常見 這篇文章的啟發,我為 Compose 寫了一個波浪效果的進度載入庫,API 的設計上符合 Compose 的開發規範,使用非常簡便。
1. 使用方式
在 root 的 build.gradle
中引入 jitpack
,
groovy
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
在 module 的 build.gradle
中引入 ComposeWaveLoading
的最新版本
groovy
dependencies {
implementation 'com.github.vitaviva:ComposeWaveLoading:$latest_version'
}
2. API 設計思想
```kotlin
Box { WaveLoading ( progress = 0.5f // 0f ~ 1f ) { Image( painter = painterResource(id = R.drawable.logo_tiktok), contentDescription = "" ) } }
``
傳統的 UI 開發方式中,設計這樣一個波浪控制元件,一般會使用自定義 View 並將 Image 等作為屬性傳入。 而在 Compose 中,我們讓
WaveLoading和
Image以組合的方式使用,這樣的 API 更加靈活,
WaveLoding的內部可以是
Image,也可以是
Text亦或是其他
Composable`。波浪動畫不拘泥於某一特定 Composable, 任何 Composable 都可以以波浪動畫的形式展現, 通過 Composable 的組合使用,擴大了 “能力” 的覆蓋範圍。
3. API 引數介紹
kotlin
@Composable
fun WaveLoading(
modifier: Modifier = Modifier,
foreDrawType: DrawType = DrawType.DrawImage,
backDrawType: DrawType = rememberDrawColor(color = Color.LightGray),
@FloatRange(from = 0.0, to = 1.0) progress: Float = 0f,
@FloatRange(from = 0.0, to = 1.0) amplitude: Float = defaultAmlitude,
@FloatRange(from = 0.0, to = 1.0) velocity: Float = defaultVelocity,
content: @Composable BoxScope.() -> Unit
) { ... }
引數說明如下:
|引數| 說明|
|--|--|
|progress| 載入進度 |
|foreDrawType| 波浪圖的繪製型別: DrawColor
或者 DrawImage
|
|backDrawType| 波浪圖的背景繪製|
|amplitude| 波浪的振幅, 0f ~ 1f 表示振幅在整個繪製區域的佔比|
|velocity|波浪移動的速度|
|content| 子Composalble|
接下來重點介紹一下 DrawType
。
DrawType
波浪的進度體現在前景(foreDrawType)和後景(backDrawType)的視覺差,我們可以為前景後景分別指定不同的 DrawType 改變波浪的樣式。
kotlin
sealed interface DrawType {
object None : DrawType
object DrawImage : DrawType
data class DrawColor(val color: Color) : DrawType
}
如上,DrawType 有三種類型:
- None: 不進行繪製
- DrawColor:使用單一顏色繪製
- DrawImage:按照原樣繪製
以下面這個 Image
為例, 體會一下不同 DrawType 的組合效果
|index|backDrawType|foreDrawType | 說明 | |--|--|--| --| |1|DrawImage| DrawImage| 背景灰度,前景原圖| |2|DrawColor(Color.LightGray)|DrawImage|背景單色,前景原圖 | |3|DrawColor(Color.LightGray)|DrawColor(Color.Cyan)| 背景單色,前景單色| |4|None|DrawColor(Color.Cyan)| 無背景,前景單色|
如下圖中,第二排是前景原圖,第三排是前景單色
下圖展示無背景色的情況
注意 backDrawType 設定為 DrawImage 時,會顯示為灰度圖。
4. 原理淺析
簡單介紹一下實現原理。為了便於理解,程式碼經過簡化處理,完整程式碼可以在 github 檢視
這個庫的關鍵是可以將 WaveLoading {...}
內容取出,加以波浪動畫的形式顯示。所以需要將子 Composalbe 轉成 Bitmap 進行後續處理。
4.1 獲取 Bitmap
我在 Compose 中沒找到獲取點陣圖的辦法,所以用了一個 trick 的方式, 通過 Compose 與 Android 原生檢視良好的互操作性,先將子 Composalbe 顯示在 AndroidView
中,然後通過 native 的方式獲取 Bitmap:
```kotlin @Composable fun WaveLoading (...) { Box {
var _bitmap by remember {
mutableStateOf(Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565))
}
AndroidView(
factory = { context ->
// Creates custom view
object : AbstractComposeView(context) {
@Composable
override fun Content() {
Box(Modifier.wrapContentSize(){
content()
}
}
override fun dispatchDraw(canvas: Canvas?) {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas2 = Canvas(source)
super.dispatchDraw(canvas2)
_bitmap = bmp
}
}
}
)
WaveLoadingInternal(bitmap = _bitmap)
}
} ```
AndroidView
是一個可以繪製 Composable 的原生控制元件,我們將 WaveLoading 的子 Composable 放在其 Content
中,然後在 dispatchDraw
中繪製時,將內容繪製到我們準備好的 Bitmap 中。
4.2 繪製波浪線
我們基於 Compose 的 Canvas 繪製波浪線,波浪線通過 Path
承載
定義 WaveAnim
用來進行波浪線的繪製
```kotlin internal data class WaveAnim( val duration: Int, val offsetX: Float, val offsetY: Float, val scaleX: Float, val scaleY: Float, ) {
private val _path = Path()
//繪製波浪線
internal fun buildWavePath(
dp: Float,
width: Float,
height: Float,
amplitude: Float,
progress: Float
): Path {
var wave = (scaleY * amplitude).roundToInt() //計算拉伸之後的波幅
_path.reset()
_path.moveTo(0f, height)
_path.lineTo(0f, height * (1 - progress))
// 通過正弦曲線繪製波浪
if (wave > 0) {
var x = dp
while (x < width) {
_path.lineTo(
x,
height * (1 - progress) - wave / 2f * Math.sin(4.0 * Math.PI * x / width)
.toFloat()
)
x += dp
}
}
_path.lineTo(width, height * (1 - progress))
_path.lineTo(width, height)
_path.close()
return _path
}
} ```
如上,波浪線 Path 通過正弦函式繪製。
4.3 波浪填充
有了 Path ,我們還需要填充內容。填充的內容前文已經介紹過,或者是 DrawColor
或者 DrawImage
。 繪製 Path 需要定義 Paint
kotlin
val forePaint = remember(foreDrawType, bitmap) {
Paint().apply {
shader = BitmapShader(
when (foreDrawType) {
is DrawType.DrawColor -> bitmap.toColor(foreDrawType.color)
is DrawType.DrawImage -> bitmap
else -> alphaBitmap
},
Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP
)
}
}
Paint 使用 Shader 著色器繪製 Bitmap, 當 DrawType 只繪製單色時, 對點陣圖做單值處理: ```kotlin /* * 點陣圖單色化 / fun Bitmap.toColor(color: androidx.compose.ui.graphics.Color): Bitmap { val bmp = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ) val oldPx = IntArray(width * height) //用來儲存原圖每個畫素點的顏色資訊 getPixels(oldPx, 0, width, 0, 0, width, height) //獲取原圖中的畫素資訊
val newPx = oldPx.map {
color.copy(Color.alpha(it) / 255f).toArgb()
}.toTypedArray().toIntArray()
bmp.setPixels(newPx, 0, width, 0, 0, width, height) //將處理後的畫素資訊賦給新圖
return bmp
} ```
4.4 波浪動畫
最後通過 Compose 動畫讓波浪動起來
```kotlin val transition = rememberInfiniteTransition()
val waves = remember(Unit) {
listOf(
WaveAnim(waveDuration, 0f, 0f, scaleX, scaleY),
WaveAnim((waveDuration * 0.75f).roundToInt(), 0f, 0f, scaleX, scaleY),
WaveAnim((waveDuration * 0.5f).roundToInt(), 0f, 0f, scaleX, scaleY)
)
}
val animates : List<State<Float>> = waves.map { transition.animateOf(duration = it.duration) }
``
為了讓波浪更有層次感,我們定義三個
WaveAnim` 以 Set 的形式做動畫
最後,配合 WaveAnim 將波浪的 Path 繪製到 Canvas 即可
```kotlin Canvas{
drawIntoCanvas { canvas ->
//繪製後景
canvas.drawRect(0f, 0f, size.width, size.height, backPaint)
//繪製前景
waves.forEachIndexed { index, wave ->
canvas.withSave {
val maxWidth = 2 * scaleX * size.width / velocity.coerceAtLeast(0.1f)
val maxHeight = scaleY * size.height
canvas.drawPath (
wave.buildWavePath(
width = maxWidth,
height = maxHeight,
amplitude = size.height * amplitude,
progress = progress
), forePaint
)
}
}
}
}
```
原始碼:https://github.com/vitaviva/ComposeWaveLoading
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!
- 面試必備:Kotlin 執行緒同步的 N 種方法
- Jetpack MVVM 七宗罪之六:ViewModel 介面暴露不合理
- CreationExtras 來了,建立 ViewModel 的新方式
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼
- 為什麼 RxJava 有 Single / Maybe 等單發資料型別,而 Flow 沒有?
- 使用整潔架構優化你的 Gradle Module
- 一道面試題:介紹一下 Fragment 間的通訊方式?
- 【程式碼吸貓】使用 Google MLKit 進行影象識別
- Kotlin 1.6 正式釋出,帶來哪些新特性?
- Android Dev Summit '21 精彩內容盤點
- @OnLifecycleEnvent 被廢棄,替代方案更簡單
- Jetpack Navigation 實現自定義 View 導航
- 實現一個 Coroutine 版 DialogFragment