当前位置:首页 > 技术 > 正文内容

使用 Jetpack Compose 实现可复用的半圆形仪表盘组件【适用于体重计、温度计等场景】

访客 技术 2026年7月26日 4

在 Android 开发中,自定义 UI 组件常常是提升用户体验的关键。本文将介绍如何利用 Jetpack Compose 构建一个美观且高度可定制的半圆形仪表盘(Gauge),适用于体重监测、体温显示、健康数据反馈等多种应用场景。

效果预览如下:
仪表盘效果图

开发环境配置

确保项目已启用 Compose 支持,并正确设置 Kotlin 与 Java 兼容版本:

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }

    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"
    }
}
    

核心实现:Compose 自定义绘制

通过 Canvas 组件进行矢量绘图,结合状态驱动动画,实现动态更新的仪表指针和渐变弧形进度条。

@Composable
fun CircularGauge(
    value: Float,
    maxValue: Float = 100f,
    modifier: Modifier = Modifier
) {
    var centerX by remember { mutableFloatStateOf(0f) }
    var centerY by remember { mutableFloatStateOf(0f) }
    var gaugeRadius by remember { mutableFloatStateOf(0f) }

    // 计算当前占比对应的角度(从左到右扫过180度)
    val sweepAngle by animateFloatAsState(
        targetValue = (value / maxValue) * 180f,
        label = "gaugeAngle"
    )

    Box(modifier = modifier) {
        Canvas(modifier = Modifier.matchParentSize()) {
            // 初始化布局参数
            centerX = size.width / 2f
            centerY = size.height
            gaugeRadius = min(size.width, size.height * 2) / 2f * 0.9f

            // 背景弧线(灰色)
            drawArc(
                color = Color(0xFFE1E6EA),
                startAngle = 180f,
                sweepAngle = 180f,
                useCenter = false,
                topLeft = Offset(centerX - gaugeRadius, centerY - gaugeRadius),
                size = Size(gaugeRadius * 2, gaugeRadius * 2),
                style = Stroke(30f, cap = StrokeCap.Round)
            )

            // 渐变色进度弧
            val gradientBrush = Brush.linearGradient(
                colors = listOf(
                    Color(0xFF7832FD), // 紫
                    Color(0xFFF293FA), // 粉
                ),
                start = Offset(-gaugeRadius, 0f),
                end = Offset(gaugeRadius, 0f)
            )
            drawArc(
                brush = gradientBrush,
                startAngle = 180f,
                sweepAngle = sweepAngle,
                useCenter = false,
                topLeft = Offset(centerX - gaugeRadius, centerY - gaugeRadius),
                size = Size(gaugeRadius * 2, gaugeRadius * 2),
                style = Stroke(30f, cap = StrokeCap.Round)
            )

            // 刻度线绘制:每单位一个刻度,共maxValue+1个
            for (tick in 0..maxValue.toInt()) {
                val angle = 180f - (tick.toFloat() / maxValue) * 180f
                val radians = Math.toRadians(angle.toDouble())

                // 区分主刻度、次刻度长度
                val length = if (tick % 10 == 0) 20f else if (tick % 5 == 0) 15f else 8f
                val strokeWidth = if (tick % 10 == 0) 2.5f else if (tick % 5 == 0) 1.8f else 1f

                val innerR = gaugeRadius - 40f
                val outerR = innerR - length

                val x1 = centerX + innerR * cos(radians).toFloat()
                val y1 = centerY - innerR * sin(radians).toFloat()
                val x2 = centerX + outerR * cos(radians).toFloat()
                val y2 = centerY - outerR * sin(radians).toFloat()

                drawLine(
                    color = Color(0xFFFBCEFF),
                    start = Offset(x1, y1),
                    end = Offset(x2, y2),
                    strokeWidth = strokeWidth
                )

                // 主刻度旁添加数字标签
                if (tick % 10 == 0) {
                    val textRadius = gaugeRadius - 70f
                    val textX = centerX + textRadius * cos(radians).toFloat()
                    val textY = centerY - textRadius * sin(radians).toFloat()

                    rotate(-angle + 180, pivot = Offset(textX, textY)) {
                        drawContext.canvas.nativeCanvas.drawText(
                            "${tick * 2}",
                            textX,
                            textY,
                            android.graphics.Paint().apply {
                                textAlign = android.graphics.Paint.Align.CENTER
                                textSize = 12.sp.toPx()
                                color = android.graphics.Color.parseColor("#FFFBCEFF")
                                isAntiAlias = true
                            }
                        )
                    }
                }
            }

            // 指针末端的小圆点
            val pointerEndAngle = Math.toRadians((180 - sweepAngle).toDouble())
            val endX = centerX + gaugeRadius * cos(pointerEndAngle).toFloat()
            val endY = centerY - gaugeRadius * sin(pointerEndAngle).toFloat()

            drawCircle(
                color = Color(0xFFFBCEFF),
                radius = 15f,
                center = Offset(endX, endY)
            )

            // 菱形指针主体
            val pointerLength = gaugeRadius * 0.8f
            val headX = centerX + pointerLength * cos(pointerEndAngle).toFloat()
            val headY = centerY - pointerLength * sin(pointerEndAngle).toFloat()

            val normalAngle = pointerEndAngle + Math.PI / 2
            val halfWidth = 17.5f
            val perpCos = cos(normalAngle).toFloat()
            val perpSin = sin(normalAngle).toFloat()

            val midRatio = 0.4f
            val midX = centerX + pointerLength * midRatio * cos(pointerEndAngle).toFloat()
            val midY = centerY - pointerLength * midRatio * sin(pointerEndAngle).toFloat()

            val leftWingX = midX + halfWidth * perpCos
            val leftWingY = midY - halfWidth * perpSin
            val rightWingX = midX - halfWidth * perpCos
            val rightWingY = midY + halfWidth * perpSin

            val path = Path().apply {
                moveTo(headX, headY)
                lineTo(leftWingX, leftWingY)
                lineTo(centerX, centerY)
                lineTo(rightWingX, rightWingY)
                close()
            }

            // 填充指针
            drawPath(path, color = Color(0xFFFEE7AC))
            // 描边
            drawPath(path, color = Color(0xFFFEE7AC), style = Stroke(2f))

            // 中心底座圆环
            drawCircle(
                color = Color(0xFFFBCEFF),
                radius = 40f,
                center = Offset(centerX, centerY)
            )
        }
    }
}
    

预览与使用示例

借助 @Preview 注解快速查看不同数据状态下的视觉表现:

@Preview(showBackground = true)
@Composable
private fun GaugePreview() {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        CircularGauge(value = 0f, modifier = Modifier.height(200.dp))
        Spacer(Modifier.height(16.dp))
        CircularGauge(value = 50f, modifier = Modifier.height(200.dp))
        Spacer(Modifier.height(16.dp))
        CircularGauge(value = 100f, modifier = Modifier.height(200.dp))
    }
}
    

若采用 MVVM 架构,可通过 LiveData 或 StateFlow 接入 ViewModel 数据流:

// 在 Composable 中观察 ViewModel 数据
val currentValue by viewModel.currentReading.observeAsState(0f)
CircularGauge(value = currentValue)
    

该组件支持灵活定制:颜色、尺寸、刻度密度、数值范围、字体样式等均可外部传参控制,便于集成进各类健康管理类 App。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。