PAYATHON 2026

Android 如何绘制贴合物理圆角屏幕边缘的 1 px 路径?

技术老齐

结论

Android 没有公开 API 可以把应用坐标精确映射到每台设备的物理玻璃轮廓,因此无法保证在所有设备上绘制出零间隙、完全贴合硬件边缘的 1 px 路径。

WindowInsets.getRoundedCorner() 返回系统提供的圆角近似信息,包括圆心和半径,可用于避让圆角或绘制近似轮廓。这些信息不一定能准确描述面板遮罩、玻璃切割形状或每个像素最终是否可见。设备边缘也未必是标准圆弧,还可能采用椭圆、连续曲率曲线或厂商自定义遮罩。

较为通用的处理方式如下:

  1. 同时使用 RoundedCorner.center 和 RoundedCorner.radius,不要只读取 radius。
  2. 将路径中心线向屏幕内部移动至少半个描边宽度。
  3. 内缩圆弧时保持圆心不变,并相应减小半径。
  4. 将绘制结果视为系统支持的近似轮廓。若要在某台设备上实现视觉上的精确贴合,需要针对设备或机型进行校准。

为什么圆角弧线会被裁剪

Stroke(width = 1f) 以路径为中心绘制。路径落在边界上时,理论上会有 0.5 px 位于边界外:

strokeWidth = 1 px
outside     = 0.5 px
inside      = 0.5 px

直线即使被裁掉一半,通常仍有像素可见。圆角区域还会受到物理面板遮罩、系统圆角遮罩和抗锯齿采样的影响。靠外侧的采样点可能全部被丢弃,整段弧线也就看不见了。

软件画布的矩形范围也不等同于屏幕实际可发光的区域。应用可以在逻辑坐标中提交某个像素,显示系统仍可能在合成阶段根据面板形状将其裁掉。黑边、非发光区域以及物理玻璃以外的区域都无法由应用覆盖。

正确的内缩几何关系

假设系统报告的外轮廓是半径为 (R) 的圆弧,描边宽度为 (w),额外预留的抗锯齿安全距离为 (\varepsilon)。

路径中心线向内移动的距离为:

[ d = \frac{w}{2} + \varepsilon ]

内缩后的圆弧半径为:

[ R_{\text{inner}} = R - d ]

内缩时圆心不变。

对于标准圆角矩形,需要同时调整边界矩形和圆角半径:

bounding rect 向内缩 d
corner radius 从 R 改为 R - d

如果只缩小矩形,圆角半径仍使用原来的 R,得到的路径就不是外轮廓的等距曲线,圆弧与直线之间的连接关系也会发生变化。

对称圆角屏幕的 Compose 示例

以下写法适用于四个圆角可以近似采用同一半径,且画布确实覆盖整个窗口的情况:

@RequiresApi(Build.VERSION_CODES.S)
@Composable
fun EdgeBorder() {
    val view = LocalView.current

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        val insets = view.rootWindowInsets ?: return@Canvas
        val topLeftCorner = insets.getRoundedCorner(
            RoundedCorner.POSITION_TOP_LEFT
        ) ?: return@Canvas

        val strokeWidth = 1f
        val antialiasSafety = 0.5f
        val inset = strokeWidth / 2f + antialiasSafety

        val outerRadius = topLeftCorner.radius.toFloat()
        val innerRadius = (outerRadius - inset).coerceAtLeast(0f)

        drawRoundRect(
            color = Color.White,
            topLeft = Offset(inset, inset),
            size = Size(
                width = size.width - inset * 2f,
                height = size.height - inset * 2f
            ),
            cornerRadius = CornerRadius(innerRadius),
            style = Stroke(width = strokeWidth)
        )
    }
}

Stroke(width = 1f) 在 Compose 绘制坐标中表示 1 个物理像素,并非 1.dp。如果希望描边的视觉宽度不受屏幕密度影响,可以使用 1.dp.toPx(),但此时描边宽度不再固定为一个物理像素。

antialiasSafety 没有统一的标准值。设为 0f 时,中心线恰好内缩半个描边宽度。增加约半个像素可以降低边缘采样被遮罩裁掉的概率,代价是路径与屏幕边缘的距离会稍大。

使用圆心绘制单个圆角

圆心不一定是 (radius, radius)。窗口偏移、旋转、分屏和厂商配置都可能使这一假设失效。绘制左上角时,可以直接读取 RoundedCorner.center:

@RequiresApi(Build.VERSION_CODES.S)
fun DrawScope.drawTopLeftCorner(
    corner: RoundedCorner,
    color: Color,
    strokeWidth: Float = 1f,
    antialiasSafety: Float = 0.5f
) {
    val inset = strokeWidth / 2f + antialiasSafety
    val radius = (corner.radius.toFloat() - inset).coerceAtLeast(0f)

    if (radius <= 0f) return

    val centerX = corner.center.x.toFloat()
    val centerY = corner.center.y.toFloat()

    drawArc(
        color = color,
        startAngle = 180f,
        sweepAngle = 90f,
        useCenter = false,
        topLeft = Offset(
            x = centerX - radius,
            y = centerY - radius
        ),
        size = Size(
            width = radius * 2f,
            height = radius * 2f
        ),
        style = Stroke(width = strokeWidth)
    )
}

绘制完整边框时,应分别读取四个圆角:

RoundedCorner.POSITION_TOP_LEFT
RoundedCorner.POSITION_TOP_RIGHT
RoundedCorner.POSITION_BOTTOM_RIGHT
RoundedCorner.POSITION_BOTTOM_LEFT

根据每个圆角各自的 center 和 radius 构造 Path,再用直线连接圆弧的切点。四个角不能默认完全对称。

窗口和坐标系必须一致

只有当路径与圆角数据位于同一窗口坐标系时,这些数据才有意义。实现时需要确认:

  • Canvas 覆盖当前窗口,而不是经过额外偏移或缩放的子布局。
  • 窗口已按需求启用 edge-to-edge。
  • 系统栏没有缩小实际绘制区域。
  • 使用当前窗口收到的 WindowInsets,不要把其他显示区域的 metrics 直接用于局部 Canvas。
  • 屏幕旋转、进入分屏、连接外部显示器或窗口尺寸变化后,重新读取四个圆角。
  • 在不支持 RoundedCorner 的系统版本上,使用普通矩形或安全内缩方案。

如果应用的 Surface 没有延伸到显示边缘,无论如何计算路径,内容都无法越过该 Surface 的边界。

为什么实测半径可能与 API 不一致

实测结果与 API 数据存在差异,通常有以下原因:

  • RoundedCorner 用圆弧近似真实边缘,而硬件轮廓未必是圆。
  • 厂商上报的参数可能来自资源配置,与面板的逐像素遮罩并不完全一致。
  • 物理玻璃边缘、有效发光区域和系统合成遮罩可能对应不同的边界。
  • 抗锯齿会对路径两侧进行采样,外侧样本可能被最终遮罩丢弃。
  • 模拟器显示的是软件模拟圆角,不能代表真实玻璃或面板的形状。

经验值 R = 68f 和 magentaD = 8f 只能作为特定设备的视觉校准结果。它们不能用于通用算法,也无法根据另一台设备的 RoundedCorner.radius 可靠推导。

实际应如何选择

如果需要避免内容被圆角遮挡,应使用系统 insets 并预留安全距离,不必尝试绘制到最后一个可见像素。

绘制装饰边框时,可以结合 center、radius 和 (R-d) 的内缩公式,获得跨设备表现较为稳定的近似轮廓。

如果产品要求边框与指定硬件的真实轮廓逐像素吻合,就需要按机型维护校准数据,或由设备厂商提供准确的轮廓信息。普通 Android 应用无法通过公开 API 自动还原任意设备的物理玻璃曲线。

备注:内容仅供参考。