- 作者:老汪软件技巧
- 发表时间:2024-11-20 17:03
- 浏览量:
在工作中遇到一个实现文本描边效果的需求,这里记录一下。
Android 中文本的描边效果,我们可以看成是两个文本相互叠加起来。这样最容易想到的方案就有两种:
方案一的代码如下:
class StrokeTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : AppCompatTextView(context, attrs, defStyle) {
private val strokePaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 5f // 描边宽度
color = Color.RED // 描边颜色,白色
typeface = this@StrokeTextView.typeface
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
// 设置文本大小
strokePaint.textSize = textSize
// 获取文本内容
val text = text.toString()
val fontMetrics = strokePaint.fontMetricsInt
val baseline = (height - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top
// 绘制描边
canvas.drawText(text, paddingLeft.toFloat(), baseline.toFloat(), strokePaint)
// 绘制文本内容
super.onDraw(canvas)
}
}
效果如下所示:
但是对于多行文本,就有问题了。这是因为我们没有在代码中处理这些情况,而且计算文本的换行也比较麻烦,因此不建议使用方案一。
接下来来试试方案二,用两个 TextView 来叠加实现描边效果。代码示例如下:
class StrokeTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : AppCompatTextView(context, attrs, defStyle) {
//用于描边的TextView
private var backGroundText = TextView(context, attrs, defStyle)
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
//同步布局参数
backGroundText.layoutParams = params
super.setLayoutParams(params)
}
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val tt = backGroundText.text
//两个TextView上的文字必须一致
if (tt == null || tt != this.text) {
backGroundText.text = text
this.postInvalidate()
}
backGroundText.measure(widthMeasureSpec, heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
backGroundText.layout(left, top, right, bottom)
super.onLayout(changed, left, top, right, bottom)
}
override fun onDraw(canvas: Canvas) {
val tp1 = backGroundText.paint
//设置描边宽度
tp1.strokeWidth = 2f
//背景描边并填充全部
tp1.style = Paint.Style.FILL_AND_STROKE
//设置描边颜色
backGroundText.setTextColor(Color.RED)
backGroundText.gravity = gravity
backGroundText.draw(canvas)
super.onDraw(canvas)
}
}
效果如下所示:
可以看到,如果使用两个 TextView 叠加绘制,就不用考虑换行,以及多行的计算问题了,方便了很多。但是这个方案还是有点缺陷,当文本的颜色为半透明的时候,文本的背景会是描边的颜色,而不是原背景。效果如下所示:
如果文本是半透明的情况,上面的叠加方案就无法使用了,需要考虑新的方案。要想半透明的文本效果不被影响,我们只有绘制文本的描边,正好 Paint 提供了 getTextPath 方法来获取文本的 path 路径。通过获取的 path 路径,我们就可以正常的绘制描边了,代码示例如下:
class StrokeTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatTextView(context, attrs, defStyle) {
// 描边画笔
private val outlinePaint = Paint().apply {
isAntiAlias = true
}
// 描边路径
private val outlinePath = Path()
private var strokeWidth = 1f
/**
* 初始化画笔属性
*/
init {
outlinePaint.strokeWidth = 1f
outlinePaint.style = Paint.Style.STROKE
outlinePaint.color = Color.parseColor("#CCFFFFFF")
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 布局变化时更新描边路径
val xOffset = layout.getLineLeft(0) + paddingLeft
val baseline = layout.getLineBaseline(0) + paddingTop.toFloat()
paint.getTextPath(text.toString(), 0, text.length, xOffset, baseline, outlinePath)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (strokeWidth > 0) {
canvas.save()
// 确保不在字符内部绘制
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipPath(outlinePath, android.graphics.Region.Op.DIFFERENCE)
} else {
canvas.clipOutPath(outlinePath)
}
canvas.drawPath(outlinePath, outlinePaint)
canvas.restore()
}
}
}
效果如下所示:
获取路径的方案虽然满足半透明文本的需求,但是对于多行,还是需要我们自己来处理,比较麻烦。因此对于没有半透明文本的描边效果的需求,还是推荐使用 使用两个 TextView 叠加 的方案;而半透明文本描边的需求,则需要考虑是否是多行,来做相应的处理。