Android 文字绘制

文字单位问题

dp

不跟随系统缩放,大部分场景不需要跟随系统缩放。

sp

适用于需要跟随系统缩放的文字,如小说、新闻资讯阅读类控件文字。

文字度量

在这里插入图片描述
自定义View中字体的绘制,默认以 baseline 为基准进行绘制、对齐等操作。
top、bottom 是文字的上下极限,无论什么字体他的顶部底部不会超过 top
和 bottom。
ascent、descent系统推荐的字体上下极限。

文字居中

// 虚线画笔
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 1f.dp
    style = Paint.Style.STROKE
    pathEffect = DashPathEffect(floatArrayOf(5f.dp, 5f.dp), 0f)
}
// 文字画笔
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.RED
    textSize = 50f.dp
}
// 虚线路径
private val centerPath = Path()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    centerPath.reset()
    centerPath.moveTo(width / 2f, 0f)
    centerPath.lineTo(width / 2f, height.toFloat())
    centerPath.moveTo(0f, height / 2f)
    centerPath.lineTo(width.toFloat(), height / 2f)
}
override fun onDraw(canvas: Canvas) {
	//虚线
    canvas.drawPath(centerPath, linePaint)
    //居中绘制文字
    canvas.drawText("abgj", width / 2f, height / 2f, textPaint)
}

代码效果:
在这里插入图片描述
可以看出 文字的绘制起点是 baseline 左侧,为了水平居中可以给文字画笔增加如下代码:

private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.RED
    textSize = 50f.dp
    textAlign = Paint.Align.CENTER
}

效果:
在这里插入图片描述
水平居中设置 paint 的 textAlign 即可。
垂直方向居中就需要用另外两种办法,首先需要根据实际业务情况判断。

静态文字

对于静态文字,可以使用 paint 的 getTextBounds 方法获取到所绘制文字的 rect,也就可以获取到所绘制文字的上下界,然后上下界的距离取中间作为垂直偏移量即可:

override fun onDraw(canvas: Canvas) {
    canvas.drawPath(centerPath, linePaint)
    val text = "abgj"
    val textRect = Rect()
    textPaint.getTextBounds(text, 0, text.length, textRect)
    canvas.drawText(
        text,
        width / 2f,
        height / 2f - (textRect.top + textRect.bottom) / 2,
        textPaint
    )
}

效果:
在这里插入图片描述

动态文字

如果文字、字体动态变化,则通过 paint.getTextBounds 获取的 rect 会随之改变,那么文字就会出现跳动的bug,这种情况就用到了上面提到的 ascent、descent,通过 paint 的 getFontMetrics 可以获取到 FontMetrics 对象,从中可以获得相关信息:

override fun onDraw(canvas: Canvas) {
    canvas.drawPath(centerPath, linePaint)
    val text = "abgj"
    val fontMetrics = textPaint.fontMetrics
    canvas.drawText(
        text,
        width / 2f,
        height / 2f - (fontMetrics.ascent + fontMetrics.descent) / 2,
        textPaint
    )
}

效果:
在这里插入图片描述
为什么不用 (fontMetrics.top + fontMetrics.bottom) / 2 呢?
top、bottom 是文字的上下极限,大部分文字都不会触碰到这个极限,只有极少数文字会超出 ascent、descent 的界限,所以使用 ascent、descent 就足够了。

文字贴边

文字居中问题解决了之后,来用代码试试文字紧贴屏幕左上角,绘制两段文字,字体一大一小:

textPaint.textAlign = Paint.Align.LEFT
textPaint.textSize = 20f.dp
fontMetrics = textPaint.fontMetrics
canvas.drawText(text, 0f, 0f, textPaint)
textPaint.textSize = 100f.dp
fontMetrics = textPaint.fontMetrics
canvas.drawText(text, 0f, 0f, textPaint)

效果:
在这里插入图片描述
发现文字压根没显示全,细想一下这个问题还是由于文字绘制默认以baseline为基准,那么我们只需要调整垂直偏移即可。

上下贴边

textPaint.textAlign = Paint.Align.LEFT
textPaint.textSize = 20f.dp
// 利用 getTextBounds 获得top偏移量
// val textRect = Rect()
// textPaint.getTextBounds(text, 0, text.length, textRect)
// canvas.drawText(text, 0f, 0f - textRect.top, textPaint)
fontMetrics = textPaint.fontMetrics
canvas.drawText(text, 0f, 0f - fontMetrics.top, textPaint)
textPaint.textSize = 100f.dp
fontMetrics = textPaint.fontMetrics
canvas.drawText(text, 0f, 0f - fontMetrics.top, textPaint)

效果:
在这里插入图片描述

代码中有两种方式计算垂直贴边的方法,getTextBounds 、fontMetrics。当使用 getTextBounds 其实会贴的更紧一些,使用 fontMetrics.top 相当于是使用文字的极限距离会有小小的间隙。视情况而定使用哪种方法。

左右贴边

解决了上贴边,再来看看两段文字的左侧,当文字较小则是紧贴左侧,而文字变大时就产生了距离,但是 fontMetrics 并没有 left right 这种成员变量。就只能使用 getTextBounds 获取 rect 的 left right 进行左右贴边:

textPaint.textAlign = Paint.Align.LEFT
textPaint.textSize = 20f.dp
var textRect = Rect()
textPaint.getTextBounds(text, 0, text.length, textRect)
canvas.drawText(text, 0f - textRect.left, 0f - textRect.top, textPaint)
textPaint.textSize = 100f.dp
textPaint.getTextBounds(text, 0, text.length, textRect)
canvas.drawText(text, 0f  - textRect.left, 0f - textRect.top, textPaint)

效果:
在这里插入图片描述
当使用 getTextBounds 进行贴边时,可以看出和上面使用 fontMetrics 的效果在顶部也有一些差距,因为 fontMetrics 的 top 是文字的极限 top。

多行绘制

再绘制文字时除了以上对齐问题,还有多行绘制也是非常常见的需求。当遇到文字需要折行的情况用什么方法处理呢?

StaticLayout

StaticLayout 是专门用于多行绘制的,使用代码如下:

textPaint.textAlign = Paint.Align.LEFT
// 文本太长 省略了
val text = "长妈妈曾经讲给我一个故事听:。..."
 val staticLayout = StaticLayout.Builder
     /**
      * 参数:
      * text:绘制的文本
      * 0:绘制文本的起始位置
      * text.length:绘制文本的结束位置
      * textPaint:画笔
      * width:可用宽度
      */
     .obtain(text, 0, text.length, textPaint, width)
     .build()
 staticLayout.draw(canvas)

效果:
在这里插入图片描述

图文混排(文字环绕)

在多行绘制的情况中,经常也会有一些图文混排的需求,一段文字的左右两侧可能会需要插入图片,那么就需要文字绕开图片,实现思路:文字一行一行绘制,每绘制一行之前都需要计算出这一行的可用宽度(是否有图片,有图片则减去图片的宽度),绘制完成后记录垂直偏移、文本绘制的位置,绘制下一行时进行偏移。实现代码:

//文本太长了 省略了...
val text = "长妈妈曾经讲给我一个故事听:。..."
//左侧增加一个内容图片
val bitmapMarginTop = 50f.dp
val bitmapWidth = 100f.dp
val bitmap = getTestBitmap(resources, bitmapWidth.toInt())
canvas.drawBitmap(bitmap, width - bitmapWidth, bitmapMarginTop, paint)
//绘制文字
textPaint.textAlign = Paint.Align.LEFT
val fontMetrics = textPaint.fontMetrics
//垂直偏移量
var offsetY = 0f - fontMetrics.top
//下一行文字的绘制开始索引
var nextLineStartIndex = 0
//用于存储每一行的宽度, breakText 需要这个参数
val lineWidths = floatArrayOf(0f)
while (text.length > nextLineStartIndex) {
    //计算这一行的可用宽度
    val canUseWidth = if (
        //注意这里的判断一定要考虑文字的 fontMetrics.bottom 和 fontMetrics.top
        offsetY + fontMetrics.bottom in bitmapMarginTop..bitmapMarginTop + bitmap.height
        || offsetY + fontMetrics.top in bitmapMarginTop..bitmapMarginTop + bitmap.height
    ) {
        width - bitmapWidth
    } else {
        width.toFloat()
    }
    // 计算这一行能绘制几个文字
    val drawCount = textPaint.breakText(
        text,
        nextLineStartIndex,
        text.length,
        true,
        canUseWidth,
        lineWidths
    )
    canvas.drawText(
        text,
        nextLineStartIndex,
        nextLineStartIndex + drawCount,
        0f,
        offsetY,
        textPaint
    )
    // 记录文字索引、垂直偏移量
    nextLineStartIndex += drawCount
    offsetY += textPaint.fontSpacing
}

效果:
在这里插入图片描述

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值