如何用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
相关产品推荐
相关产品推荐

