您的位置:首页 > 其它

博客笔记:自定义View之绘图(1)--drawText

2017-05-22 18:39 274 查看
声明:

发现了一篇自定义View的博客索引,有比较系统的自定义View技术点。欣喜之下准备学习一下,把这些技术点学会,消化,变成自己的东西。做些笔记,或有copy,原文更好。看原文,请转到:启舰的博客—Android自定义控件三部曲文章索引

一、概述

1. 四线格与基线

在阅读启舰的博客: 自定义控件之绘图篇( 五):drawText()详解后,终于知道,原来canvas在绘制文字时,是有规则的,这个规则就是–基线。这也正是我之前疑惑之一, 为什么总画不好文字,感觉代码没错呀,文字位置为什么不对。看了此博客,终于解惑,感谢启舰的博客^_^。

记得初学字母时,要用到四线格作业本, 我们将字母按格式写到四线格内,见下图:



canvas在使用drawText绘制文字时是有标准的,它是以基线为基准的。



由图可以看出,基线就相当于四线格的第三条线。基线位置确定,文字位置也就确定了。

2. canvas.drawText()

- [b]1> canvas.drawText()和基线 [/b]

下面是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值确定以后,文字具体怎么显示还跟对齐设置有关。

- [b]2> 代码 [/b]

自定义一个
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 defSt
1341a
yleAttr) {
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)

- [b]1> 文字对齐和原点 [/b]

上面我们知道了我们确定了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);
}


- [b]2> 代码和效果 [/b]

下面,我们以(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

- [b]1> FontMetrics源码及概述 [/b]

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();


- [b]2> 代码和效果 [/b]

上面我们计算通过基线和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. 文字的宽度和高度

- [b]1> 文字高度 [/b]

文字高度容易获取,直接用bottomLineY - topLineY即可。

Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int bottomY = fontMetricsInt.bottom + baseLineY;
int topY = fontMetricsInt.top + baseLineY;
//所占高度
int height = bottomY - topY;


- [b]2> 文字宽度 [/b]

文字的宽度要用到Paint类的一个方法:measureText(String text)

mPaint.setTextSize(120f);
int width = (int) mPaint.measureText(text);


注意:使用前,一定要设置好文字的大小(如,mPaint.setTextSize(160f)),不然无法测量文字的宽度。

- [b]3> 最小矩形 [/b]

要得到绘制文字的最小矩形,同样要用到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)。相当于平移了最小矩形。

- [b]2> 代码和效果 [/b]

上面我们知道了怎样获得文字宽度、高度以及最小矩形,我们就可以绘制出文字所站区域和最小矩形。代码如下:

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中线则接近与文字上部。

笔记终于做完,经过自己的学习和验证,多有收获。看似有点耗时,但是确实值得。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: