概述:在日常开发者自定义绘图是很常见的一个技能,有时候我们需要自己实现一个不是特别复杂的视图。则需要通过Android提供的API进行计算绘制等。本篇的目的就是通过简单的例子对Android绘图进行简单的认识。注意,本例中没有对其做任何封装,知识简单示范。

开发中已经知道Android的Canvas绘图,drawText里的标准是以baseline为基准的。那么,如果使用需要绘制的区域竖直中点传递进绘制API作为参数,则绘制的效果会默认偏上。代码如下,部分初始化代码已经省略,文章末尾会给出完整项目代码。

1
2
3
4
5
6
7
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.MAGENTA);
canvas.drawRect(mRectF, mPaint); // 绘制第一个背景
mPaint.setColor(Color.GREEN);
canvas.drawText(testStr, mRectF.left, mRectF.centerY(), mPaint); // 绘制第一个文字
}

如图示

截图

由上图可以看出很明显文字并不是上下居中显示。那么在解决问题之前先来看看跟字体相关很重要的类FontMetrics。FontMetrics其实就与字体测量相关,它是Paint的一个内部类,里面有float类型的top、ascent、descent、bottom、leading五个成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Class that describes the various metrics for a font at a given text size.
* Remember, Y values increase going down, so those values will be positive,
* and values that measure distances going up will be negative. This class
* is returned by getFontMetrics().
*/
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in
* the font at a given text size.
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
*/
public float leading;
}

下面一张图很好的说明了其中几个成员参数。在Android中文字的绘制都是从baseline开始的,baseline往上至字符最高处的距离称之为ascent(上坡度),baseline往下至字符最底处的距离称之为descent(下坡度),而leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离。

截图

为了更直观的展示出来,可以分别绘制出线条来表示出这几个参数。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Override
protected void onDraw(Canvas canvas) {
int x = 0, y = 480; // baseline
float textWidth = mPaint.measureText(testStr);
mPaint.setColor(Color.CYAN);
mRectF = new RectF(0, 300, 1000, 550);
canvas.drawRect(mRectF, mPaint); // 绘制第二个背景
mPaint.setColor(Color.BLACK);
canvas.drawText(testStr, x, y, mPaint); // 绘制第二个文字, 基于baseline
FontMetrics fontMetrics = mPaint.getFontMetrics();
Rect bounds1 = new Rect();
mPaint.getTextBounds("我", 0, 1, bounds1);
Rect bounds2 = new Rect();
mPaint.getTextBounds("我是Jinl!n,g", 0, 10, bounds2);
mPaint.setStrokeWidth(1);
// baseline
mPaint.setColor(Color.RED);
canvas.drawLine(x, y, textWidth, y, mPaint);
mPaint.setTextSize(32);
canvas.drawText("baseline", textWidth, y, mPaint);
// bounds1
canvas.save();
canvas.translate(x, y);
mPaint.setStyle(Style.STROKE);
mPaint.setColor(Color.GREEN);
canvas.drawRect(bounds1, mPaint);
canvas.restore();
// bounds2
canvas.save();
canvas.translate(x, y);
mPaint.setColor(Color.MAGENTA);
canvas.drawRect(bounds2, mPaint);
canvas.restore();
// ascent
mPaint.setStyle(Style.FILL);
mPaint.setColor(Color.YELLOW);
canvas.drawLine(x, y + fontMetrics.ascent, textWidth, y + fontMetrics.ascent, mPaint);
canvas.drawText("ascent", textWidth, y + fontMetrics.ascent + 10, mPaint);
// descent
mPaint.setColor(Color.BLUE);
canvas.drawLine(x, y + fontMetrics.descent, textWidth, y + fontMetrics.descent, mPaint);
canvas.drawText("descent", textWidth, y + fontMetrics.descent, mPaint);
// top
mPaint.setColor(Color.DKGRAY);
canvas.drawLine(x, y + fontMetrics.top, textWidth, y + fontMetrics.top, mPaint);
canvas.drawText("top", textWidth, y + fontMetrics.top, mPaint);
// bottom
mPaint.setColor(Color.GREEN);
canvas.drawLine(x, y + fontMetrics.bottom, textWidth, y + fontMetrics.bottom, mPaint);
canvas.drawText("bottom", textWidth, y + fontMetrics.bottom + 20, mPaint);
// center
mPaint.setColor(Color.GRAY);
canvas.drawLine(x, mRectF.centerY(), textWidth, mRectF.centerY(), mPaint);
canvas.drawText("center", textWidth, mRectF.centerY() + 20, mPaint);
}

上述代码的呈现效果为

截图

可以看到,红色是baseline,最上面深灰色是top,最下面绿色是bottom,蓝色的descent,descent和bottom非常接近,黄色的是ascent,以及绘制区域的中线center。由图示可以看出字体整体是基于descent和ascent居中的,那问题的解决方就很明显了。而使用paint.getTextBounds方法获取的仅仅是字符边界,并不是字体的边界这一点一定要注意区分开。
回到问题上来,需要将文本绘制到目标区域,那么baseline的计算公式就是
targetRect.centerY() - (FontMetrics.descent - FontMetrics.ascent) / 2 - FontMetrics.ascent
合并整理之后得到
mRectF.centerY() - fontMetrics.ascent / 2 - fontMetrics.descent / 2;
此时,问题就得到解决。尝试一下,效果说话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onDraw(Canvas canvas) {
mRectF = new RectF(0, 600, 1000, 850);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(mRectF, mPaint); // 绘制第三个背景
mPaint.setColor(Color.RED);
canvas.drawLine(x, mRectF.centerY(), textWidth, mRectF.centerY(), mPaint);
canvas.drawText("center", textWidth, mRectF.centerY(), mPaint);
mPaint.setTextSize(80);
fontMetrics = mPaint.getFontMetrics();
float baseline = mRectF.centerY() - fontMetrics.ascent / 2 - fontMetrics.descent / 2;
canvas.drawText(testStr, mRectF.centerX() - textWidth / 2, baseline, mPaint);
}

截图

到这里关于字体绘制的一些注意细节就差不多了。最后,看看合并之后的对比效果
截图
完整项目Demo代码已上传至GitHub中