本文部分圖文摘錄自這篇文章
自定義View的時(shí)候有時(shí)要在View里面繪制文字,就會(huì)調(diào)用Canvas的drawText系列方法,其中值得注意的就是drawText方法中的x和y這兩個(gè)參數(shù)。按照Android的習(xí)慣,一般都會(huì)認(rèn)為點(diǎn)(x,y)代表著文字所在的矩形的左上角的點(diǎn),但實(shí)際上并不是!先來看看官方文檔的解釋:
@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
也就是說:
參數(shù)x代表文字開始被繪制的源點(diǎn)的橫坐標(biāo),而文字從這個(gè)源點(diǎn)開始會(huì)如何被繪制則由Paint.Align這個(gè)枚舉類型來決定(文章后面有效果圖)。
Paint.Align有三個(gè)取值:
Paint.Align.LEFT 從源點(diǎn)開始向右繪制文字,即源點(diǎn)在整串文字的最左邊;
Paint.Align.CENTER 從源點(diǎn)開始向左右兩邊繪制文字,即源點(diǎn)在整串文字的中間;
Paint.Align.RIGHT 從源點(diǎn)開始向左繪制文字,即源點(diǎn)在整串文字的最右邊。
參數(shù)y代表文字的baseline的縱坐標(biāo),關(guān)于baseline的含義看下圖:

除了基線以外,如上圖所示,另外還有四條線,分別是ascent,descent,top,bottom,他們的意義分別是:
- ascent: 系統(tǒng)建議的,繪制單個(gè)字符時(shí),字符應(yīng)當(dāng)?shù)淖罡吒叨人诰€
- descent:系統(tǒng)建議的,繪制單個(gè)字符時(shí),字符應(yīng)當(dāng)?shù)淖畹透叨人诰€
- top: 可繪制的最高高度所在線
- bottom: 可繪制的最低高度所在線
一般來說,文字都會(huì)落在ascent和descent這兩條線之間,這也是系統(tǒng)建議的。一些特殊字符例外,但也不會(huì)超出top和bottom這兩條線。由這幾條線的值(可以由textPaint.getFontMetrics().bottom這樣的方式得到,下面會(huì)提到)就可以比較精準(zhǔn)的計(jì)算出文字區(qū)域的高度了。
所以,當(dāng)文字的size和typeface(這個(gè)一般不用管,默認(rèn)就行了)和繪制源點(diǎn)(x, y)都決定了之后,文字的位置也就決定了。
一般在繪制文字的時(shí)候,需求都是“在某個(gè)地方的中間繪制一串文字”,更通用的情況就是給定一個(gè)已知的點(diǎn)(originX, originY),將這個(gè)點(diǎn)作為將要繪制的字符串所在的矩形的四個(gè)頂點(diǎn)或中心繪制文字,這時(shí)就需要對文字的寬高進(jìn)行測量。
網(wǎng)上搜集了一下測量文字的寬高主要有以下方法
測量文字寬度
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方法,逐個(gè)計(jì)算出文字的寬,然后求和
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);
}
基本上方法二和方法三輸出的結(jié)果是一樣的,方法一的結(jié)果則不會(huì)大于方法二和方法三的結(jié)果。
測量文字的高度
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));
}
以上方法中提到的文字所在的最小矩形就是下圖的紅色區(qū)域,綠色區(qū)域就是由top、bottom和x(Align.LEFT)以及textPaint.measureText(str)畫出來的:

所以我一般是使用方法一來測量文字的寬高,畢竟是我們眼睛能看到的區(qū)域。
有了文字的寬高和已知的定點(diǎn)(x,y)就能比較準(zhǔn)確的在想要的位置繪制文字了。來試一下以指定點(diǎn)(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
*
* 以指定點(diǎn)(x,y)為中心繪制文字,方框?yàn)榇L制文字所在的最小矩形,則矩形的各頂點(diǎn)均可以通過簡單的邏輯換算得到。
* 下面分別以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三種方式繪制文字,
* 其繪制源點(diǎn)分別為矩形底邊的左右中三點(diǎn)
*/
//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);
}
}
運(yùn)行效果如下:

發(fā)現(xiàn)繪制的字母“g”在Align.LEFT時(shí)偏右了;而在Align.RIGHT時(shí)偏左了;只有Align.CENTER時(shí)是正常的,雖然“g”并沒有完全落到綠色的最小矩形里面,但是因?yàn)榫匦蔚牡走吺撬腷aseline所以這樣顯示是正常的,下圖有助于理解這一說法。

那為什么在Align.LEFT和Align.RIGHT時(shí)繪制文字會(huì)出現(xiàn)了一點(diǎn)偏移呢?原因我不太清楚,但是在查找原因的過程中我無意的將利用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
這些數(shù)字很奇怪,如果將這個(gè)矩形在屏幕上畫出來的話,會(huì)發(fā)現(xiàn)這個(gè)矩形的左下角非常接近(0,0),而且如果以(0,0)為源點(diǎn)繪制文字的話,則文字會(huì)完全落入到這個(gè)最小矩形里面。而textPaint的getTextBounds方法說明貌似也證實(shí)了這一點(diǎn):
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)為源點(diǎn)繪制文字的話,文字則會(huì)完全落入到這個(gè)最小矩形里面。
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
*
* 以指定點(diǎn)(x,y)為中心繪制文字,方框?yàn)榇L制文字所在的最小矩形,則矩形的各頂點(diǎn)均可以通過簡單的邏輯換算得到。
* 下面分別以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三種方式繪制文字,
* 其繪制源點(diǎn)分別為矩形底邊的左右中三點(diǎn)
*/
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時(shí)繪制文字會(huì)出現(xiàn)一點(diǎn)偏移這個(gè)問題,我做出了如下猜想:
當(dāng)textPaint的Align方式為LEFT或RIGHT時(shí),系統(tǒng)不會(huì)在x處就開始往右或往左繪制文字,而是會(huì)留出一定的空間再開始繪制,目的應(yīng)該就是當(dāng)x為0或者為屏幕寬度時(shí)繪制的文字不會(huì)緊貼著屏幕邊緣,那樣會(huì)顯得不美觀。
那這個(gè)空間到底是多大呢?就是minRect.left的值。所以想要文字在x處就開始繪制的話,當(dāng)Align為LEFT時(shí),x的值要減去minRect.left,而當(dāng)Align為RIGHT時(shí),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)為源點(diǎn)繪制文字的話,文字則會(huì)完全落入到這個(gè)最小矩形里面。
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
*
* 以指定點(diǎn)(x,y)為中心繪制文字,方框?yàn)榇L制文字所在的最小矩形,則矩形的各頂點(diǎn)均可以通過簡單的邏輯換算得到。
* 下面分別以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三種方式繪制文字,
* 其繪制源點(diǎn)分別為矩形底邊的左右中三點(diǎn)
*/
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);
}
}
修改后的效果:

這就正常了??赡苡腥藭?huì)發(fā)現(xiàn)在Paint.Align.RIGHT和Paint.Align.CENTER時(shí)其實(shí)文字還是有點(diǎn)偏左,沒錯(cuò),但是只要繪制另外的文字,比如說繪制字母“y”就完全沒問題了,猜想可能是字母“g”這個(gè)字本身的特點(diǎn),再者這點(diǎn)偏差也是在接受范圍內(nèi)的,特別是在沒有那綠色區(qū)域可比較的時(shí)候就更不明顯了。

然而事情并沒有結(jié)束,雖然說“g”的尾巴在baseline的下面是正常的,但是由于需求的原因,還是希望“g”能整體落在最小矩形里面,畢竟需求是“以指定點(diǎn)(x,y)為中心繪制文字”嘛,有了上面的經(jīng)驗(yàn),馬上就能想到了minRect.bottom,因?yàn)樵谝?0,0)為源點(diǎn)繪制“g”的時(shí)候,“g”在baseline以下的部分剛好出現(xiàn)在了屏幕里面。

這樣的話是不是只要將繪制的源點(diǎn)往上移動(dòng)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)為源點(diǎn)繪制文字的話,文字則會(huì)完全落入到這個(gè)最小矩形里面。
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
*
* 以指定點(diǎn)(x,y)為中心繪制文字,方框?yàn)榇L制文字所在的最小矩形,則矩形的各頂點(diǎn)均可以通過簡單的邏輯換算得到。
* 下面分別以Paint.Align.LEFT,Paint.Align.RIGHT和Paint.Align.CENTER三種方式繪制文字,
* 其繪制源點(diǎn)分別為矩形底邊的左右中三點(diǎn)
*/
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);
}
}
運(yùn)行一下:

Good!?。≡僭囋?yán)L制其它文字:

沒問題,OK,那么到此為止,“以指定點(diǎn)(x,y)為中心繪制文字”這個(gè)需求就算完成了。