自定义控件绘图(drawText)篇三

参考

  1. https://blog.csdn.net/harvic880925/article/details/50423762

drawText复杂但很重要;

四线格与基线

自定义控件绘图(drawText)篇三_第1张图片
来着原博客-四线格

在canvas在利用drawText绘制文字时,也是有规则的,这个规则就是基线!

自定义控件绘图(drawText)篇三_第2张图片
来自原博客-基线

基线就是4线格的第三条线,确定好基线的位置,文字的位置就好办了

Canvas.drawText()

drawText()与基线

/**
 * x: 绘制原点x坐标 
 * y:绘制原点y坐标 
 */
public void drawText(String text, float x, float y, Paint paint)

示例代码(在坐标近原点处,画文字)

val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    style = Paint.Style.STROKE
    color = Color.RED
    textSize = 38f
}
canvas.drawText("Android开发艺术探索", 0f, 10f, paint1)

实际效果为:


自定义控件绘图(drawText)篇三_第3张图片
效果

为什么会这样?
但这里有两个参数需要非常注意,表示原点坐标的x和y,这里传进去的原点参数(x,y)不是所在绘制文字所在矩形的左上角的点
如果要画"harvic's blog"这几个字,这个原点坐标应当是下图中绿色小点的位置,y所代表的是基线的位置!

自定义控件绘图(drawText)篇三_第4张图片
图片来自原博客-基线

这样,就能明白上面的绘制为什么被挡住了,基线跟Y坐标有关系

指定基线位置

val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    style = Paint.Style.FILL
    color = Color.RED
    textSize = 48f
}

val baselineX = 0f
val baselineY = 200f
// 画基线
canvas.drawLine(baselineX, baselineY, width.toFloat(), baselineY, paint1)
paint1.color = Color.GREEN
canvas.drawText("Android开发艺术探索,fffggg", baselineX, baselineY, paint1)
自定义控件绘图(drawText)篇三_第5张图片
红色为基线

结论

  • drawText是中的参数y是基线的位置;
  • 一定要清楚的是,只要x坐标、基线位置、文字大小确定以后,文字的位置就是确定的了;

paint.setTextAlign(Paint.Align.XXX)

设置文字对齐方向,在drawText()中,y表示基线baseline位置,那么x表示什么呢?

x代表所要绘制文字所在矩形的相对位置。相对位置就是指指定点(x,y)在在所要绘制矩形的位置;相对位置,只有左、中、右三种,由paint.setTextAlign(Paint.Align.XXX)指定;
相对位置是根据所要绘制文字所在矩形来计算的;

示例:

val stringText = "Android开发艺术探索,fffggg"

val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    textSize = 48f
}

// 平移到中间
canvas.translate(width / 4.toFloat(), height / 2.toFloat())
paint1.color = Color.GRAY
paint1.style = Paint.Style.STROKE
val rect = Rect()
paint1.getTextBounds(stringText, 0, stringText.length, rect)
// 画文字所在的矩形
canvas.drawRect(0f, 0f, rect.width().toFloat(), rect.height().toFloat(), paint1)

val baseLineX = -width / 2.toFloat()
val baselineY = rect.height().toFloat()
// 画基线
paint1.color = Color.RED
canvas.drawLine(baseLineX, baselineY, width.toFloat(), baselineY, paint1)

// 画文字
paint1.color = Color.GREEN
paint1.style = Paint.Style.FILL
paint1.textAlign = Paint.Align.LEFT  // CENTER,RIGHT
canvas.drawText(stringText, 0f, baselineY, paint1)

效果:

  • 红色为基线;
  • 灰色框为text所在rect;
  • 注意绿色文字变化;
自定义控件绘图(drawText)篇三_第6张图片
textAlign=LEFT 效果
CENTER
自定义控件绘图(drawText)篇三_第7张图片
RIGHT

drawText的四线格与FontMetrics

Text的绘图五线格

通过上面的例子,我们可以看到文字绘制所在的矩形,没有完全包含文字(垂直方法,注意后面的g字母),这是因为系统在绘制Text时,还是有其它线的;

自定义控件绘图(drawText)篇三_第8张图片
图片来自原博客

除了基线,如上图所示,另外还有四条线,共5条线:

  • ascent: 系统建议的,绘制单个字符时,字符应当的最高高度所在线
  • descent:系统建议的,绘制单个字符时,字符应当的最低高度所在线
  • top: 可绘制的最高高度所在线
  • bottom: 可绘制的最低高度所在线
  • baseline:基线

为什么需要这么多线?,来自原博客的解释:

我们来看一下电视的显示。用过视频处理工具的同学,在制作视频时,视频显示位置都会有一个安全区域框,如下图所示:


自定义控件绘图(drawText)篇三_第9张图片
来自原博客

如上图所示,黑色部分表示电视屏幕,红色框就表示安全区域框。
这个安全框是用来干嘛的?这个安全框就是系统推荐给我们的显示区域,虽然说我们可以讲电视屏幕是每个区域都是可以显示图像的,但由于制式的不同,每个国家的屏幕大小并不一定我们这里的屏幕大小一致,当遇到不一致时,就会裁剪。但系统给我们推荐的显示区域是无论哪种制式都是可以完整显示出来的,所以我们在制作视频时,尽量要把要显示的图像放在所推荐的显示区域内。
同样,在这里,我们在绘制文字时,ascent是推荐的绘制文字的最高高度,就表示在绘制文字时,尽力要在这个最高高度以下绘制文字。descent是推荐的绘制文字的最底高度线,同样表示是在绘制文字时尽量在这个descent线以上来绘制文字。而top线则指该文字可以绘制的最高高度线,bottom则是表示该文字可以绘制的最低高度线。ascent,descent是系统建议上的绘制高度而top,bottom则是物理上屏幕最高,最低可以画的高度值。他们的差别与我们上面说的视频处理的安全框和屏幕的道理是一样的。

FontMetrics(内容全部copy 自原博客)

系统在画文字时的五条线,baseline、ascent、descent、top、bottom;
baseline的位置是在构造drawText()时的参数y来决定的;
那ascent,descent,top,bottom这些线的位置要怎么计算出来呢?

Android给我们提供了一个类:FontMetrics,它里面有5个成员变量:

public static class FontMetrics {
        public float   top;
        public float   ascent;
        public float   descent;
        public float   bottom;
        public float   leading;  // 特殊
}

计算方法分别为:

  • ascent = ascent线的y坐标 - baseline线的y坐标;
  • descent = descent线的y坐标 - baseline线的y坐标;
  • top = top线的y坐标 - baseline线的y坐标;
  • bottom = bottom线的y坐标 - baseline线的y坐标;
  • leading: 行间距,即前一行的descent与下一行的ascent之间的距离;
自定义控件绘图(drawText)篇三_第10张图片
copy from 原博客

上图中,我们先说明两点,然后再回过头来看上面的公式:

  1. X轴,Y轴的正方向走向是X轴向右是正方向,Y轴向下是正方向,所以越往下Y坐标越大!
  2. 大家千万不要将FontMetrics中的ascent,descent,top,bottom与现实中的ascent,descent,top,bottom所在线混淆!这几条线是真实存在的,而FontMetrics中的ascent,descent,top,bottom这个变量的值就是用来计算这几条线的位置的。

下面我们将看到如何利用这几个变量来计算这几条线的位置。
看完这个图,我们再重新说说这几个公式,我们拿一个来说吧,其它都是相同的道理。

ascent = ascent线的y坐标 - baseline线的y坐标

FontMetrics的这几个变量的值都是以baseline为基准的,对于ascent来说,baseline线在ascent线之下,所以必然baseline的y值要大于ascent线的y值,所以ascent变量的值是负的

同理,对于descent而言:

descent = descent线的y坐标 - baseline线的y坐标

descent线在baseline线之下,所以必然descent线的y坐标要大于baseline线的y坐标,所以descent变量的值必然是正数。

得到Text四线格的各线位置

如何通过这些变量来得到对应线所在位置吧。
我们先列出来一个公式:

ascent线Y坐标 = baseline线Y坐标 + fontMetric.ascent;

推算过程如下:

  • 因为ascent线的Y坐标等于baseline线的Y坐标减去从baseline线到ascent线的这段距离, 也就是:(|fontMetric.ascent|表示取绝对值)

    • ascent线Y坐标 = baseline线Y坐标 - |fontMetric.ascent|;
  • 又因为fontMetric.ascent是负值,所以:

    • ascent线Y坐标 = baseline线Y坐标 - |fontMetric.ascent|;
    • ascent线Y坐标 = baseline线Y坐标 - ( -fontMetric.ascent);
    • ascent线Y坐标 = baseline线Y坐标 + fontMetric.ascent;
  • 这就是整个推算过程,没什么难度,同理可以得到:

    • ascent线Y坐标 = baseline线的y坐标 + fontMetric.ascent;
    • descent线Y坐标 = baseline线的y坐标 + fontMetric.descent;
    • top线Y坐标 = baseline线的y坐标 + fontMetric.top;
    • bottom线Y坐标 = baseline线的y坐标 + fontMetric.bottom;

获取FontMetrics对象

获取FontMetrics对象是根据paint对象来获取的,有了她,我们5条线,就可以画出来了;

val fontMetrics = paint1.fontMetrics  // 通过paint对象获取 fontMetrics对象
val fontMetricsInt = paint1.fontMetricsInt  // int 类型
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        strokeWidth = 2f
        textSize = 100f
        setTextAlign(Paint.Align.LEFT)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val stringText = "Android开发艺术探索"

        val baselineX = 0f
        val baselineY = 200f

        // 画文字
        canvas.drawText(stringText, baselineX, baselineY, paint1)

        // 计算各线在位置
        val fontMetrics = paint1.fontMetrics
        val top = baselineY + fontMetrics.top
        val ascent = baselineY + fontMetrics.ascent
        val descent = baselineY + fontMetrics.descent
        val bottom = baselineY + fontMetrics.bottom

        // top
        paint1.color = Color.BLUE
        canvas.drawLine(baselineX, top, width.toFloat(), top, paint1)
        // ascent
        paint1.color = Color.GREEN
        canvas.drawLine(baselineX, ascent, width.toFloat(), ascent, paint1)
        //画基线
        paint1.color = Color.RED
        canvas.drawLine(baselineX, baselineY, width.toFloat(), baselineY, paint1)
        // descent
        paint1.color = Color.BLACK
        canvas.drawLine(baselineX, descent, width.toFloat(), descent, paint1)
        // bottom
        paint1.color = Color.MAGENTA
        canvas.drawLine(baselineX, bottom, width.toFloat(), bottom, paint1)
    }

效果如下(注意黑色的线(descent),不是那么明显):


自定义控件绘图(drawText)篇三_第11张图片
五条线

获取绘制文字的宽、高

上面的例子中,我们通过paint.getTextBounds获取过;

这里,如何获取所绘制字符串所占区域的高度、宽度和仅包裹字符串的最小矩形呢?

字符串所占高度和宽度

高度的获取

字符串所占高度很容易得到,直接用bottom线所在位置的Y坐标减去top线所在位置的Y坐标就是字符串所占的高度:

// 1.高度
Paint.FontMetricsInt fm = paint.getFontMetricsInt();  
int top = baseLineY + fm.top;  
int bottom = baseLineY + fm.bottom;  
//所占高度  
int height = bottom - top;  

// 2.宽
int width = paint.measureText(String text); 

最小矩形
要获取最小矩形,我们上面有用到过getTextBounds

val rect = Rect()
paint1.getTextBounds(stringText, 0, stringText.length, rect)
测量结果

从上图中,可以看到这个矩形的左上角位置为(1,-81),右下角的位置为(1044,21);左上角的Y坐标是个负数?因为我们并没有给getTextBounds()传递基线位置。那它就是以(0,0)为基线来得到这个最小矩形的!所以这个最小矩形的位置就是以(0,0)为基线的结果!

获取最小矩形的实际位置

自定义控件绘图(drawText)篇三_第12张图片
copy 自原博客

在上面这个图中,我们将黑色矩形平行下移距离Y(黄色线依照的是基线的位置),那么平移后的左上角点的y坐标就是 y2 = y1 + Y;
同样的道理,由于paint.getTextBounds()得到最小矩形的基线是y = 0;那我们直接将这个矩形移动baseline的距离就可以得到这个矩形实际应当在的位置了。
所以矩形应当所在实际位置的坐标是:

Rect minRect = new Rect();  
paint.getTextBounds(text,0,text.length(),minRect);  
//最小矩形,实际top位置 , top  再加上 baselineY
int minTop = bounds.top + baselineY;  
//最小矩形,实际bottom位置, bottom  再加上 baselineY  
int minBottom = bounds.bottom + baselineY;  

例子:

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        strokeWidth = 2f
        textSize = 100f
        setTextAlign(Paint.Align.LEFT)
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val stringText = "Android g 开发艺术探索"

    val baselineX = 0f
    val baselineY = 200f

    // == 1. 画text所占的区域Rect
    val fm = paint.fontMetricsInt
    val top = baselineY + fm.top            // 加 baselineY
    val bottom = baselineY + fm.bottom
    val width = paint.measureText(stringText).toInt()
    val rect = RectF(baselineX, top, baselineX + width, bottom)

    paint.color = Color.GREEN
    canvas.drawRect(rect, paint)

    // == 2.写文字
    paint.color = Color.BLACK
    canvas.drawText(stringText, baselineX, baselineY, paint)

    // == 3.画文字对应的最小矩形
    val minRect = Rect()
    paint.getTextBounds(stringText, 0, stringText.length, minRect)
    minRect.top = baselineY.toInt() + minRect.top
    minRect.bottom = baselineY.toInt() + minRect.bottom
    paint.color = Color.RED
    paint.style = Paint.Style.STROKE
    canvas.drawRect(minRect, paint)
}
自定义控件绘图(drawText)篇三_第13张图片
注意红色的框 - 最小矩形

红色为最小矩形(安全区域),外层绿色背景为text所在矩形区域,也就是上面提到的电视屏幕

给定左上顶点,并绘制文字

假定给出所要绘制矩形的左上角顶点坐标(left,top),然后画出这个文字;
因为drawText()中传进去的Y坐标是基线的位置,所以我们就必须根据top的位置计算出baseline的位置;

公式:
上面得出:
top线Y坐标 = baseline线的y坐标 + fontMetric.top;
所以:
fontMetrics.top = top - baseline;
所以:
baseline = top - FontMetrics.top;
因为FontMetrics.top是可以得到的,又因为我们的top坐标是给定的,所以通过这个公式就能得到baseline的位置了。

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    textSize = 38f
    setTextAlign(Paint.Align.LEFT)
}

val stringText = "Android g 开发艺术探索"

val top = 200f
val baselineX = 0f


// == 画top线
paint.color = Color.BLUE
canvas.drawLine(baselineX, top, width.toFloat(), top, paint)

// == 计算baseline,并画出baseline
val fontMetrics = paint.fontMetrics
val baseLineY = top - fontMetrics.top

paint.color = Color.RED
canvas.drawLine(baselineX, baseLineY, width.toFloat(), baseLineY, paint)

// == 画文字
paint.color = Color.GREEN
canvas.drawText(stringText, baselineX, baseLineY, paint)
自定义控件绘图(drawText)篇三_第14张图片
给定左上角,并绘制文字

给定中间线位置绘图

即,将文字的垂直中心为中间线;如下图:

自定义控件绘图(drawText)篇三_第15张图片
来自源博客

如何根据center(中间线)来推导出baseline?

图中center线正是在top线和bottom线的正中间。
为了方便推导公式,原作者另外标了三个距离A,B,C;
很显然,距离A和距离C是相等的,都等于文字所在矩形高度以的一半,即:
A = C = (bottom - top)/2;
因为:
bottom = baseline + FontMetrics.bottom;
top = baseline+FontMetrics.top;
所以,将它们两个代入上面的公式,就可得到:
A = C = (FontMetrics.bottom - FontMetrics.top)/2;
而距离B,则表示Center线到baseline的距离;
很显然距离:
B = C - (bottom - baseline);
又因为:
FontMetrics.bottom = bottom-baseline;
C = A;
所以:
B = A - FontMetrics.bottom;
所以:
baseline = center + B = center + A - FontMetrics.bottom = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;

根据上面的推导过程,我们最终可知,当给定中间线center位置以后,baseline的位置为:

baseline = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;
val stringText = "Android g 开发艺术探索"

val center = 200f
val baselineX = 0f


// == 画center线
paint.color = Color.BLUE
canvas.drawLine(baselineX, center, width.toFloat(), center, paint)

// == 计算baseline,并画出baseline
val fontMetrics = paint.fontMetrics
val baseLineY = center + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom

paint.color = Color.RED
canvas.drawLine(baselineX, baseLineY, width.toFloat(), baseLineY, paint)

// == 画文字
paint.color = Color.BLACK
canvas.drawText(stringText, baselineX, baseLineY, paint)
自定义控件绘图(drawText)篇三_第16张图片
垂直居中效果

你可能感兴趣的:(自定义控件绘图(drawText)篇三)