自定义View drawText()绘制文字

1.Canvas 绘制文字方式

Canvas 绘制文字的方式:drawText() drawTextRun() drawTextOnPath()

1.1 drawText(String text,float x,float y,Paint paint)

注:如果你从(0,0)点开始绘制Text,文字不会显示在View左上角,会显示在View的左上方。


image.png

这张图,电线上的小鸟,这里,电线就类似于文字的基线。


image.png

盗图:Hencoder

canvas.drawText()中,参数y,是指的文字的基线(baseLine)。参数x,也不是文字最左面的点,x的位置是文字起点靠左一点(这里是因为对于绝大多数字符,他们的宽度都要略微大于实际显示的宽度,字符的左右两边都会留出一部分间隙,用于文字间的间隔,因此我们设定绘制文字起点的时候 ,会发现实际文字绘制时候会靠右一点)。

1.2 drawTextRun() (这个在中国应该用不到)

类 似于drawText() 但是增加了两个设置----上下文---文字方向

drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)

参数:
text:要绘制的文字
start:从那个字开始绘制
end:绘制到哪个字结束
contextStart:上下文的起始位置。contextStart 需要小于等于 start
contextEnd:上下文的结束位置。contextEnd 需要大于等于 end
x:文字左边的坐标
y:文字的基线坐标
isRtl:是否是 RTL(Right-To-Left,从右向左)

1.3 drawTextOnPath()

沿着一条Path来绘制文字。
例:


image.png
paint.setTextSize(60);
paint.setColor(Color.RED);
mRectF = new RectF(100,100,800,500);
mPath.addOval(mRectF,Path.Direction.CW);
    
canvas.drawTextOnPath(text,mPath,1,0,paint);

drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)

参数
hOffset:文字相对于 Path 的水平偏移量
vOffset:文字相对于 Path 的竖直偏移量
利用它们可以调整文字的位置。例如你设置 hOffset 为 5, vOffset 为 10,文字就会右移 5 像素和下移 10 像素。

1.4 staticLayout

staticLayout 是用canvas来绘制,但是不是canvas的方法。staticLayout一般用于绘制多行textView。如果你需要进行多行文字的绘制,并且对文字的排列和样式没有太复杂的花式要求,那么使用staticLayout最好。

StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,它是纯粹用来绘制文字的。 StaticLayout 支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \n 处主动换行。

例:


image.png
    String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";  
    StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600,  
            Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    String text2 = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";  
    StaticLayout staticLayout2 = new StaticLayout(text2, paint, 600,  
            Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    
    ...
    
    canvas.save();  
    canvas.translate(50, 100);  
    staticLayout1.draw(canvas);  
    canvas.translate(0, 200);  
    staticLayout2.draw(canvas);  
    canvas.restore(); 

StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)

参数:
width 是文字区域的宽度,文字到达这个宽度后就会自动换行;
align 是文字的对齐方向;
spacingmult 是行间距的倍数,通常情况下填 1 就好;
spacingadd 是行间距的额外增加值,通常情况下填 0 就好;
includeadd 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。

2.Paint 对文字绘制的辅助

2.1 设置显示效果类

2.1.1 setTextSize(float textSize)

设置文字大小

2.1.2 setTypeface(Typeface typeface)

设置字体,字体可以是系统所有的字体,也可以是自己的字体。官方文档

这里的Typeface跟font是一个意思,都表示字体,但是,typerface指的是某套字体(font family),而font值指的是typeface具体的某个weight和size的分支。

2.1.3 setFakeBoldText(boolean fakeBoldText)

是否使用伪粗体。


image.png

这种粗体叫做伪粗体,因为他不是设置更高的weight的字体让文字变粗,他是通过程序在运行时候将字体描粗。

2.1.4 setStrikeThurText(boolean strikeThruText)

是否添加删除线


image.png
2.1.5 setUnderlineText(boolean underlineText)

是否添加下划线


image.png

#######2.1.6 setTextSkewX(float skewX)
设置文字的错切角度,通俗的说,就是设置文字倾斜


image.png
2.1.7 setTextScaleX(float scaleX)

设置文字横向缩放(就是文字变胖变瘦)。


image.png
    paint.setTextScaleX(0.5f);
    canvas.drawText(text, 50, 100, paint);
    paint.setTextScaleX(1.0f);
    canvas.drawText(text, 50, 150, paint);
    paint.setTextScaleX(1.5f);
    canvas.drawText(text, 50, 200, paint);
2.1.8 setLetterSpacing(float letterSpacing)

设置字符间距。默认为0。
注:setLetterSpacing为字符间距,setTextScaleX为文字横向宽度。

2.1.9 setFontFeatureSettrings(String settings)

用CSS的font-feature-settings 的方式来设置文字。

2.1.10 setTextAlign(Paint.Align align)

设置文字对其方式(LEFFT,CETNER,RIGHT 左中右,默认为左 既LEFT)。


image.png
2.1.11setTextLocale(Locale locale)/setTextLocales(LocaleList locales)

设置绘制所使用的Locale,就是设置不同地域的语言(是汉语还是英语)

2.1.12 setHinting(int mode)

设置是否启用字体的hinting(字体微调)
现在的 Android 设备大多数都是是用的矢量字体。矢量字体的原理是对每个字体给出一个字形的矢量描述,然后使用这一个矢量来对所有的尺寸的字体来生成对应的字形。由于不必为所有字号都设计它们的字体形状,所以在字号较大的时候,矢量字体也能够保持字体的圆润,这是矢量字体的优势。不过当文字的尺寸过小(比如高度小于 16 像素),有些文字会由于失去过多细节而变得不太好看。 hinting 技术就是为了解决这种问题的:通过向字体中加入 hinting 信息,让矢量字体在尺寸过小的时候得到针对性的修正,从而提高显示效果。效果图盗一张维基百科的:


image.png

对于现在的手机(屏幕密度非常高),几乎不会出现字体尺寸小道需要修改hinting来修正的情况,所以这个方法,现在几乎不会用到。

2.1.13 setElegantTextHeight(boolean elegant)

设置是否开启文字的elegant height。
这个方法适用于有较高文字的语言。
把「大高个」文字的高度恢复为原始高度;
增大每行文字的上下边界,来容纳被加高了的文字。
中文不需要!!!

2.1.14 setSubpixelText(boolean subpixeText)

是否开启像素级抗锯齿
详细介绍

2.1.15 setLinearText(boolean linearText)

2.2 测量文字尺寸类

文字也有他自己的尺寸。
########2.2.1 float getFontsPACING()
获取推荐的行距

即推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字(而不是使用 StaticLayout)的时候,可以在换行的时候给 y 坐标加上这个值来下移文字。

2.2.2 FontMetrics getFontMetrice()

获取Paint的FontMetrics

盗图:henCoder


image.png

如图,图中有两行文字,每一行都有 5 条线:top, ascent, baseline, descent, bottom。

• baseline: 上图中黑色的线。它的作用是作为文字显示的基准线。

• ascent / descent: 上图中绿色和橙色的线,它们的作用是限制普通字符的顶部和底部范围。
普通的字符,上不会高过 ascent ,下不会低过 descent ,例如上图中大部分的字形都显示在 ascent 和 descent 两条线的范围内。具体到 Android 的绘制中, ascent 的值是图中绿线和 baseline 的相对位移,它的值为负(因为它在 baseline 的上方); descent 的值是图中橙线和 baseline 相对位移,值为正(因为它在 baseline 的下方)。

• top / bottom: 上图中蓝色和红色的线,它们的作用是限制所有字形( glyph )的顶部和底部范围。
除了普通字符,有些字形的显示范围是会超过 ascent 和 descent 的,而 top 和 bottom 则限制的是所有字形的显示范围,包括这些特殊字形。例如上图的第二行文字里,就有两个泰文的字形分别超过了 ascent 和 descent 的限制,但它们都在 top 和 bottom 两条线的范围内。具体到 Android 的绘制中, top 的值是图中蓝线和 baseline 的相对位移,它的值为负(因为它在 baseline 的上方); bottom 的值是图中红线和 baseline 相对位移,值为正(因为它在 baseline 的下方)。

• leading: 这个词在上图中没有标记出来,因为它并不是指的某条线和 baseline 的相对位移。 leading 指的是行的额外间距,即对于上下相邻的两行,上行的 bottom 线和下行的 top 线的距离,也就是上图中第一行的红线和第二行的蓝线的距离(对,就是那个小细缝)。

leading 这个词的本意其实并不是行的额外间距,而是行距,即两个相邻行的 baseline 之间的距离。不过对于很多非专业领域,leading 的意思被改变了,被大家当做行的额外间距来用;而 Android 里的 leading ,同样也是行的额外间距的意思。leading 在这里应该读作 "ledding" 而不是 "leeding" 。

FontMetrics 提供的就是 Paint 根据当前字体和字号,得出的这些值的推荐值。它把这些值以变量的形式存储,供开发者需要时使用。

FontMetrics.ascent:float 类型。
FontMetrics.descent:float 类型。
FontMetrics.top:float 类型。
FontMetrics.bottom:float 类型。
FontMetrics.leading:float 类型。
另外,ascent 和 descent 这两个值还可以通过 Paint.ascent() 和 Paint.descent() 来快捷获取。

FontMetrics 和 getFontSpacing();
从定义可以看出,上图中两行文字的 font spacing (即相邻两行的 baseline 的距离) 可以通过 bottom - top + leading (top 的值为负,前面刚说过,记得吧?)来计算得出。

实际 bottom - top + leading 的结果是要大于 getFontSpacing() 的返回值的。

两个方法计算得出的 font spacing 竟然不一样?

这并不是 bug,而是因为 getFontSpacing() 的结果并不是通过 FontMetrics 的标准值计算出来的,而是另外计算出来的一个值,它能够做到在两行文字不显得拥挤的前提下缩短行距,以此来得到更好的显示效果。所以如果你要对文字手动换行绘制,多数时候应该选取 getFontSpacing() 来得到行距,不但使用更简单,显示效果也会更好。

getFontMetrics() 的返回值是 FontMetrics 类型。它还有一个重载方法 getFontMetrics(FontMetrics fontMetrics) ,计算结果会直接填进传入的 FontMetrics 对象,而不是重新创建一个对象。这种用法在需要频繁获取 FontMetrics 的时候性能会好些。

另外,这两个方法还有一对同样结构的对应的方法 getFontMetricsInt() 和 getFontMetricsInt(FontMetricsInt fontMetrics) ,用于获取 FontMetricsInt 类型的结果。

推荐:计算基线

2.2.3 getTextBounds(String text,int start,int end, Rect bounds);

获取文字的显示范围,text为要测量的文字,start为文字起始的位置,end为文字介素的位子,bounds存储文字显示范围的对象,在测量结束以后会将结果写入bounds.

(他还有一个重载方法 getTextBounds(char[] text, int index, int count, Rect bounds) 用法相似)

    paint.setStyle(Paint.Style.FILL);  
    canvas.drawText(text, offsetX, offsetY, paint);
    
    paint.getTextBounds(text, 0, text.length(), bounds);  
    bounds.left += offsetX;  
    bounds.top += offsetY;  
    bounds.right += offsetX;  
    bounds.bottom += offsetY;  
    paint.setStyle(Paint.Style.STROKE);  
    canvas.drawRect(bounds, paint);
image.png
2.2.4 float measure(String text)

测量文字宽度并返回

注:measure(String text) 测量后的宽度比getTextBounds()测量后的值大一点
• getTextBounds: 它测量的是文字的显示范围(关键词:显示)。形象点来说,你这段文字外放置一个可变的矩形,然后把矩形尽可能地缩小,一直小到这个矩形恰好紧紧包裹住文字,那么这个矩形的范围,就是这段文字的 bounds。

• measureText(): 它测量的是文字绘制时所占用的宽度(关键词:占用)。前面已经讲过,一个文字在界面中,往往需要占用比他的实际显示宽度更多一点的宽度,以此来让文字和文字之间保留一些间距,不会显得过于拥挤。上面的这幅图,我并没有设置 setLetterSpacing() ,这里的 letter spacing 是默认值 0,但你可以看到,图中每两个字母之间都是有空隙的。另外,下方那条用于表示文字宽度的横线,在左边超出了第一个字母 H 一段距离的,在右边也超出了最后一个字母 r(虽然右边这里用肉眼不太容易分辨),而就是两边的这两个「超出」,导致了 measureText() 比 getTextBounds() 测量出的宽度要大一些。

#######2.2.5 getTextWidths(String text,float[] widths)
获取字符串中每个字符的宽度,并把结果填入参数 widths。

这相当于 measureText() 的一个快捷方法,它的计算等价于对字符串中的每个字符分别调用 measureText() ,并把它们的计算结果分别填入 widths 的不同元素。

2.2.6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)

这个方法也是用来测量文字宽度的。但和 measureText() 的区别是, breakText() 是在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。
注:是在给出的宽度范围下测量文字宽度,如果文字宽度超出上限,在临近超限位置截断文字。
breakText的返回值是截取的文字个数。常用语多行文字的折行计算。

2.2.7 光标

2.2.7.1 getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
对于一段文字,计算出某个字符处光标的 x 坐标。 start end 是文字的起始和结束坐标;contextStart contextEnd 是上下文的起始和结束坐标;isRtl 是文字的方向;offset 是字数的偏移,即计算第几个字符处的光标。
例:

    int length = text.length();  
    float advance = paint.getRunAdvance(text, 0, length, 0, length, false, length);  
    canvas.drawText(text, offsetX, offsetY, paint);  
    canvas.drawLine(offsetX + advance, offsetY - 50, offsetX + advance, offsetY + 10, paint); 
image.png

其实,说是测量光标位置的,本质上这也是一个测量文字宽度的方法。上面这个例子中,start 和 contextStart 都是 0, end contextEnd 和 offset 都等于 text.length()。在这种情况下,它是等价于 measureText(text) 的,即完整测量一段文字的宽度。而对于更复杂的需求,getRunAdvance() 能做的事就比 measureText() 多了。

例:

    // 包含特殊符号的绘制(如 emoji 表情)
    String text = "Hello HenCoder \uD83C\uDDE8\uD83C\uDDF3" // "Hello HenCoder 🇨🇳"
        
        ...
        
        float advance1 = paint.getRunAdvance(text, 0, length, 0, length, false, length);  
        float advance2 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 1);  
        float advance3 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 2);  
        float advance4 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 3);  
        float advance5 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 4);  
        float advance6 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 5);
        
        ...
image.png

如上图,🇨🇳 虽然占了 4 个字符(\uD83C\uDDE8\uD83C\uDDF3),但当 offset 是表情中间处时, getRunAdvance() 得出的结果并不会在表情的中间处。

2.2.7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量(即第几个字符最接近这个坐标)。

参数:
text 是要测量的文字;
start end 是文字的起始和结束坐标;
contextStart contextEnd 是上下文的起始和结束坐标;
isRtl 是文字方向;
advance 是给出的位置的像素值。填入参数,对应的字符偏移量将作为返回值返回。

getOffsetForAdvance() 配合上 getRunAdvance() 一起使用,就可以实现「获取用户点击处的文字坐标」的需求。

#######2.2.8 hasGlyph(String string)
检查指定的字符串中是否是一个单独的字形 (glyph)。最简单的情况是,string 只有一个字母(比如 a)。

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容