Android 绘制文本

176 阅读3分钟

Android绘制文本

下面以绘制"Hello Word"在屏幕中间位置为例。

已知

高度 height

宽度 width

1.准备

//画笔
val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    textSize = 100f
    color = Color.BLACK
}
//获取文本宽度
val textWidth = paint.measureText("Hello Word")
//获取绘制文本时所需descent ascent top 和 bottom坐标
val fontMetrics = paint.fontMetrics // 直接获取Paint的FontMetrics,无需额外变量
val ascent = fontMetrics.ascent // 获取ascent值
val descent = fontMetrics.descent // 获取descent值
val top = fontMetrics.top
val bottom = fontMetrics.bottom

这些对象需要了解,才能对文本进行绘制

2.计算

2.1 绘制文本

绘制文本方法,使用Canvas进行绘制。

/**
 * Draw the
 text, with origin at (x,y), using the specified paint. The origin is interpreted
 * based on the Align setting in the paint.
 *
 * @param text The text to be drawn
 * @param x The x-coordinate of the origin of the text being drawn
 * @param y The y-coordinate of the baseline of the text being drawn
 * @param paint The paint used for the text (e.g. color, size, style)
 */
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
    super.drawText(text, x, y, paint);
}

参数1:文本。

参数2: x坐标,文本最左侧坐标。

参数3: y坐标,需要通过基线进行定位。

参数4:画笔

2.2 计算x,y坐标位置

  • 2.2.1 计算X坐标
//获取文本宽度
val textWidth = paint.measureText("Hello Word")
val x = width/2 - textWidth 

x坐标为: width/2 - textWidth/2

  • 2.2.2 计算Y坐标
  • 2.2.2.1 五根线

绘制文本需对top线、ascent线、descent线、baseLine线和bottom线了解,线的大致位置如图。

3cf98e3b9df64c66a3678ac3259e711a~tplv-73owjymdk6-watermark.png

top线到bottom线之间的距离是文字可以绘制的最大高度但是一般绘制的文字高度只有ascent线到descent线距离大小。图中还可以看到第五根线BaseLine线,这根线就是我们需要寻找的坐标Y,但是Android未直接提供获取接口。 Android SDK接口中,只为我们提供了关于top线、ascent线、descent线、baseLine线和bottom线相关的几个值。

val ascent = fontMetrics.ascent
val descent = fontMetrics.descent
val top = fontMetrics.top
val bottom = fontMetrics.bottom

从图中可以看到top线、ascent线、descent线、baseLine线和bottom线四条线的大致位置。

代码块中的几个数据是根据他们的Y坐标计算得出,公式如下:

fontMetrics.ascent = ascent线坐标 - BaseLine线Y坐标

fontMetrics.descent = descent - BaseLine线Y坐标

fontMetrics.top = top - BaseLine线Y坐标

fontMetrics.bottom = bottom - BaseLine线Y坐标

因为Android坐标Y轴由上到下大小逐渐增大,所以:fontMetrics.ascent和fontMetrics.top为负值,fontMetrics.descent和fontMetrics.bottom为正值。

  • 2.2.2.2 计算中间线到基线位置

假设在ascent线和descent线有一条height/2线,这条线到ascent线与到descent线距离相等。

  • 如图:

未命名绘图.drawio.svg

注意: 如图ABC ,A代表ascent线到BaseLine线距离。B代表height/2线到BaseLine线距离。C为baseLine线到descent线距离。

图中如果想要将文本绘制在ascent线和descent线中间,只需要使用BaseLine线作为Y坐标就可以了。由于Android没有直接提供相关接口,我们需要使用fontMetrics.ascent、fontMetrics.descent、fontMetrics.top、 fontMetrics.bottom计算获取得出所需值。由于想把文字绘制到中间位置,只需要将height/2线定位到视图中间位置,中间位置加上距离B就可以做到。

中间height/2线到基线距离B公式

中间height/2线到基线距离 = (fontMetrics.descent-fontMetrics.ascent)/2 -fontMetrics.descent

  • 2.2.2.3 推导中间线到基线距离公式:

从上面2.2.2.2图中可以看到这样关系A = 2(B+C),故B = A/2-C。

由2.2.2.1中fontMetrics.descent和fontMetrics.ascent计算过程,可以看出fontMetrics.descent为BaseLine线到descent线的距离,-fontMetrics.ascent为BaseLine线到ascent线距离。所以A = fontMetrics.descent-fontMetrics.ascent。

C = fontMetrics.descent

所以B = A/2-C = (fontMetrics.descent-fontMetrics.ascent)/2 - fontMetrics.descent。

注意:fontMetrics.ascent为负数

  • Y绘制坐标
Y= height/2+B = height/2 + (fontMetrics.descent-fontMetrics.ascent)/2 - fontMetrics.descent

3.代码

  • 3.1 布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
​
​
​
    <com.kt.view.textview.CustomTextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
​
​
​
</androidx.constraintlayout.widget.ConstraintLayout>
  • 3.2 代码
package com.kt.view.textview
​
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.util.Log
import android.widget.TextView
import kotlin.math.abs
​
class CustomTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0):
    TextView(context, attrs, defStyleAttr, defStyleRes) {
​
    val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        textSize = 100f
        color = Color.BLACK
    }
​
​
    override fun onDraw(canvas: Canvas) {
      val baseLine =   height/2 + getBaseline(paint)
      val textWidth = paint.measureText("Hello Word")
      //绘制最上面的top
      //canvas.drawLine(0f,)
      canvas.drawText("Hello Word", width/2 - textWidth/2, baseLine, paint)
    }
​
    private fun getBaseline(paint: Paint): Float {
        val fontMetrics = paint.fontMetrics
        val ascent = fontMetrics.ascent // 获取ascent值
        val descent = fontMetrics.descent // 获取descent值
        return (descent - ascent) / 2 - descent
    }
}
  • 3.3 展示

9e745f039c8e4e048e7c0db715fcfa39~tplv-73owjymdk6-watermark.png

4.总结

  • 1.获取文本宽度
    val textWidth = paint.measureText("Hello Word")
  • 2.获取基线位置
    private fun getBaseline(paint: Paint): Float {
        val fontMetrics = paint.fontMetrics
        val ascent = fontMetrics.ascent // 获取ascent值
        val descent = fontMetrics.descent // 获取descent值
        return (descent - ascent) / 2 - descent
    }