自定义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)。