Android 中自定义View(三)

系列文章之 Android中自定义View(一)
系列文章之 Android中自定义View(二)
系列文章之 Android中自定义View(三)
系列文章之 Android中自定义View(四)
系列文章之 Android中自定义View(xml绘图)
本文出自:
http://www.jianshu.com/u/a1251e598483

我们在使用各种App时都会看到好多漂亮的效果,说实话有的效果真的很好看,所以觉得能写出这些效果的人都好厉害的说,自定义View 在Android 进阶相关的图书中都是必会内容,我也一直看过大概的自定义View 的内容,看过之后还是觉得不够详细,上手还是抓瞎. 刚好网上 扔物线 大神 写了一个自定义view 的详细教程. http://hencoder.com .如果想学习自定义View的同学请去 大神那里围观,本文是记录自己学习 自定义View 的理解和收获,也是一个记录吧,等到用的时候比较容易找到.

我是分割线,下面开始本文内容--------------------------

自定义View分为以下几个部分

  • Canvas 的 drawXXX() 系列方法及 Paint 最常见的使用
  • Paint 的完全攻略
  • Canvas 对绘制的辅助——范围裁切和几何变换。
  • 使用不同的绘制方法来控制绘制顺序
今天这篇就是第三部分: 文字的绘制

1 Canvas 绘制文字的方式

Canvas 的文字绘制方法有三个:drawText() drawTextRun() 和 drawTextOnPath()。

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

drawText() 是 Canvas 最基本的绘制文字的方法:给出文字的内容和位置, Canvas 按要求去绘制文字。
这个很简单,不需要解释
方法的参数很简单: text是文字内容,x和 y是文字的坐标。但需要注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。大概在这里:


而如果你像绘制其他内容一样,在绘制文字的时候把坐标填成 (0, 0),文字并不会显示在 View 的左上角,而是会几乎完全显示在 View 的上方,到了 View 外部看不到的位置:
drawText()参数中的 y ,指的是文字的基线( baseline ) 的位置。

1.2 drawTextOnPath()

沿着一条 Path
来绘制文字。这是一个耍杂技的方法。
canvas.drawPath(path, paint); // 把 Path 也绘制出来,理解起来更方便 canvas.drawTextOnPath("Hello HeCoder", path, 0, 0, paint);

drawTextOnPath() 使用的 Path,拐弯处全用圆角,别用尖角。

具体的方法很简单:

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

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

1.3 StaticLayout

额外讲一个 StaticLayout。这个也是使用 Canvas 来进行文字的绘制,不过并不是使用 Canvas 的方法。

Canvas.drawText() 只能绘制单行的文字,而不能换行。不能在 View 的边缘自动折行, 不能在换行符 \n 处换行
如果需要绘制多行的文字,你必须自行把文字切断后分多次使用 drawText() 来绘制,或者——使用 StaticLayout 。
StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,它是纯粹用来绘制文字的。 StaticLayout 支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \n 处主动换行。

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
的构造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中参数里:
width
是文字区域的宽度,文字到达这个宽度后就会自动换行;
align是文字的对齐方向;
spacingmult是行间距的倍数,通常情况下填 1 就好;
spacingadd 是行间距的额外增加值,通常情况下填 0 就好;
includeadd是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。
如果你需要进行多行文字的绘制,并且对文字的排列和样式没有太复杂的花式要求,那么使用 StaticLayout就好。

2.1 设置显示效果类

2.1.1 setTextSize(float textSize)

设置文字大小。

2.1.2 setTypeface(Typeface typeface)

设置字体。

2.1.3 setFakeBoldText(boolean fakeBoldText)

是否使用伪粗体。
paint.setFakeBoldText(false); 之所以叫伪粗体( fake bold ),因为它并不是通过选用更高 weight 的字体让文字变粗,而是通过程序在运行时把文字给「描粗」了。

2.1.4 setStrikeThruText(boolean strikeThruText)

是否加删除线。

2.1.5 setUnderlineText(boolean underlineText)

是否加下划线。

2.1.6 setTextSkewX(float skewX)

设置文字横向错切角度。其实就是文字倾斜度的啦。

2.1.7 setTextScaleX(float scaleX)

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

2.1.8 setLetterSpacing(float letterSpacing)

设置字符间距。默认值是 0。

2.1.9 setFontFeatureSettings(String settings)

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

2.1.10 setTextAlign(Paint.Align align)

设置文字的对齐方式。一共有三个值:LEFT CETNER 和 RIGHT。默认值为 LEFT。

2.1.11 setTextLocale(Locale locale) / setTextLocales(LocaleList locales)

设置绘制所使用的 Locale。

2.1.12 setHinting(int mode)

设置是否启用字体的 hinting (字体微调)。

2.1.13 setElegantTextHeight(boolean elegant)

设置是否开启文字的 elegant height 。开启之后,文字的高度就变优雅了(误)。下面解释一下所谓的 elegant height:

2.1.14 setSubpixelText(boolean subpixelText)

是否开启次像素级的抗锯齿( sub-pixel anti-aliasing )。

2.1.15 setLinearText(boolean linearText)

2.2 测量文字尺寸类

不论是文字,还是图形或 Bitmap,只有知道了尺寸,才能更好地确定应该摆放的位置。由于文字的绘制和图形或 Bitmap 的绘制比起来,尺寸的计算复杂得多,所以它有一整套的方法来计算文字尺寸。

2.2.1 float getFontSpacing()

获取推荐的行距。

即推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字(而不是使用 StaticLayout)的时候,可以在换行的时候给 y 坐标加上这个值来下移文字。
canvas.drawText(texts[0], 100, 150, paint); canvas.drawText(texts[1], 100, 150 + paint.getFontSpacing, paint); canvas.drawText(texts[2], 100, 150 + paint.getFontSpacing * 2, paint);

2.2.2 FontMetircs getFontMetrics()

获取 Paint 的 FontMetrics。FontMetrics
是个相对专业的工具类,它提供了几个文字排印方面的数值:ascent, descent, top, bottom, leading。



如图,图中有两行文字,每一行都有 5 条线:top, ascent, baseline, descent, bottom。(leading
并没有画出来,因为画不出来,下面会给出解释)

  • 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 ,同样也是行的额外间距的意思。

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。

2.2.4 float measureText(String text)

测量文字的宽度并返回。

如果你用代码分别使用 getTextBounds() 和 measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是比 getTextBounds() 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。

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

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

在实际的开发中,测量宽度要用 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() 是在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。

2.2.7 光标相关

对于 EditText 以及类似的场景,会需要绘制光标。光标的计算很麻烦,不过 API 23 引入了两个新的方法,有了这两个方法后,计算光标就方便了很多。

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 是字数的偏移,即计算第几个字符处的光标。

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)。

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

推荐阅读更多精彩内容