You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Modifier镜像Jetpack Compose中Canvas绘制的七段显示组件?

问题描述

我在Android平台用Jetpack Compose和Canvas开发七段式显示组件,目标效果如下:

为简化实现,我只绘制组件上半部分,再通过镜像得到完整组件。上半部分效果如下:

但镜像时遇到问题:添加Modifier.rotate(180f)后,图形会绕Canvas原点顺时针旋转,导致无法在屏幕上显示(逆时针旋转能正常显示)。

我不想用字体方案实现,希望用Canvas和Compose本身解决,同时也想了解不用镜像的更优Canvas绘制思路。

我的代码

DisplayComponent.kt

@Composable
fun DisplayComponent(
    modifier: Modifier = Modifier,
    size: Int = 1000,
    color: Color = MaterialTheme.colors.primary,
) {
    Column(modifier = modifier) {
        HalfDisplayComponent(size, color)
        HalfDisplayComponent(
            modifier = Modifier.rotate(180f),
            size = size,
            color = color
        )
    }
}

@Composable
private fun HalfDisplayComponent(
    size: Int,
    color: Color,
    modifier: Modifier = Modifier,
) {
    Box(modifier = modifier) {
        LedModel.values().forEach {
            LedComponent(
                ledModel = it,
                size = size,
                color = color
            )
        }
    }
}

LedModel.kt

enum class LedModel(val coordinates: List<Pair<Float, Float>>) {
    HorizontalTop(
        listOf(
            Pair(0.04f, 0.03f), // Point A
            Pair(0.07f, 0f), // Point B
            Pair(0.37f, 0f), // Point C
            Pair(0.4f, 0.03f), // Point D
            Pair(0.34f, 0.08f), // Point E
            Pair(0.1f, 0.08f), // Point F
        )
    ),
    VerticalRight(
        listOf(
            Pair(0.41f, 0.04f), // Point A
            Pair(0.44f, 0.07f), // Point B
            Pair(0.44f, 0.37f), // Point C
            Pair(0.41f, 0.4f), // Point D
            Pair(0.35f, 0.35f), // Point E
            Pair(0.35f, 0.09f), // Point F
        )
    ),
    VerticalLeft(
        listOf(
            Pair(0.03f, 0.4f), // Point A
            Pair(0f, 0.37f), // Point B
            Pair(0f, 0.07f), // Point C
            Pair(0.03f, 0.04f), // Point D
            Pair(0.09f, 0.09f), // Point E
            Pair(0.09f, 0.35f), // Point F
        )
    ),
    HorizontalBottom(
        listOf(
            Pair(0.1f, 0.36f), // Point A
            Pair(0.34f, 0.36f), // Point B
            Pair(0.39f, 0.4f), // Point C
            Pair(0.05f, 0.4f), // Point D
        )
    ),
}

LedComponent.kt

@Composable
fun LedComponent(
    modifier: Modifier = Modifier,
    size: Int = 30,
    color: Color = MaterialTheme.colors.primary,
    ledModel: LedModel = LedModel.HorizontalTop
) = getPath(ledModel.coordinates).let { path ->
    Canvas(modifier = modifier.scale(size.toFloat())) {
        drawPath(path, color)
    }
}

private fun getPath(coordinates: List<Pair<Float, Float>>): Path = Path().apply {
    coordinates.map {
        transformPointCoordinate(it)
    }.forEachIndexed { index, point ->
        if (index == 0) moveTo(point.x, point.y) else lineTo(point.x, point.y)
    }
}

private fun transformPointCoordinate(point: Pair<Float, Float>) =
    Offset(point.first.dp.value, point.second.dp.value)
失败尝试

我尝试用Modifier旋转组件,但无效,代码如下:

@Composable
fun DisplayComponent(
    modifier: Modifier = Modifier,
    size: Int = 1000,
    color: Color = MaterialTheme.colors.primary,
) {
    Column(modifier = modifier) {
        DisplayFABGComponent(size, color)
        DisplayFABGComponent(
            modifier = Modifier.rotate(180f),
            size = size,
            color = color
        )
    }
}
解决方案

一、解决镜像旋转显示问题

问题出在Modifier.rotate(180f)默认绕组件左上角(Canvas原点)旋转,旋转后整个组件会偏移到可视区域外。可以通过以下两种方式修复:

方式1:指定旋转原点

使用graphicsLayer设置旋转原点为组件中心,避免旋转后偏移:

@Composable
fun DisplayComponent(
    modifier: Modifier = Modifier,
    size: Int = 1000,
    color: Color = MaterialTheme.colors.primary,
) {
    Column(modifier = modifier) {
        HalfDisplayComponent(size, color)
        HalfDisplayComponent(
            modifier = Modifier.graphicsLayer {
                rotationZ = 180f
                // 旋转原点设为上半部分组件的中心
                originX = (0.4f * size).toFloat()
                originY = (0.4f * size).toFloat()
            },
            size = size,
            color = color
        )
    }
}

方式2:Canvas内部变换

不在Modifier层旋转,而是在Canvas绘制时对路径做平移+旋转操作,确保图形在可视区域内:

@Composable
private fun HalfDisplayComponent(
    size: Int,
    color: Color,
    modifier: Modifier = Modifier,
    isMirrored: Boolean = false
) {
    Box(modifier = modifier) {
        LedModel.values().forEach {
            LedComponent(
                ledModel = it,
                size = size,
                color = color,
                isMirrored = isMirrored
            )
        }
    }
}

@Composable
fun LedComponent(
    modifier: Modifier = Modifier,
    size: Int = 30,
    color: Color = MaterialTheme.colors.primary,
    ledModel: LedModel = LedModel.HorizontalTop,
    isMirrored: Boolean = false
) = getPath(ledModel.coordinates).let { path ->
    Canvas(modifier = modifier.scale(size.toFloat())) {
        if (isMirrored) {
            withTransform({
                // 先平移到上半部分下方,再绕顶部中心旋转180度
                translate(top = (0.4f * size).toFloat())
                rotate(180f, pivot = Offset((0.4f * size)/2, 0f))
            }) {
                drawPath(path, color)
            }
        } else {
            drawPath(path, color)
        }
    }
}

// 调用示例
@Composable
fun DisplayComponent(
    modifier: Modifier = Modifier,
    size: Int = 1000,
    color: Color = MaterialTheme.colors.primary,
) {
    Column(modifier = modifier) {
        HalfDisplayComponent(size, color)
        HalfDisplayComponent(size, color, isMirrored = true)
    }
}

二、无需镜像的更优绘制思路

直接定义完整七段的相对坐标,基于组件尺寸动态计算每个段的位置,避免镜像操作。核心是用比例坐标适配任意尺寸,同时直接控制每个段的显示状态:

enum class FullLedSegment(val coordinates: List<Pair<Float, Float>>) {
    // 顶部段A
    A(listOf(
        Pair(0.1f, 0.02f), Pair(0.15f, 0f), Pair(0.85f, 0f), Pair(0.9f, 0.02f),
        Pair(0.85f, 0.05f), Pair(0.15f, 0.05f)
    )),
    // 右上段B
    B(listOf(
        Pair(0.92f, 0.1f), Pair(0.95f, 0.15f), Pair(0.95f, 0.45f), Pair(0.92f, 0.5f),
        Pair(0.87f, 0.45f), Pair(0.87f, 0.15f)
    )),
    // 右下段C
    C(listOf(
        Pair(0.92f, 0.52f), Pair(0.95f, 0.55f), Pair(0.95f, 0.85f), Pair(0.92f, 0.9f),
        Pair(0.87f, 0.85f), Pair(0.87f, 0.55f)
    )),
    // 底部段D
    D(listOf(
        Pair(0.1f, 0.88f), Pair(0.15f, 0.9f), Pair(0.85f, 0.9f), Pair(0.9f, 0.88f),
        Pair(0.85f, 0.85f), Pair(0.15f, 0.85f)
    )),
    // 左下段E
    E(listOf(
        Pair(0.08f, 0.52f), Pair(0.05f, 0.55f), Pair(0.05f, 0.85f), Pair(0.08f, 0.9f),
        Pair(0.13f, 0.85f), Pair(0.13f, 0.55f)
    )),
    // 左上段F
    F(listOf(
        Pair(0.08f, 0.1f), Pair(0.05f, 0.15f), Pair(0.05f, 0.45f), Pair(0.08f, 0.5f),
        Pair(0.13f, 0.45f), Pair(0.13f, 0.15f)
    )),
    // 中间段G
    G(listOf(
        Pair(0.1f, 0.48f), Pair(0.15f, 0.5f), Pair(0.85f, 0.5f), Pair(0.9f, 0.48f),
        Pair(0.85f, 0.45f), Pair(0.15f, 0.45f)
    ))
}

@Composable
fun FullSevenSegmentDisplay(
    modifier: Modifier = Modifier,
    size: Int = 1000,
    color: Color = MaterialTheme.colors.primary,
    // 控制各段是否点亮,默认全亮(显示数字8)
    activeSegments: Map<FullLedSegment, Boolean> = FullLedSegment.values().associateWith { true }
) {
    Canvas(modifier = modifier.size(size.dp)) {
        val canvasSize = size.toFloat()
        FullLedSegment.values().forEach { segment ->
            if (activeSegments[segment] == true) {
                val path = Path().apply {
                    segment.coordinates.map { (x, y) ->
                        Offset(x * canvasSize, y * canvasSize)
                    }.forEachIndexed { index, offset ->
                        if (index == 0) moveTo(offset.x, offset.y) else lineTo(offset.x, offset.y)
                    }
                    close()
                }
                drawPath(path, color)
            }
        }
    }
}

// 使用示例:显示数字8
@Composable
fun DemoDisplay() {
    FullSevenSegmentDisplay(
        size = 200,
        color = Color.Red
    )
}

这种方式的优势:

  • 直接控制每个段的显示,灵活支持不同数字/字符
  • 基于相对比例,适配任意尺寸
  • 无镜像操作,避免旋转偏移问题

内容的提问来源于stack exchange,提问作者Pierre Vieira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 18:40:38