Jetpack Compose 实现波浪加载效果

简介: Jetpack 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
                    )
                }
            }
        }
    }
目录
相关文章
|
3月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
86 4
|
5月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
6月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
6月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
6月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
6月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
6月前
深入了解 Jetpack Compose 中的 Modifier
深入了解 Jetpack Compose 中的 Modifier
113 0
|
6月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
|
6月前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
|
6月前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用