ITmob-Ly
发布于 2023-08-13 / 132 阅读
0

Jetpack Compose 中 DrawStyle 详解 - (线段/笔画/轮廓的绘制样式)

1. 简介

Jetpack Compose 中在 drawLine drawRect drawText drawRect drawImage, drawOval 等绘制方法中可以通过 drawStyle 属性控制线段/轮廓的绘制效果(drawLine 是线段本身,不需要设置 drawStyle 属性,直接设置其相关属性即可)。

compose-ui 库的 1.4.0-alpha01 版本开始为 TextStyle, SpanStyle, Paragraph, MultiParagraph, 等增加了 DrawStyle 属性用于绘制文字笔画/轮廓效果。

Text(
    text = "https://itmob.cn",
    style = LocalTextStyle.current.merge(
        TextStyle(
            color = Color.Red,
            fontSize = 32.sp,
            drawStyle = Fill,
        ),
    ),
)

Draw text in JetpackCompose

如上代码使用 Fill 作为 drawStyle 属性。Fill 是 drawStyle 属性的默认值,表示应使用所提供的颜色或图案完全填充形状。

2. DrawStyle 属性详解

Compose 中定义了两个 DrawStyle 的子类:FillStroke

  1. FilldrawStyle 属性的默认值,表示应使用所提供的颜色或图案完全填充形状。
  2. Stroke 是为带有笔画的绘制提供信息。

2.1 Fill

/**
 * 默认DrawStyle, 表示应使用所提供的颜色或图案完全填充形状
 */
object Fill : DrawStyle

Fill 是默认属性,文字轮廓是填充状态,是文字的默认样式,可以不用设置(即简介中示例的样式)。

2.2 Stroke

如下是 Stroke 的定义:

/**
 * 提供用笔划绘制内容的信息
 */
class Stroke(
    val width: Float = 0.0f,
    val miter: Float = DefaultMiter,
    val cap: StrokeCap = DefaultCap,
    val join: StrokeJoin = DefaultJoin,
    val pathEffect: PathEffect? = null
) : DrawStyle() {
    companion object {

        /**
         * Width to indicate a hairline stroke of 1 pixel
         */
        const val HairlineWidth = 0.0f

        /**
         * Default miter length used in combination with joins
         */
        const val DefaultMiter: Float = 4.0f

        /**
         * Default cap used for line endings
         */
        val DefaultCap = StrokeCap.Butt

        /**
         * Default join style used for connections between line and curve segments
         */
        val DefaultJoin = StrokeJoin.Miter
    }
...
}

Stroke 提供了如下属性:

  • width - 轮廓/描边的宽度(单位:像素)
  • miter - 轮廓/描边斜接值。这用于在拐角是锐角连接时控制锐角连接的行为。此值必须>= 0
  • cap - 返回绘制的端点,控制如何处理描边线和路径的起始和结束。默认值为StrokeCap.Butt。
  • join - 设置拐角形状,设置在描边路径上加入直线和曲线段的处理。默认值为:尖角 StrokeJoin.Miter
  • pathEffect - 应用于笔画/描边/轮廓的效果,空表示将绘制实线描边线。

2.2.1 miter 斜接值

miter 的英文意思是:斜接/斜面接头/尖角。在这里表示笔画使用斜接/尖角连接。
miter 是对 Stroke 类中 join 属性的一个补充,即拐角形状是斜接(StrokeJoin.**Miter**)时,它用于设置 miter 型拐角的延长线的最大值。

示例:

Text(
    text = "笔画",
    style = LocalTextStyle.current.merge(
        TextStyle(
            color = Color.Red,
            fontSize = 128.sp,
            drawStyle = Stroke(
                width = strokeWith,
                miter = 0F,
                join = StrokeJoin.Miter,
            ),
        ),
    ),
)
Text(
    text = "笔画",
    style = LocalTextStyle.current.merge(
        TextStyle(
            color = Color.Blue,
            fontSize = 128.sp,
            drawStyle = Stroke(
                width = strokeWith,
                miter = Stroke.DefaultMiter,
                join = StrokeJoin.Miter,
            ),
        ),
    ),
)

DrawStyle: Stroke

2.2.2 cap: StrokeCap 线帽/端点的样式

Canvas(modifier = Modifier.fillMaxWidth().height(200.dp)) {
    drawLine(
        color = Color.Blue,
        start = Offset.Zero,
        end = Offset(200.dp.toPx(), 0F),
        strokeWidth = 16.sp.toPx(),
        cap = StrokeCap.Butt,
    )
    drawLine(
        color = Color.Blue,
        start = Offset(0F, 20.dp.toPx()),
        end = Offset(200.dp.toPx(), 20.dp.toPx()),
        strokeWidth = 16.sp.toPx(),
        cap = StrokeCap.Square,
    )
    drawLine(
        color = Color.Blue,
        start = Offset(0F, 40.dp.toPx()),
        end = Offset(200.dp.toPx(), 40.dp.toPx()),
        strokeWidth = 16.sp.toPx(),
        cap = StrokeCap.Round,
    )
}

DrawStyle: StrokeCap

  1. 上例中三个线段的长度是相同的,但是设置线帽后长度却不同了,因为线帽是在线段外绘制的。
  2. 绘制圆形或文字的轮廓时,因为笔画/线条是闭合的不存在线帽,所以即使设置 cap 属性也是没有效果的。
// 绘制 圆形 和 文字轮廓时,cap 属性是没有效果的
drawCircle(
    color = Color.Red,
    radius = 50.dp.toPx(),
    center = Offset(200.dp.toPx(), 120.dp.toPx()),
    style = Stroke(
        width = 2.sp.toPx(),
        cap = StrokeCap.Round,
    ),
)
drawText(
    topLeft = Offset(0F, 64.dp.toPx()),
    textMeasurer = textMeasurer,
    text = "https://itmob.cn",
    style = TextStyle(
        fontSize = 24.sp,
        drawStyle = Stroke(
            width = 2.sp.toPx(),
            cap = StrokeCap.Round,
        ),
    ),
)
}

DrawStyle: StrokeCap.Round

2.2.3 join: StrokeJoin 笔画/线段的连接方式

线段的连接方式有三种:Miter 尖角/斜接, Round 圆角, Bevel 平角

// 绘制笔画连接方式为 Miter/斜接/尖角 的矩形
drawRect(
    color = Color.Red,
    topLeft =Offset(0F, 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        join = StrokeJoin.Miter,
    ),
)
// 绘制笔画连接方式为 Miter 且 miter 的斜接值为 0 的矩形
drawRect(
    color = Color.Blue,
    topLeft =Offset(0F, 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        miter = 0F,
        join = StrokeJoin.Miter,
    ),
)

// 绘制笔画连接方式为 Round/圆角 的矩形
drawRect(
    color = Color.Red,
    topLeft =Offset(60.dp.toPx(), 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        join = StrokeJoin.Round,
    ),
)
drawRect(
    color = Color.Blue,
    topLeft =Offset(60.dp.toPx(), 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        miter = 0F,
        join = StrokeJoin.Miter,
    ),
)

// 绘制笔画连接方式为 Bevel/平角 的矩形
drawRect(
    color = Color.Red,
    topLeft =Offset(120.dp.toPx(), 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        join = StrokeJoin.Bevel,
    ),
)
drawRect(
    color = Color.Blue,
    topLeft =Offset(120.dp.toPx(), 200.dp.toPx()),
    size =Size(50.dp.toPx(), 50.dp.toPx()),
    style = Stroke(
        width = 8.dp.toPx(),
        miter = 0F,
        join = StrokeJoin.Miter,
    ),
)

// 通过 Path 绘制三角形,且笔画的连接方式为 Miter/斜接/尖角
valpath =Path()
path.moveTo(0f, 300.dp.toPx())
path.lineTo(size.width/ 2f, 400.dp.toPx())
path.lineTo(size.width, 300.dp.toPx())
path.close()
drawPath(
    path = path,
    color = Color.Magenta,
    style = Stroke(
        width = 8.dp.toPx(),
    ),
)
// 绘制 miter 为0F 的尖角
drawPath(
    path = path,
    color = Color.Blue,
    style = Stroke(
        width = 8.dp.toPx(),
        miter = 0F,
        join = StrokeJoin.Miter,
    ),
)

DrawStyle: StrokeJoin

DrawStyle: StrokeJoin

通过绘制 miter 斜接值为 0 的蓝色图形,更直观的看到线段的不同连接方式的效果。

2.2.4 pathEffect: PathEffect 应用于笔划的效果

PathEffect 是应用于笔划的效果,null 表示要绘制实心笔划线。例如,可以用于将笔画的形状绘制为虚线或图案,或在线段交点周围应用处理。

PathEffect 源码:

/**
 * 应用于图形基本体的几何图形的效果。
 * 例如,这可以用于将形状绘制为虚线或成形图案,或在线段交点周围应用处理。
 */
interface PathEffect {
    companion object {

        /**
         * 将线段之间的锐角替换为指定半径的圆角
         */
        fun cornerPathEffect(radius: Float): PathEffect = actualCornerPathEffect(radius)

        /**
         * 将形状绘制为具有给定间隔的一系列短划线/虚线
         * intervals 间隔必须包含偶数个条目(>=2)
         * 
         * 例如:如果“intervals[]={10,20}”,并且phase=25,
         * 这将设置一个虚线路径,如下所示:5个像素断开,10个连续,20个像素断开,10个像素连续,20个像素断开
         * 
         * @param intervals Array of "on" and "off" distances for the dashed line segments
         * @param phase Pixel offset into the intervals array
         */
        fun dashPathEffect(intervals: FloatArray, phase: Float = 0f): PathEffect =
            actualDashPathEffect(intervals, phase)

        /**
         * 创建一个PathEffect,将 inner 指定效果应用于路径,然后将 outer 效果应用于 inner 效果的结果。
         */
        fun chainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
            actualChainPathEffect(outer, inner)

        /**
         * 通过在绘制的路径上盖指定的形状(从名称可理解为将指定的形状像盖章/邮戳一样绘制在路径上)。
         * 这仅适用于笔划形状,对于填充形状将被忽略。与此[PathEffect]一起使用的笔划宽度也将被忽略。
         *
         * @param shape 用于作为印/盖章记的 Path 类型的图形
         * @param advance 每个印记形状之间的间距
         * @param phase 在绘制第一个印记形状之前要偏移的量
         * @param style 如何在绘制印记时变换每个位置的形状
         */
        fun stampedPathEffect(
            shape: Path,
            advance: Float,
            phase: Float,
            style: StampedPathEffectStyle
        ): PathEffect = actualStampedPathEffect(shape, advance, phase, style)
    }
}

PathEffect 接口中只定义了一个伴生对象,其中包含四个返回值类型为 PathEffect 的方法,使用时根据开发的实际需求调用合适的方法即可。

chainPathEffect 方法与 android.graphics.ComposePathEffect 相同,因为它的源码中就是通过调用 ComposepathEffect 实现的。

internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
    AndroidPathEffect(
        android.graphics.ComposePathEffect(
            (outer as AndroidPathEffect).nativePathEffect,
            (inner as AndroidPathEffect).nativePathEffect
        )
    )

生成 PathEffect 的方法在上面的源码的注释中已经简单介绍,下面用实例来看看它们的用法:

  1. cornerPathEffect 圆角效果

     Canvas(Modifier.fillMaxWidth().height(100.dp)) {
        val triangle = Path().apply {
            moveTo(size.width / 2, 0F)
            lineTo(0F, size.height)
            lineTo(size.width, size.height)
            lineTo(size.width / 2, 0F)
            close()
        }
        drawPath(
            path = triangle,
            color = Color.Red,
            style = Stroke(
                width = 2.dp.toPx(),
                pathEffect = PathEffect.cornerPathEffect(16.dp.toPx()),
            ),
        )
    }
    Spacer(modifier = Modifier.size(16.dp))
    Canvas(Modifier.fillMaxWidth().height(100.dp)) {
        drawText(
            textMeasurer = textMeasurer,
            text = "https://itmob.cn",
            style = TextStyle(
                fontSize = 36.sp,
                drawStyle = Stroke(
                    width = 2.dp.toPx(),
                    pathEffect = PathEffect.cornerPathEffect(16.dp.toPx()),
                ),
            ),
        )
    }
    

    DrawStyle: PathEffect - cornerPathEffect

  2. dashPathEffect 虚线效果

    Canvas(Modifier.fillMaxWidth().height(100.dp)) {
        val triangle = Path().apply {
            moveTo(size.width / 2, 0F)
            lineTo(0F, size.height)
            lineTo(size.width, size.height)
            lineTo(size.width / 2, 0F)
            close()
        }
        drawPath(
            path = triangle,
            color = Color.Red,
            style = Stroke(
                width = 2.dp.toPx(),
                pathEffect = PathEffect.dashPathEffect(
                    intervals = floatArrayOf(10.dp.toPx(), 20.dp.toPx()),
                    phase = 25.dp.toPx(),
                ),
            ),
        )
        // 参考线,可以更方便看到 phase 为 25 时,首先会绘制时会有 5 的断开
        drawLine(
            color = Color.Blue,
            start = Offset(size.width / 2, 0F),
            end = Offset(size.width / 2, size.height),
        )
    }
    

    DrawStyle: PathEffect - dashPathEffect

  3. stampedPathEffect 印章效果

    Canvas(Modifier.fillMaxWidth().height(100.dp)) {
        val triangleStampShape = Path().apply {
            val size = 5.dp.toPx()
            moveTo(size / 2, 0F)
            lineTo(0F, size)
            lineTo(size, size)
            lineTo(size / 2, 0F)
            close()
        }
        val triangle = Path().apply {
            moveTo(size.width / 2, 0F)
            lineTo(0F, size.height)
            lineTo(size.width, size.height)
            lineTo(size.width / 2, 0F)
            close()
        }
        // 使用小三角形的形状作为印章效果绘制一个更大的三角形
        drawPath(
            path = triangle,
            color = Color.Red,
            style = Stroke(
                width = 2.dp.toPx(),
                pathEffect = PathEffect.stampedPathEffect(
                    shape = triangleStampShape,
                    advance = 10.dp.toPx(),
                    phase = 0F,
                    style = StampedPathEffectStyle.Morph,
                ),
            ),
        )
    }
    

    DrawStyle: PathEffect - StampedPathEffectStyle

    Canvas(Modifier.fillMaxWidth().height(200.dp)) {
        val triangleStampShape = Path().apply {
            val size = 5.dp.toPx()
            moveTo(size / 2, 0F)
            lineTo(0F, size)
            lineTo(size, size)
            lineTo(size / 2, 0F)
            close()
        }
        // 使用小三角形的形状作为印章效果绘制文字的笔画效果
        drawText(
            textMeasurer = textMeasurer,
            text = "itmob",
            style = TextStyle(
                fontSize = 146.sp,
                drawStyle = Stroke(
                    pathEffect = PathEffect.stampedPathEffect(
                        shape = triangleStampShape,
                        advance = 7.dp.toPx(),
                        phase = 0F,
                        style = StampedPathEffectStyle.Morph,
                    ),
                ),
            ),
        )
    }
    

    DrawStyle: PathEffect - StampedPathEffectStyle

    StampedPathEffectStyle 是沿绘制路径变换印章形状的的策略

    Row(
        modifier = Modifier.fillMaxWidth().height(100.dp),
        horizontalArrangement = Arrangement.SpaceEvenly,
    ) {
        val size = with(LocalDensity.current) { 10.dp.toPx() }
        val square = Path().apply {
            lineTo(size, 0f)
            lineTo(size, size)
            lineTo(0f, size)
            close()
        }
        val canvasModifier = Modifier
            .requiredSize(100.dp)
            .align(Alignment.CenterVertically)
    
        // StampedPathEffectStyle.Morph
        // 将修改弯曲正方形的线,以适应圆本身的曲率。
        // 以使每个正方形被渲染为完全包含在圆本身边界中
        Canvas(modifier = canvasModifier) {
            drawCircle(color = Color.Blue)
            drawCircle(
                color = Color.Red,
                style = Stroke(
                    pathEffect = PathEffect.stampedPathEffect(
                        shape = square,
                        advance = 20.dp.toPx(),
                        phase = 0f,
                        style = StampedPathEffectStyle.Morph,
                    ),
                ),
            )
        }
        // StampedPathEffectStyle.Rotate
        // 围绕圆绘制正方形,使每个正方形以圆为中心沿圆本身的曲率旋转
        // 但是正方形旋转后超出圆形边界,不会做特殊处理
        Canvas(modifier = canvasModifier) {
            drawCircle(color = Color.Blue)
            drawCircle(
                color = Color.Red,
                style = Stroke(
                    pathEffect = PathEffect.stampedPathEffect(
                        shape = square,
                        advance = 20.dp.toPx(),
                        phase = 0f,
                        style = StampedPathEffectStyle.Rotate,
                    ),
                ),
            )
        }
        // StampedPathEffectStyle.Translate
        // 围绕圆绘制正方形,每个正方形的左上角位于圆的圆周上
        // 也就是印章图形会沿路径以路径上的点为零点绘制,没有其他处理
        Canvas(modifier = canvasModifier) {
            drawCircle(color = Color.Blue)
            drawCircle(
                color = Color.Red,
                style = Stroke(
                    pathEffect = PathEffect.stampedPathEffect(
                        shape = square,
                        advance = 20.dp.toPx(),
                        phase = 0f,
                        style = StampedPathEffectStyle.Translate,
                    ),
                ),
            )
        }
    }
    

    DrawStyle: PathEffect - StampedPathEffectStyle

  4. chainPathEffect PathEffect 组合

    Canvas(Modifier.fillMaxWidth().height(150.dp)) {
        val triangleStampShape = Path().apply {
            val size = 5.dp.toPx()
            moveTo(size / 2, 0F)
            lineTo(0F, size)
            lineTo(size, size)
            lineTo(size / 2, 0F)
            close()
        }
        val triangle = Path().apply {
            moveTo(size.width / 2, 0F)
            lineTo(0F, size.height)
            lineTo(size.width, size.height)
            lineTo(size.width / 2, 0F)
            close()
        }
        // 使用两个 PathEffect 组合绘制效果
        drawPath(
            path = triangle,
            color = Color.Red,
            style = Stroke(
                width = 2.dp.toPx(),
                pathEffect = PathEffect.chainPathEffect(
                    outer = PathEffect.stampedPathEffect(
                        shape = triangleStampShape,
                        advance = 10.dp.toPx(),
                        phase = 0F,
                        style = StampedPathEffectStyle.Morph,
                    ),
                    inner = PathEffect.cornerPathEffect(36.dp.toPx()),
                ),
            ),
        )
    }
    

    DrawStyle: PathEffect

总结

以上就是 DrawStyle 的详解和示例。无论绘制图形,图像,还是文字时都可以通过它们的 DrawStyle 属性来修改它们的线段或轮廓的效果。通过这个属性也可以实现更复杂的文字样式,这将在下一篇文章中介绍。

其他

更多关于 Jetpack Compose 文本的介绍:

在 Jetpack Compose 中怎样实现印章样式(弧形)的文字效果?

Jetpack Compose 中如何自定义文本笔画的描边效果

Jetpack Compose 中怎样隐藏或禁用 TextField(文本字段)的光标、文本选择手柄和文本工具栏?

怎样处理 Jetpack Compose 中文本的长按选择和点击事件(如何在 Text 中添加超链接?)

Jetpack Compose TextField VisualTransformation 视觉转换详解-高亮显示URL-格式化显示银行卡号码

怎样处理 Jetpack Compose 中文本的长按选择和点击事件(如何在 Text 中添加超链接?)

Jetpack Compose 怎样实现跑马灯效果(Marquee)

在 Jetpack Compose 中使用 DrawScope.drawText API 将文本绘制到 Canvas 上