声明:
发现了一篇自定义View的博客索引,有比较系统的自定义View技术点。欣喜之下准备学习一下,把这些技术点学会,消化,变成自己的东西。做些笔记,或有copy,原文更好。看原文,请转到:启舰的博客—Android自定义控件三部曲文章索引。
一、概述
1. 四线格与基线
在阅读启舰的博客: 自定义控件之绘图篇( 五):drawText()详解后,终于知道,原来canvas在绘制文字时,是有规则的,这个规则就是–基线。这也正是我之前疑惑之一, 为什么总画不好文字,感觉代码没错呀,文字位置为什么不对。看了此博客,终于解惑,感谢启舰的博客^_^。
记得初学字母时,要用到四线格作业本, 我们将字母按格式写到四线格内,见下图:
canvas在使用drawText绘制文字时是有标准的,它是以基线为基准的。
由图可以看出,基线就相当于四线格的第三条线。基线位置确定,文字位置也就确定了。
2. canvas.drawText()
- 1> canvas.drawText()和基线
下面是canvas的drawText()方法:
/**
* 使用指定的画笔从起点(x,y)开始绘制文字。 根据画笔对齐设置确定起点位置。
* @param text 要绘制的文字
* @param x 文字绘制起点x坐标
* @param y 文字绘制起点y坐标
* @param paint 用于绘制文字的画笔(e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint){
...
}
传入一个字符串text,设置绘制原点的x、y坐标和一个画笔paint
。就可以绘制文字了。
但是这个原点坐标(x, y)的位置到底在哪里呢?之前以为是在左上角,但实际不是。其实原点(x, y)跟基线和对齐设置(paint.setTextAlign(align))有关。对齐设置后面再提。如下图,绘制文字“changes”时,起点就在基线上的绿点位置。
坐标(x, y)难搞的是y坐标,绘制图形时,原点(x, y)一般代表的是图形的左上角(left, top),但在绘制文字时,是个例外。y代表的是基线位置,x值确定以后,文字具体怎么显示还跟对齐设置有关。
- 2> 代码
自定义一个DrawTextView
,继承View
,重写onDraw()方法,绘制文字和基线。
public class DrawTextView extends View {
private Paint mPaint;
public DrawTextView(Context context) {
super(context);
initPaint();
}
public DrawTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.MAGENTA);
mPaint.setStrokeWidth(2);
}
@Override
protected void onDraw(Canvas canvas) {
drawTextBase(canvas);
//drawText_textAlign(canvas, 300, 200);
//drawText_4Lines(canvas);
//drawText_textBounds(canvas);
//drawText_leftTop(canvas);
//drawText_centerLine(canvas);
}
private void drawTextBase(Canvas canvas) {
int baseX = 120;
int baseLineY = 200;
//写文字
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(120f);
canvas.drawText("changes", baseX, baseLineY, mPaint);
//画基线
mPaint.setColor(Color.RED);
canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);
//绘制原点
mPaint.setColor(Color.GREEN);
mPaint.setStrokeWidth(8f);
canvas.drawPoint(baseX, baseLineY, mPaint);
}
...
}
这里定义绘制文字的原点为(120, 200)。然后设置文字颜色和大小绘制文字,接着画基线和原点。
其中基线是一条从(0, 200)到(3000, 200)的水平线。
然后在布局文件中引用。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_draw_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.wzhy.customviewdemos.customviews.drawtext.DrawTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
效果跟上面一样:
结论:
- drawText(text, x, y, paint)t的y就是基线位置。
- 只有原点(x, y)、文字大小、对齐方式确定后文字位置才真正确定。
3. 设置文字对齐:paint.setTextAlign(align)
- 1> 文字对齐和原点
上面我们知道了我们确定了drawText(text, x, y, paint)的y就是基线位置。但是,文字绘制的原点(x, y)、文字大小确定后,并不能完全确定文字位置,还要看文字对齐。
我们在drawText(text, x, y, paint)中传入了原点(x, y),y代表基线位置,x就应该是文字绘制的起始了吧?但事实并不是想象的样子。x所代表的其实是水平方向上的一个参考点,文字可以以x为左边缘、右边缘以及中间位置。也就是文字相对于参考点x,有一个相对位置,这个相对位置就是文字对齐。下面是Paint类的设置文字对齐的方法:
/**
* 设置Paint的文字对齐。这个方法控制文字相对于起点的位置。左对齐意味着所有的文字
* 将会被绘制在原点的右边(原点指定文字的左边缘),以此类推。
*
* @param align 设置Paint绘制文字的对齐参数值。align可以是:
* Paint.Align.LEFT 左对齐,文字以原点为左边缘,在原点右边
* Paint.Align.CENTER 居中对齐,文字以原点为中间位置
* Paint.Align.RIGHT 右对齐,文字以原点为右边缘,再远点左边
*/
public void setTextAlign(Align align) {
nSetTextAlign(mNativePaint, align.nativeInt);
}
- 2> 代码和效果
下面,我们以(300, 200)为原点,绘制文字“Align”,但我们设置不同的文字对齐,看看有什么效果。代码:
private void drawText_textAlign(Canvas canvas, int baseX, int baseY) {
//文字大小和对齐
mPaint.setTextSize(120);
mPaint.setTextAlign(Paint.Align.LEFT);
//mPaint.setTextAlign(Paint.Align.CENTER);
//mPaint.setTextAlign(Paint.Align.RIGHT);
//绘制文字
mPaint.setColor(Color.BLACK);
canvas.drawText("Align", baseX, baseY, mPaint);
//画基线
mPaint.setColor(Color.RED);
canvas.drawLine(0, baseY, 3000, baseY, mPaint);
//画起始线
canvas.drawLine(baseX, 0, baseX, baseY + 60, mPaint);
//绘制原点
mPaint.setColor(Color.GREEN);
mPaint.setStrokeWidth(8f);
canvas.drawPoint(baseX, baseY, mPaint);
mPaint.setStrokeWidth(2f);
}
在onDraw()方法中调用drawText_textAlign(canvas, 300, 200),效果如下:
mPaint.setTextAlign(Paint.Align.LEFT)
mPaint.setTextAlign(Paint.Align.CENTER)
mPaint.setTextAlign(Paint.Align.RIGHT)
从效果图看出,原点(x, y)的x坐标,表示文字的相对位置。其实设置文字的对齐,就是相对于原点的对齐:
LEFT,表示文字左边对齐于原点;
CENTER,表示文字中间对齐于原点;
RIGNT,表示文字右边对齐于原点。
这样文字的位置就确定了。同时我们也知道了,确定了原点坐标和对齐方式,文字的位置就确定了。
二、绘制文字的四线格和FontMetrics
1. 绘制文字的四线格
上面我们知道文字的基线就是绘制文字原点的y坐标。其实系统绘制文字时还有其他几条线,见下图:
-图2.1.1-
由图,位置文字时除了基线外,还有四条线(topLine,ascentLine,descentLine,bottomLine),它们意义分别是:
-1> topLine–可绘制最高线
-2> ascentLine–建议绘制单行字符最高线
-3> descentLine–建议绘制单行字符最低线
-4> bottomLine–可绘制最低线
从图中我们还可以看到,ascentLine距离文字顶部的距离大于descentLine距离文字底部的距离,多出来的部分是做什么用呢?看下面的图片:
原来是不同国家文字不同,需要空出空间放置注音等符号。
2. FontMetrics
- 1> FontMetrics源码及概述
FontMetric是Paint类的一个静态内部类,可由Paint的getFontMetrics()获得。下面是它的源码:
/**
* 此类描述了给定文字大小字体的尺寸变量。记住Y值向下为正,向上为负。
* 测量值相对于基线,在基线下的为正值,在基线上的为负值。此类是Paint类
* 的一个静态内部类。保存几个测量值。由Paint的getFontmetrics()返回。
*/
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in the font at a given text size.
* 距离baseline之上最大的距离。
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
* 在单行文字里距离baseline之上推荐的距离。
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
* 在单行文字里距离baseline之下推荐的距离。
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
* 距离baseline之下最大的距离。
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
* 行距:在两行文字之间推荐的额外空间。
*/
public float leading;
}
从上面我们知道了原点的y坐标,表示基线位置。那么其他4条线位置怎么确定,FontMetrics的属性top、ascent、descent、bottom与4条线之间的关系是什么?我们接着看。
除了leading,FontMetrics还有四个值:top,ascent,descent,bottom。
由源码可知,它们分别表示对应线距基线的距离。
由源码和图可知FontMetrics的四个值分别是:
fontMetrics.top = topLineY - baselineY;
fontMetrics.ascent = ascentLineY - baselineY;
fontMetrics.descent = descentLineY - baselineY;
fontMetrics.bottom = bottomLineY - baselineY;
注意:Y值向下为正。这四个值都是以baseline为基准的,那么top和ascent就会为负值。
由上面可得四条线的位置:
topLineY = fontMetrics.top + baselineY;
ascentLineY = fontMetrics.ascent + baselineY;
descentLineY - fontMetrics.descent + baselineY;
bottomLineY = fontMetrics.bottom + baselineY;
而FontMetrics的leading表示的是行距,见下图:
· 获得FontMetrics
//获得FontMetrics
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
//或
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
- 2> 代码和效果
上面我们计算通过基线和FontMetrics获得了四条线的位置,我们就可以绘制出这四条线。我们在文字对齐代码基础上绘制这四条线,代码如下:
private void drawText_4Lines(Canvas canvas) {
int baseX = 60;
int baseLineY = 300;
drawText_textAlign(canvas, baseX, baseLineY);
//计算四条线的位置
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float ascentY = fontMetrics.ascent + baseLineY;
float descentY = fontMetrics.descent + baseLineY;
float topY = fontMetrics.top + baseLineY;
float bottomY = fontMetrics.bottom + baseLineY;
//画top
mPaint.setColor(Color.BLUE);
canvas.drawLine(0, topY, 3000, topY, mPaint);
//画ascent
mPaint.setColor(Color.GREEN);
canvas.drawLine(0, ascentY, 3000, ascentY, mPaint);
//画descent
mPaint.setColor(Color.MAGENTA);
canvas.drawLine(0, descentY, 3000, descentY, mPaint);
//画bottom
mPaint.setColor(Color.CYAN);
canvas.drawLine(0, bottomY, 3000, bottomY, mPaint);
}
原点坐标为(60, 300),文字对齐为左对齐,根据FontMetrics和baseLineY计算出四条线位置,然后画出四条线。
效果跟上面一样:
三、所绘文字的宽度、高度和最小矩形的获取
这部分,将了解如何获取文字所占区域的高度、宽度和仅包裹文字的最小矩形。
图中绿色矩形是文字所占区域,蓝色区域是仅包裹文字的最小矩形。
1. 文字的宽度和高度
- 1> 文字高度
文字高度容易获取,直接用bottomLineY - topLineY即可。
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int bottomY = fontMetricsInt.bottom + baseLineY;
int topY = fontMetricsInt.top + baseLineY;
//所占高度
int height = bottomY - topY;
- 2> 文字宽度
文字的宽度要用到Paint类的一个方法:measureText(String text)
mPaint.setTextSize(120f);
int width = (int) mPaint.measureText(text);
注意:使用前,一定要设置好文字的大小(如,mPaint.setTextSize(160f)),不然无法测量文字的宽度。
- 3> 最小矩形
要得到绘制文字的最小矩形,同样要用到Paint类的一个方法:
/**
* 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线
* @param text 要测量最小矩形的字符串
* @param start 要测量起始字符在字符串中的索引
* @param end 所要测量的字符的长度
* @param bounds 接收测量结果
*/
public void getTextBounds(String text, int start, int end, Rect bounds){...}
使用起来很简单:
mPaint.setTextSize(120f);
/*最小矩形*/
Rect minRect = new Rect();
mPaint.getTextBounds(text, 0, text.length(), minRect);
Log.i("Rect", "minRect: " + minRect.toShortString());
使用之前同样需要设置文字大小。
Log打印结果为:
I/Rect: minRect: [2,-144][570,34]
可以看到,这个矩形左上角位置为(2, -144),右下角坐标为(570, 34)。
有点疑惑,左上角的Y坐标为什么是负数?我们在代码中并没有给getTextBounds()设置原点,那么它就是以点(0, 0)为原点(y = 0作为基线),来绘制的矩形。所以跟绘制文字一样,这个最小矩形是以(0, 0)为原点来绘制的(同样以y坐标为基线)。
而我们上面绘制文字时传入了原点,这个最小矩形就会与实际文字位置错开,所以这个矩形需要考虑加上绘制文字时的原点。
minRect.left += baseX;
minRect.top += baseLineY;
minRect.bottom += baseLineY;
minRect.right += baseX;
经过相加才是正确的最小矩形位置。因为文字以(baseX, baseLineY)为原点绘制,而最小矩形的测量基准为(0, 0)。相当于平移了最小矩形。
- 2> 代码和效果
上面我们知道了怎样获得文字宽度、高度以及最小矩形,我们就可以绘制出文字所站区域和最小矩形。代码如下:
private void drawText_textBounds(Canvas canvas) {
//定义原点
int baseX = 300;
int baseLineY = 300;
//定义要绘制的文字
String text = "AgeÂǎЙ";
//设置文字的大小和对齐方式
mPaint.setTextSize(160f);
mPaint.setTextAlign(Paint.Align.LEFT);
/*字符串所占的高度和宽度*/
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int bottomY = fontMetricsInt.bottom + baseLineY;
int topY = fontMetricsInt.top + baseLineY;
//所占高度 int height = bottomY - topY;
//宽度
int width = (int) mPaint.measureText(text);
//绘制所占区域
Rect rect = new Rect(baseX, topY, baseX + width, bottomY);
mPaint.setColor(Color.GREEN);
canvas.drawRect(rect, mPaint);
/*最小矩形*/
Rect minRect = new Rect();
mPaint.getTextBounds(text, 0, text.length(), minRect);
Log.i("Rect", "minRect: " + minRect.toShortString());
minRect.left += baseX;
minRect.top += baseLineY;
minRect.bottom += baseLineY;
minRect.right += baseX;
mPaint.setColor(Color.BLUE);
canvas.drawRect(minRect, mPaint);
//绘制文字
mPaint.setColor(Color.BLACK);
canvas.drawText(text, baseX, baseLineY, mPaint);
//画基线
mPaint.setColor(Color.RED);
canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);
//画起始线
canvas.drawLine(baseX, 0, baseX, baseLineY + 60, mPaint);
//绘制原点
mPaint.setColor(Color.GREEN);
mPaint.setStrokeWidth(8f);
canvas.drawPoint(baseX, baseLineY, mPaint);
mPaint.setStrokeWidth(2f);
}
效果如下:
四、定点写字
我们在实际绘制文字时,一般不会直接得到原点来绘制文字。较为常见的是给出左上角或水平中线,或被约束在一个宽高中居中显示。
而我们绘制文字时就是根据原点和文字对齐来定位文字位置的。所以我们需要计算出原点位置(或基线位置)。
1. 给定左上顶点绘图
如果给出左上顶点(left, top),我们需要计算出基线的位置。
上面我们知道:
topLineY = fontMetrics.top + baselineY
=>baselineY = topLineY - fontMetrics.top
我们就得到了baseline位置,也就是原点的y坐标;而left就是原点的x坐标。
这里的top就是topLine的y坐标topLineY。
那么原点坐标就是:
(left, top - fontMetrics.top)
先看效果图:
再看代码:
给定了左上定点(60, 60),得到FontMetrics,计算得到基线位置,最后绘制文字。
private void drawText_leftTop(Canvas canvas) {
String text = "AngelÂ";
int topX = 60;
int topY = 60;
//设置paint
mPaint.setTextSize(200);//单位:px
mPaint.setTextAlign(Paint.Align.LEFT);
//画左上顶点
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(8f);
canvas.drawPoint(topX, topY, mPaint);
mPaint.setStrokeWidth(2f);
//画top线
mPaint.setColor(Color.RED);
canvas.drawLine(0, topY, 3000, topY, mPaint);
//找到基线位置
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int baseLineY = topY - fontMetricsInt.top;
//画基线
mPaint.setColor(Color.GREEN);
canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);
/*写文字*/
mPaint.setColor(Color.BLACK);
canvas.drawText(text, topX, baseLineY, mPaint);
}
2. 给定中线位置绘制文字
给定中线位置centerLineY,我们同样需要通过它计算出基线位置baselineY才能绘制文字。
下面是计算步骤,根据坐标运算:
①centerLine作为ascentLine和descentLine的中间线
centerLineY = (ascentLineY + descentLineY)/2
<=> centerLineY = (ascent + baselineY + descent + baselineY)/2
<=> centerLineY = baselineY + (ascent + descent)/2
<=>baselineY = centerLineY - (ascent + descent)/2
∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2
②centerLine作为topLine和bottomLine的中间线
centerLineY = (bottomLineY + topLineY)/2
<=> centerLineY = (bottom + baselineY + top + baselineY)/2
<=> centerLineY = baselineY + (bottom + top)/2
<=>baselineY = centerLineY - (bottom + top)/2
∵ bottom = fontMetrics.bottom, top = fontMetrics.top
∴ baseLineY = centerLineY - (fontMetrics.bottom + fontMetrics.top)/2
因为topLine与ascentLine的距离大于bottomLine与descentLine距离。所以①方法更接近与文字中线。选择①的计算结果为基线位置(①和②差别不很大)。
代码如下:
private void drawText_centerLine(Canvas canvas) {
String text = "AngelÂ";
int baseX = 120;
int centerLineY = 200;
//设置文字大小和文字排列
mPaint.setTextSize(200);//单位px
mPaint.setTextAlign(Paint.Align.LEFT);
//画中线
mPaint.setColor(Color.BLUE);
canvas.drawLine(0, centerLineY, 3000, centerLineY, mPaint);
//计算基线位置
/*
* centerLineY = (ascentLineY + descentLineY)/2
* <=> centerLineY = (ascent + baselineY + descent + baselineY)/2
* <=> centerLineY = baselineY + (ascent + descent)/2
* <=>baselineY = centerLineY - (ascent + descent)/2
* ∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
* ∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2
* */
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
//float baselineY = centerLineY - (fontMetrics.top + fontMetrics.bottom) / 2;
float baselineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent) / 2;
//画基线
mPaint.setColor(Color.GREEN);
canvas.drawLine(0, baselineY, 3000, baselineY, mPaint);
//画文字
mPaint.setColor(Color.BLACK);
canvas.drawText(text, baseX, baselineY, mPaint);
//画出其他几条线
mPaint.setColor(Color.MAGENTA);
float topY = baselineY + fontMetrics.top;
float bottomY = baselineY + fontMetrics.bottom;
float ascentY = baselineY + fontMetrics.ascent;
float decentY = baselineY + fontMetrics.descent;
canvas.drawLine(0, topY, 3000, topY, mPaint);
canvas.drawLine(0, bottomY, 3000, bottomY, mPaint);
canvas.drawLine(0, ascentY, 3000, ascentY, mPaint);
canvas.drawLine(0, decentY, 3000, decentY, mPaint);
}
效果:
①baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2
②baseLineY = centerLineY - (fontMetrics.bottom + fontMetrics.top)/2
从效果可以看出,centerLine作为ascentLine和descentLine的中线更接近于文字真正的中线,而作为topLine和bottomLine中线则接近与文字上部。
笔记终于做完,经过自己的学习和验证,多有收获。看似有点耗时,但是确实值得。