本文部分图文摘录自这篇文章
自定义View的时候有时要在View里面绘制文字,就会调用Canvas的drawText系列方法,其中值得注意的就是drawText方法中的x和y这两个参数。按照Android的习惯,一般都会认为点(x,y)代表着文字所在的矩形的左上角的点,但实际上并不是!先来看看官方文档的解释:
@param x The x-coordinate of the origin of the text being drawn.The origin is interpreted based on the Align setting in the paint
@param y The y-coordinate of the baseline of the text being drawn
也就是说:
参数x代表文字开始被绘制的源点的横坐标,而文字从这个源点开始会如何被绘制则由Paint.Align这个枚举类型来决定(文章后面有效果图)。
Paint.Align有三个取值:
Paint.Align.LEFT 从源点开始向右绘制文字,即源点在整串文字的最左边;
Paint.Align.CENTER 从源点开始向左右两边绘制文字,即源点在整串文字的中间;
Paint.Align.RIGHT 从源点开始向左绘制文字,即源点在整串文字的最右边。
参数y代表文字的baseline的纵坐标,关于baseline的含义看下图:
除了基线以外,如上图所示,另外还有四条线,分别是ascent,descent,top,bottom,他们的意义分别是:
- ascent: 系统建议的,绘制单个字符时,字符应当的最高高度所在线
- descent:系统建议的,绘制单个字符时,字符应当的最低高度所在线
- top: 可绘制的最高高度所在线
- bottom: 可绘制的最低高度所在线
一般来说,文字都会落在ascent和descent这两条线之间,这也是系统建议的。一些特殊字符例外,但也不会超出top和bottom这两条线。由这几条线的值(可以由textPaint.getFontMetrics().bottom这样的方式得到,下面会提到)就可以比较精准的计算出文字区域的高度了。
所以,当文字的size和typeface(这个一般不用管,默认就行了)和绘制源点(x, y)都决定了之后,文字的位置也就决定了。
一般在绘制文字的时候,需求都是“在某个地方的中间绘制一串文字”,更通用的情况就是给定一个已知的点(originX, originY),将这个点作为将要绘制的字符串所在的矩形的四个顶点或中心绘制文字,这时就需要对文字的宽高进行测量。
网上搜集了一下测量文字的宽高主要有以下方法
测量文字宽度
private void measureTextWidth() {
TextPaint textPaint = new TextPaint();
textPaint.setTextSize(10);
String str = ",";
//方法一:利用textPaint的getTextBounds方法,可以得到文字所在的最小矩形的宽高
Rect rect = new Rect();
textPaint.getTextBounds(str, 0, str.length(), rect);
Log.i("tag", "text's width by getTextBounds is: " + rect.width());
//方法二:利用textPaint的measureText方法
Log.i("tag", "text's width by measureText is: " + textPaint.measureText(str));
//方法三:利用textPaint的getTextWidths方法,逐个计算出文字的宽,然后求和
float[] widths = new float[str.length()];
textPaint.getTextWidths(str, widths);
float totalWidth = 0;
for (float width : widths) {
totalWidth += width;
}
Log.i("tag", "text's width by getTextWidths is: " + totalWidth);
}
基本上方法二和方法三输出的结果是一样的,方法一的结果则不会大于方法二和方法三的结果。
测量文字的高度
private void measureTextHeight() {
TextPaint textPaint = new TextPaint();
textPaint.setTextSize(10);
String str = ",";
//方法一:利用textPaint的getTextBounds方法,可以得到文字所在的最小矩形的宽高
Rect rect = new Rect();
textPaint.getTextBounds(str, 0, str.length(), rect);
Log.i("tag", "text's height by getTextBounds is: " + rect.height());
//方法二:利用Paint.FontMetrics。
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
Log.i("tag", "text's height by getFontMetrics is: " + (fontMetrics.descent - fontMetrics.ascent));
Log.i("tag", "text's height by getFontMetrics is: " + (fontMetrics.bottom - fontMetrics.top));
}
以上方法中提到的文字所在的最小矩形就是下图的红色区域,绿色区域就是由top、bottom和x(Align.LEFT)以及textPaint.measureText(str)画出来的:
所以我一般是使用方法一来测量文字的宽高,毕竟是我们眼睛能看到的区域。
有了文字的宽高和已知的定点(x,y)就能比较准确的在想要的位置绘制文字了。来试一下以指定点(x,y)为中心绘制文字,代码如下:
public class DrawTextView extends View{
public DrawTextView(Context context) {
super(context);
init();
}
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private TextPaint textPaint;
private String text = "g";
private Rect minRect;
private int textWidth;
private int textHeight;
private Paint rectPaint;
private RectF rectF;
private Paint pointPaint;
private void init() {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(200);
textPaint.setTypeface(Typeface.DEFAULT);
textPaint.setColor(Color.RED);
minRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), minRect);
textWidth = minRect.width();
textHeight = minRect.height();
pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pointPaint.setStrokeWidth(10);
pointPaint.setColor(Color.BLACK);
rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setColor(Color.GREEN);
rectF = new RectF();
}
private int originX;
private int originY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
originX = getMeasuredWidth() / 2;
originY = getMeasuredHeight() / 2;
/**
* _ _ _ _ _
* | |
* | (x,y) |
* |----· |height
* | | |
* | | |
* ·- - · - -·
* width
*
* 以指定点(x,y)为中心绘制文字,方框为待绘制文字所在的最小矩形,则矩形的各顶点均可以通过简单的逻辑换算得到。
* 下面分别以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三种方式绘制文字,
* 其绘制源点分别为矩形底边的左右中三点
*/
//Paint.Align.LEFT
textPaint.setTextAlign(Paint.Align.LEFT);
originY = 200;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX - textWidth / 2, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.RIGHT
textPaint.setTextAlign(Paint.Align.RIGHT);
originY = 500;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX + textWidth / 2, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.CENTER
textPaint.setTextAlign(Paint.Align.CENTER);
originY = 800;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
}
}
运行效果如下:
发现绘制的字母“g”在Align.LEFT时偏右了;而在Align.RIGHT时偏左了;只有Align.CENTER时是正常的,虽然“g”并没有完全落到绿色的最小矩形里面,但是因为矩形的底边是它的baseline所以这样显示是正常的,下图有助于理解这一说法。
那为什么在Align.LEFT和Align.RIGHT时绘制文字会出现了一点偏移呢?原因我不太清楚,但是在查找原因的过程中我无意的将利用textPaint获得的文字所在的最小矩形minRect的位置信息的值打印了出来:
Log.i("tag",minRect.left + "");
Log.i("tag",minRect.top + "");
Log.i("tag",minRect.right + "");
Log.i("tag",minRect.bottom + "");
输出为:
11-09 22:08:17.047 22634-22634/com.eichinn.androidheroes I/tag: 9
11-09 22:08:17.047 22634-22634/com.eichinn.androidheroes I/tag: -108
11-09 22:08:17.047 22634-22634/com.eichinn.androidheroes I/tag: 99
11-09 22:08:17.047 22634-22634/com.eichinn.androidheroes I/tag: 42
这些数字很奇怪,如果将这个矩形在屏幕上画出来的话,会发现这个矩形的左下角非常接近(0,0),而且如果以(0,0)为源点绘制文字的话,则文字会完全落入到这个最小矩形里面。而textPaint的getTextBounds方法说明貌似也证实了这一点:
Return in bounds (allocated by the caller) the smallest rectangle that encloses all of the characters, with an implied origin at (0,0).
画出来看看:
public class DrawTextView extends View{
public DrawTextView(Context context) {
super(context);
init();
}
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private TextPaint textPaint;
private String text = "g";
private Rect minRect;
private int textWidth;
private int textHeight;
private Paint rectPaint;
private RectF rectF;
private Paint pointPaint;
private void init() {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(200);
textPaint.setTypeface(Typeface.DEFAULT);
textPaint.setColor(Color.RED);
minRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), minRect);
textWidth = minRect.width();
textHeight = minRect.height();
Log.i("tag", minRect.left + "");
Log.i("tag", minRect.top + "");
Log.i("tag", minRect.right + "");
Log.i("tag", minRect.bottom + "");
pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pointPaint.setStrokeWidth(10);
pointPaint.setColor(Color.BLACK);
rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setColor(Color.GREEN);
rectF = new RectF();
}
private int originX;
private int originY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//以(0,0)为源点绘制文字的话,文字则会完全落入到这个最小矩形里面。
textPaint.setTextAlign(Paint.Align.LEFT);
originX = 0;
originY = 0;
canvas.drawRect(minRect, rectPaint);
canvas.drawText(text, originX, originY, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
/**
* _ _ _ _ _
* | |
* | (x,y) |
* |----· |height
* | | |
* | | |
* ·- - · - -·
* width
*
* 以指定点(x,y)为中心绘制文字,方框为待绘制文字所在的最小矩形,则矩形的各顶点均可以通过简单的逻辑换算得到。
* 下面分别以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三种方式绘制文字,
* 其绘制源点分别为矩形底边的左右中三点
*/
originX = getMeasuredWidth() / 2;
//Paint.Align.LEFT
textPaint.setTextAlign(Paint.Align.LEFT);
originY = 200;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX - textWidth / 2, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.RIGHT
textPaint.setTextAlign(Paint.Align.RIGHT);
originY = 500;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX + textWidth / 2, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.CENTER
textPaint.setTextAlign(Paint.Align.CENTER);
originY = 800;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
}
}
效果图:
于是,对于在Align.LEFT和Align.RIGHT时绘制文字会出现一点偏移这个问题,我做出了如下猜想:
当textPaint的Align方式为LEFT或RIGHT时,系统不会在x处就开始往右或往左绘制文字,而是会留出一定的空间再开始绘制,目的应该就是当x为0或者为屏幕宽度时绘制的文字不会紧贴着屏幕边缘,那样会显得不美观。
那这个空间到底是多大呢?就是minRect.left的值。所以想要文字在x处就开始绘制的话,当Align为LEFT时,x的值要减去minRect.left,而当Align为RIGHT时,x的值要加上minRect.left。
修改的程序:
public class DrawTextView extends View{
public DrawTextView(Context context) {
super(context);
init();
}
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private TextPaint textPaint;
private String text = "g";
private Rect minRect;
private int textWidth;
private int textHeight;
private Paint rectPaint;
private RectF rectF;
private Paint pointPaint;
private void init() {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(200);
textPaint.setTypeface(Typeface.DEFAULT);
textPaint.setColor(Color.RED);
minRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), minRect);
textWidth = minRect.width();
textHeight = minRect.height();
Log.i("tag", minRect.left + "");
Log.i("tag", minRect.top + "");
Log.i("tag", minRect.right + "");
Log.i("tag", minRect.bottom + "");
pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pointPaint.setStrokeWidth(10);
pointPaint.setColor(Color.BLACK);
rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setColor(Color.GREEN);
rectF = new RectF();
}
private int originX;
private int originY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//以(0,0)为源点绘制文字的话,文字则会完全落入到这个最小矩形里面。
textPaint.setTextAlign(Paint.Align.LEFT);
originX = 0;
originY = 0;
canvas.drawRect(minRect, rectPaint);
canvas.drawText(text, originX, originY, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
/**
* _ _ _ _ _
* | |
* | (x,y) |
* |----· |height
* | | |
* | | |
* ·- - · - -·
* width
*
* 以指定点(x,y)为中心绘制文字,方框为待绘制文字所在的最小矩形,则矩形的各顶点均可以通过简单的逻辑换算得到。
* 下面分别以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三种方式绘制文字,
* 其绘制源点分别为矩形底边的左右中三点
*/
originX = getMeasuredWidth() / 2;
//Paint.Align.LEFT
textPaint.setTextAlign(Paint.Align.LEFT);
originY = 200;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX - textWidth / 2 - minRect.left, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.RIGHT
textPaint.setTextAlign(Paint.Align.RIGHT);
originY = 500;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX + textWidth / 2 + minRect.left, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.CENTER
textPaint.setTextAlign(Paint.Align.CENTER);
originY = 800;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX, originY + textHeight / 2, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
}
}
修改后的效果:
这就正常了。可能有人会发现在Paint.Align.RIGHT和Paint.Align.CENTER时其实文字还是有点偏左,没错,但是只要绘制另外的文字,比如说绘制字母“y”就完全没问题了,猜想可能是字母“g”这个字本身的特点,再者这点偏差也是在接受范围内的,特别是在没有那绿色区域可比较的时候就更不明显了。
然而事情并没有结束,虽然说“g”的尾巴在baseline的下面是正常的,但是由于需求的原因,还是希望“g”能整体落在最小矩形里面,毕竟需求是“以指定点(x,y)为中心绘制文字”嘛,有了上面的经验,马上就能想到了minRect.bottom,因为在以(0,0)为源点绘制“g”的时候,“g”在baseline以下的部分刚好出现在了屏幕里面。
这样的话是不是只要将绘制的源点往上移动minRect.bottom,即y - minRect.bottom就行了呢?试一试:
public class DrawTextView extends View{
public DrawTextView(Context context) {
super(context);
init();
}
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private TextPaint textPaint;
private String text = "g";
private Rect minRect;
private int textWidth;
private int textHeight;
private Paint rectPaint;
private RectF rectF;
private Paint pointPaint;
private void init() {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(200);
textPaint.setTypeface(Typeface.DEFAULT);
textPaint.setColor(Color.RED);
minRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), minRect);
textWidth = minRect.width();
textHeight = minRect.height();
Log.i("tag", minRect.left + "");
Log.i("tag", minRect.top + "");
Log.i("tag", minRect.right + "");
Log.i("tag", minRect.bottom + "");
pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pointPaint.setStrokeWidth(10);
pointPaint.setColor(Color.BLACK);
rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setColor(Color.GREEN);
rectF = new RectF();
}
private int originX;
private int originY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//以(0,0)为源点绘制文字的话,文字则会完全落入到这个最小矩形里面。
textPaint.setTextAlign(Paint.Align.LEFT);
originX = 0;
originY = 0;
canvas.drawRect(minRect, rectPaint);
canvas.drawText(text, originX, originY, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
/**
* _ _ _ _ _
* | |
* | (x,y) |
* |----· |height
* | | |
* | | |
* ·- - · - -·
* width
*
* 以指定点(x,y)为中心绘制文字,方框为待绘制文字所在的最小矩形,则矩形的各顶点均可以通过简单的逻辑换算得到。
* 下面分别以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三种方式绘制文字,
* 其绘制源点分别为矩形底边的左右中三点
*/
originX = getMeasuredWidth() / 2;
//Paint.Align.LEFT
textPaint.setTextAlign(Paint.Align.LEFT);
originY = 200;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX - textWidth / 2 - minRect.left, originY + textHeight / 2 - minRect.bottom, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.RIGHT
textPaint.setTextAlign(Paint.Align.RIGHT);
originY = 500;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX + textWidth / 2 + minRect.left, originY + textHeight / 2 - minRect.bottom, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
//Paint.Align.CENTER
textPaint.setTextAlign(Paint.Align.CENTER);
originY = 800;
rectF.set(originX - textWidth / 2, originY - textHeight / 2, originX + textWidth / 2, originY + textHeight / 2);
canvas.drawRect(rectF, rectPaint);
canvas.drawText(text, originX, originY + textHeight / 2 - minRect.bottom, textPaint);
canvas.drawPoint(originX, originY, pointPaint);
}
}
运行一下:
Good!!!再试试绘制其它文字:
没问题,OK,那么到此为止,“以指定点(x,y)为中心绘制文字”这个需求就算完成了。