Dialog 按照順序彈窗的優雅寫法

我為 Compose 寫瞭一個波浪效果的進度加載庫,API 的設計上符合 Compose 的開發規范,使用非常簡便。

1. 使用方式

在 root 的 build.gradle 中引入 jitpack

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}

在 module 的 build.gradle 中引入 ComposeWaveLoading 的最新版本

dependencies {
    implementation 'com.github.vitaviva:ComposeWaveLoading:$latest_version'
}

2. API 設計思想

Box {
    WaveLoading (
        progress = 0.5f // 0f ~ 1f
    ) {
        Image(
          painter = painterResource(id = R.drawable.logo_tiktok),
          contentDescription = ""
        )
    }
}

傳統的 UI 開發方式中,設計這樣一個波浪控件,一般會使用自定義 View 並將 Image 等作為屬性傳入。 而在 Compose 中,我們讓 WaveLoadingImage 以組合的方式使用,這樣的 API 更加靈活,WaveLoding 的內部可以是 Image,也可以是 Text 亦或是其他 Composable。波浪動畫不拘泥於某一特定 Composable, 任何 Composable 都可以以波浪動畫的形式展現, 通過 Composable 的組合使用,擴大瞭 “能力” 的覆蓋范圍。

3. API 參數介紹

@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 改變波浪的樣式。

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:

@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 用來進行波浪線的繪制

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

 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 隻繪制單色時, 對位圖做單值處理:

/**
 * 位圖單色化
 */
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 動畫讓波浪動起來

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 即可

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
                    )
                }
 
            }
        }
    }

需要源碼可以私信我,當天回復

到此這篇關於Dialog 按照順序彈窗的文章就介紹到這瞭,更多相關Dialog 按照順序彈窗內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: