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,
),
),
)
如上代码使用 Fill
作为 drawStyle
属性。Fill 是 drawStyle
属性的默认值,表示应使用所提供的颜色或图案完全填充形状。
2. DrawStyle 属性详解
Compose 中定义了两个 DrawStyle 的子类:Fill
和 Stroke
Fill
是 drawStyle
属性的默认值,表示应使用所提供的颜色或图案完全填充形状。
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,
),
),
),
)
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,
)
}
- 上例中三个线段的长度是相同的,但是设置线帽后长度却不同了,因为线帽是在线段外绘制的。
- 绘制圆形或文字的轮廓时,因为笔画/线条是闭合的不存在线帽,所以即使设置 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,
),
),
)
}
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,
),
)
通过绘制 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
的方法在上面的源码的注释中已经简单介绍,下面用实例来看看它们的用法:
-
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()),
),
),
)
}
-
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),
)
}
-
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,
),
),
)
}
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,
),
),
),
)
}
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,
),
),
)
}
}
-
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
的详解和示例。无论绘制图形,图像,还是文字时都可以通过它们的 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 上