Canvas的那些事,图解Android中的绘制技巧
今日快讯
北京时间2月7日凌晨4时45分,太空探索公司SpaceX试射的猎鹰重型火箭已成功发射升空,将把一辆特斯拉跑车送入绕太阳飞行的轨道。该火箭拥有3个推进器和27个引擎,能够提供超过200万公斤的推力,是世界现役最强大的运载火箭。
作者简介
今天这篇文章是过年前的倒数二篇文章,很快就要和大家短暂地说再见了,也希望每一位朋友都能坚守好最后几天的岗位。
本篇来自
容华谢后
的投稿,分享了Android中Canvas调用drawText方法的原理,希望大家会喜欢!
容华谢后
的博客地址:
http://www.jianshu.com/u/34ece31cd6eb
正文
写在前面
在实现自定义控件的过程中,常常会有绘制居中文字的需求,于是在网上搜了一些相关的博客,总是看的一脸懵逼,就想着自己分析一下,在此记录下来,希望对大家能够有所帮助。
绘制一段文本
首先把坐标原点移动到控件中心(默认坐标原点在屏幕左上角),这样看起来比较直观一些,然后绘制x、y轴,此时原点向上y为负,向下y为正,向左x为负,向右x为正,以(0,0)坐标开始绘制一段文本:
@Override
public
void
draw
(Canvas canvas)
{super
.draw(canvas);// 将坐标原点移到控件中心
canvas.translate(getWidth() /
2
, getHeight() /2
);// x轴
canvas.drawLine(-getWidth() /
2
,0
, getWidth() /2
,0
, paint);// y轴
canvas.drawLine(
0
, -getHeight() /2
,0
, getHeight() /2
, paint);// 绘制文字
paint.setTextSize(sp2px(
50
));canvas.drawText(
"YangLe"
,0
,0
, paint);}
看下绘制的文本:
咦,为什么绘制的文本在第一象限,y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴,带着这些疑问继续往下看:
首先看一个重要的类:
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;}
FontMetrics类是Paint的一个内部类,主要定义了绘制文本时的一些关键坐标位置,看下这些值都代表什么:
看图说话:
top:从基线(x轴)向上绘制区域的最高点,此值为负值
ascent:单行文本,从基线(x轴)向上绘制的推荐最高点,此值为负值
baseline:基线,此值为0
descent:单行文本,从基线(x轴)向下绘制的推荐最低点,此值为正值
bottom:从基线(x轴)向下绘制区域的最低点,此值为正值
leading:推荐的额外行距,一般为0
下面再来看看drawText这个方法:
/**
* Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
* based on the Align setting in the paint.
*
*
@param
text The text to be drawn*
@param
x The x-coordinate of the origin of the text being drawn*
@param
y The y-coordinate of the baseline of the text being drawn*
@param
paint The paint used for the text (e.g. color, size, style)*/
public
void
drawText
(@NonNull String text,
float
x,float
y, @NonNull Paint paint) {super
.drawText(text, x, y, paint);}
重点看下x、y参数的含义:
x:绘制文本的起始x坐标
y:绘制文本的baseline在y轴方向的位置
有点难理解,举个栗子,上文中的x、y参数传的是(0,0),此时的baseline正好是坐标系中x轴,就相当于从y轴开始向右绘制,以x轴作为文本的baseline进行绘制。
如果参数传(0,10),此时绘制文本的baseline从x轴开始向下移动10px,也就是以y10作为文本的baseline进行绘制,y10就是绘制文本的baseline在y轴方向的位置。
注意:baseline是绘制文本的基线,相对于绘制文本区域来说,相当于x轴,向上为负(top、ascent),向下为正(descent、bottom),但是这个x轴并不是控件的x轴,切记切记!!!
还记得我们在上文中提出的疑问吗,这下可以解释了:
为什么绘制的文本在第一象限?
因为我们把坐标原点移到了控件中心,文本的baseline正好为x轴,top、ascent值为负,所以绘制的文本在第一象限。
y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴?
drawText方法默认x轴方向是从左到右绘制的,y轴方向是从baseline为基准绘制的,文中的baseline正好为x轴,以baseline为基准绘制文本向下还有一段距离,所以文本穿过了x轴。
绘制居中的文本
在上文中,我们学习了如何绘制一段文本,以及其中参数和坐标的含义,接下来进入正题,看下如何才能绘制居中的文本。
首先看一张图,此时文本的baseline正好为x轴,如果想要文本居中显示的话,就需要先计算文本的宽度和高度:
宽度:调用Paint的measureText方法就可以获得文本的宽度
高度:文本的高度就是实际绘制区域的高度,可以用(fontMetrics.descent - fontMetrics.ascent)获取,因为ascent为负数,所以最终算出来的是两者的和
现在有了宽度,把绘制文本的x坐标向左移动(宽度 / 2)就可以水平居中,但是垂直方向就不能这么干了,我们要将文本向下移动baseline到文本中心的距离,也就是(高度 / 2 - fontMetrics.descent),如下图所示:
现在的公式为:
float
baseLineY= (fontMetrics.descent - fontMetrics.ascent) /
2
- fontMetrics.descent;= -fontMetrics.ascent /
2
- fontMetrics.descent /2
;= -(fontMetrics.ascent + fontMetrics.descent) /
2
;= Math.abs(fontMetrics.ascent + fontMetrics.descent) /
2
;Paint中也有获取ascent和descent值的方法,所以公式最终为:
float
baseLineY = Math.abs(paint.ascent() + paint.descent()) /2
;注意:此公式是相对于坐标原点在控件中心来计算的,如果坐标原点在左上角,baseLineY需要加上控件高度的一半。
float
baseLineY = height /2
+ Math.abs(paint.ascent() + paint.descent()) /2
;看下代码:
@Override
public
void
draw
(Canvas canvas)
{super
.draw(canvas);// 将坐标原点移到控件中心
canvas.translate(getWidth() /
2
, getHeight() /2
);// x轴
canvas.drawLine(-getWidth() /
2
,0
, getWidth() /2
,0
, paint);// y轴
canvas.drawLine(
0
, -getHeight() /2
,0
, getHeight() /2
, paint);// 绘制居中文字
paint.setTextSize(sp2px(
50
));paint.setColor(Color.GRAY);
// 文字宽
float
textWidth = paint.measureText("YangLe"Blog"
);// 文字baseline在y轴方向的位置
float
baseLineY = Math.abs(paint.ascent() + paint.descent()) /2
;canvas.drawText(
"YangLe"Blog"
, -textWidth /2
, baseLineY, paint);}
看下居中了吗:
大功告成!
绘制多行居中的文本
注意:drawText方法不支持绘制多行文本
方式一
使用支持自动换行的StaticLayout:
/**
* 绘制多行居中文本(方式1)
*
*
@param
canvas 画布*/
private
void
drawCenterMultiText1
(Canvas canvas)
{String text =
"ABC"
;// 画笔
TextPaint textPaint =
new
TextPaint();textPaint.setAntiAlias(
true
);textPaint.setColor(Color.GRAY);
// 设置宽度超过50dp时换行
StaticLayout staticLayout =
new
StaticLayout(text, textPaint, dp2px(50
),Layout.Alignment.ALIGN_CENTER,
1f
,0f
,false
);canvas.save();
// StaticLayout默认从(0,0)点开始绘制
// 如果需要调整位置,只能在绘制之前移动Canvas的起始坐标
canvas.translate(-staticLayout.getWidth() /
2
, -staticLayout.getHeight() /2
);staticLayout.draw(canvas);
canvas.restore();
}
看下StaticLayout的构造方法参数含义:
public
StaticLayout
(CharSequence source, TextPaint paint,
int
width, Alignment align,float
spacingmult,float
spacingadd,boolean
includepad) {this
(source,0
, source.length(), paint, width, align, spacingmult, spacingadd, includepad);}
source:需要分行的文本
paint:画笔对象
width:layout的宽度,文本超出宽度时自动换行
align:layout的对其方式
spacingmult:相对行间距,相对字体大小,1f表示行间距为1倍的字体高度
spacingadd:基础行距偏移值,实际行间距等于(spacingmult + spacingadd)
includepad:参数未知
看下效果:
使用StaticLayout,每行设置的宽度是相同的,当需求为每行显示不同长度的文本时,这种方式就不能使用了,别担心,接着来看下第二种方式。
方式二
使用循环drawText的方式进行绘制,看图说话:
现在需要绘制A、B、C三行文本,红色A代表每行文本默认的绘制位置,绿色的线代表每行文本的baseline,x轴为红色A的baseline,现在分为三种情况:
文本在x轴上方:红色A的baseline向上移动a距离,总高度的/2 - 文本的top值(绝对值)
文本在x轴中间:红色A的baseline向下移动b距离,计算公式请参考单行文本居中公式
文本在x轴下方:红色A的baseline向下移动c距离,总高度的/2 - 文本的bottom值(绝对值)
看下代码:
/**
* 绘制多行居中文本(方式2)
*
*
@param
canvas 画布*/
private
void
drawCenterMultiText2
(Canvas canvas)
{String[] texts = {
"A"
,"B"
,"C"
};Paint.FontMetrics fontMetrics = paint.getFontMetrics();
// top绝对值
float
top = Math.abs(fontMetrics.top);// ascent绝对值
float
ascent = Math.abs(fontMetrics.ascent);// descent,正值
float
descent = fontMetrics.descent;// bottom,正值
float
bottom = fontMetrics.bottom;// 行数
int
textLines = texts.length;// 文本高度
float
textHeight = top + bottom;// 文本总高度
float
textTotalHeight = textHeight * textLines;// 基数
float
basePosition = (textLines -1
) /2f
;for
(int
i =0
; i < textLines; i++) {// 文本宽度
float
textWidth = paint.measureText(texts[i]);// 文本baseline在y轴方向的位置
float
baselineY;if
(i < basePosition) {// x轴上,值为负
// 总高度的/2 - 已绘制的文本高度 - 文本的top值(绝对值)
baselineY = -(textTotalHeight /
2
- textHeight * i - top);}
else
if
(i > basePosition) {// x轴下,值为正
// 总高度的/2 - 未绘制的文本高度 - 文本的bottom值(绝对值)
baselineY = textTotalHeight /
2
- textHeight * (textLines - i -1
) - bottom;}
else
{// x轴中,值为正
// 计算公式请参考单行文本居中公式
baselineY = (ascent - descent) /
2
;}
canvas.drawText(texts[i], -textWidth /
2
, baselineY, paint);}
}
对照上图再看代码就很好理解了,觉得代码中的公式还有可以优化的地方,如果你有好的方法,可以留言告诉我哈。
再看下中文版的多行文本:
TextAlign
Paint的TextAlign属性决定了绘制文本相对于drawText方法中x参数的相对位置。
举个栗子:
Paint.Align.LEFT:默认属性,x坐标为绘制文本的最左侧坐标
Paint.Align.CENTER:x坐标为绘制文本的水平中心坐标
Paint.Align.RIGHT:x坐标为绘制文本的最右侧坐标
看图理解下:
文本居中的公式
坐标原点在控件中心:
float
baseLineY = Math.abs(paint.ascent() + paint.descent()) /2
;坐标原点在控件左上角:
float
baseLineY = height /2
+ Math.abs(paint.ascent() + paint.descent()) /2
;结语
源码已经上传到GitHub上了,欢迎Fork,觉得还不错就Start一下吧!
项目地址:
http://github.com/alidili/Demos/tree/master/DrawTextDemo
如果有什么问题欢迎留言哈,谢谢!
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
- 还记得那些lol很装逼很搞笑的句子吗
- 凤梨和菠萝究竟是什么关系?说说那些外形相似的水果!
- 《我的世界》抄袭《迷你世界》?说说那些属于MC的感动!
- “企业年金”你应该知道的那些事
- 盘点十二星座那些让人烦的性格特质
- LCK那些惊悚对局:Faker也比不上这个补刀机器!
- 春节翻书 那些名著的序言也非常有意思哦
- 盘点那些年小卖部里的“5角零食”,最后一种不多见了
- 明朝奇葩皇帝那些二三事,一个个都活生生给自己作死
- 【极产品】寻找2017年那些牛X而任性的产品(5)