前言
关于TextView相关的文章,我们之前也实现过,在读本篇之前,可以先读读其他文章
TextView是相当复杂的UI组件,TextView不仅仅支持纯文本展示,而且还支持图片、SpannableString、文本输入、超链接等诸多功能,因此很多View本身也是直接继承自TextView的,如EditText、Button、Chronometer等。可见TextView功能非常强大,基本上是app中使用率最高的View组件。
不过 TextView 缺点也不少,主要问题点如下:
- 跑马灯执行的条件过高,且部分属性有一定的重复问题
- setText 容易触发requestLayout
- 换行文本容易出现犬牙(很多小说类app自行绘制文本来解决此问题)
当然,以上是大多数情况中我们容易遇到的问题。
优化方法
上面我们列出了3个常见的问题,我们这边逐一来看。
跑马灯问题
TextView对跑马灯的要求比较高,必须是单行文本,而且必须设置MaxLines,而且不支持Lines设置,另外必须是focused或者是selected,这显然增加了一些成本,要知道如果父布局focused,那么子View是不可能focused,显然对TV设备不够有好。但是另外一个问题,View可以同时具备Focused和Selected状态,这显然增加了问题的难度,为此我们需要剥离focused状态。
1 | scss复制代码 private void startMarquee() { |
那么,这里我们通过优化,使其仅在selected状态具备跑马灯,当然,如果你还想用selected状态实现其他用途,显然是无法使用了,不过系统中还有setEnable、setActivated状态供大家使用。
下面是跑马灯兼容逻辑
1 | java复制代码public class MarqueeTextView extends AppCompatTextView { |
频繁触发requestLayout
TextView很容易触发requestLayout,除非长宽必须是固定大小的,不过固定大小可能遇到文本展示的不全的问题,另外Google也提供了PrecomputedText异步测量文本的方式去优化性能,但是requestLayout造成的性能问题实际上比PrecomputedText测量要高,另外PrecomputedText编码方式也不够方便。
那么有没有更好的方法去抑制requestLayout的频繁调用呢?
实际上单行文本的使用远超多行文本,即便是播放器时间进度也是单行文本,因此我们可以自行测量单行文本,比较前后的尺寸差异,选择性调用requestLayout。
方式很多,这里我们利用BoringLayout优化,当然在android 5.0之前的版本BoringLayout 兼容性并不好,因此这里还引入StaticLayout进行兜底。
优化setText
构建Layout,优先是BoringLayout,我们前面说过,android 5.0之前的BoringLayout的兼容性不好,有些语言会转为StaticLayout兼容,特别需要注意的是,android 4.4 之前的版本,使用StaticLayout 时mLineSpacingMult参数必须大于0,否则测量出的大小会不正确。
1 | java复制代码 protected Layout buildTextLayout(CharSequence text, int wantWidth) { |
设置文本,通过setText减少requestLayout触发的机率,主要做了以下几件事
- 判断文本是否相同,如果相同则无需调用requestLayout,而TextView受限于需要支持Spanned,显然这点很难做到。
- 判断是否AttachedToWindow,如果没有,这里也不需要测量,在onMeasure中进行测量。
- 判断文本是否已测量过,如果没有则立即测量
- 构建TextLayout,比较文本宽高,相同时只调用invalidate,不同时需要调用requestLayout
1 | java复制代码 public void setText(final CharSequence text) { |
性能优化完整代码
下面是BoringTextView完整代码,通过自定义View实现了setText性能优化,当然你可能会想,这种写法缺少了一些TextView的属性,实际上这些属性是可以自行解析的,按照代码中的规则进行即可,特别要属性的大小顺序。
性能问题:
我们知道文本宽高的测量必然消耗性能,这是个显而易见的问题,不过,文本测量的性能损耗是要小于requestLayout造成的损耗,因为后者也会测量。
1 | java复制代码public class BoringTextView extends View { |
下面,我们进行一下性能测试,之前测过但是没往博客里添加,不过评论区有同学要求,这个要求并不过分,写性能优化方面的还得实事求是才能解决问题。
第一类性能测试
我们这里设计一下测试方案:
- 定时展示时间,测试20次
- 每500ms刷新一次
- 测量时直接测量子类的方法
- View方法测量使用纳秒
- 计算平均耗时
测试代码如下
1 | java复制代码Runnable r = new Runnable() { |
测试结果
优化前(使用TextView):
onMeasure=223249.75ns onDraw=65753.3ns setText=588362.94ns
优化后(使用BoringTextView):
onMeasure=50216.9ns onDraw=25258.2ns setText=170792.8ns
我们看到,对于频繁刷新的情况,BoringTextView性能明显好于TextView,比如我们播放器的进度刷新场景(一般是500-800ms刷新一次),这种收益很明显。
当然,上面是单个View,如果从整体View树中测量,那么嵌套越深,理论上BoringTextView性能收益会越明显。
第二类性能测试
不过,你可能会疑问,如果测试间隔扩大至1秒以上,性能会怎么样,实际上BoringTextView的原理是如下
- 相同文本不刷新
- 文本长度一样不调用requestLayout
- 减少Spannable造成的性能问题
显然,上面的优化没有包含长短变化的考虑,但是如果文本长短变化频繁的场景,那么onmeasure也会多次调用,另外,在实际开发中,还有一些系统字体也会引发长短变化问题,BoringTextView实际上也提供了优化方法
1 | java复制代码public void setLockMaxWidth(boolean lockMaxWidth) { |
我们设置isLockMaxWidth = true进行测试
测试结果
优化前:onMeasure=299225.2 onDraw=88330.8 setText=635284.4
优化后:onMeasure=95652.8 onDraw=56671.3 setText=408131.2
以上就是测试结果,显然,自定义TextView比原生的TextView性能可以更好。
犬牙问题:
这种问题的解决方法网上能搜出很多,但是对中文支持最好的得参考下面文章
《关于TextView中换行后对齐问题》
其中实现原理是对TextView重写,但是缺点是对英文支持的不够好,不过关系不大,对英文分词即可快速实现。
其核心逻辑是:对最后一行的以外的其他文本行增加文字间距(word space),从而使得看起来犬牙的文本显的规整,但其本身并非是两边对齐。
1 | java复制代码 private void drawScaledText(Canvas canvas, int lineStart, String line, |
emoji全角字符展示问题
我们来试一下效果,我们设置一段下面的文字
1 | java复制代码"Good Morning ! 😂来,今天是一个美妙的日子,我们一起走在大街上,唱着 |
效果预览,很遗憾,表情符没有展示出来
这个问题是评论区有同学提出来的,原则上说展示表情符不在本篇范畴,既然提出来了自然需要解决一下,提供一个思路,以后可以解决类似的问题。
UnicodeBlock
在Android中,为了识别各个国家的文字编码,在Character类中定义了很多类似的常量。通常意义上,我们判断字符编码的范围的也是可以的,但是字符编码范围的坏处就是需要查找范围,而且还存在不准确的情况。其实最简单的方法还是使用UnicodeBlock来判断
下面我们来打印一句话的范围
1 | java复制代码G - BASIC_LATIN |
显然,我们发现emoji占2个char code 位置,分别是: HIGH_SURROGATES和LOW_SURROGATES,实际上UnicodeBlock内置了EMOJIICONS,但是这里并没有命中,说明这里的解析规则无法匹配。不过,这也不是无法修复,我们完全可以在Canvas绘制的时候,将2个字符当作一个字符绘制或者按行绘制,就可以解决此问题。
按行绘制
我们知道,Paint#setWordSpacing和Paint#setLetterSpacing可以调整文字间距。
canvas按行绘制是可以解决此问题的,但是我们要处理对齐必然要增大文字间的空格,遗憾的是paint#setWordSpacing只有高版本支持。另一个问题,如果使用paint#setLetterSpacing,会导致英文字符不够紧凑,更严重的是单位是EM,如果按16px = 1EM 转换,会发现兼容性存在一些问题。因此这种方案是不可行的。
两个字符合并为一个
这个是可行的,但是可能存在不周全的问题,目前来说,我们使用这种方法解决了emoji和全角字符展示的问题,后续如有其他需求可以在评论区提出。
问题修复代码
1 | java复制代码int width = getWidth(); |
修复后的效果
完整代码
到这里我们实现了两端尽可能对齐,避免犬牙问题,同时解决了emoji展示问题
1 | java复制代码public class TextAlignTextView extends AppCompatTextView { |
总结
到这里本篇就结束了,TextView作为Android中最复杂的View组件之一,其中有很多方法的调用也是非公开的,另外其中的Editor也是没有公开的,这显然是造成TextView存在性能问题的原因之一。
本篇这里的优化基本都有线上使用,在播放器页面中,用到了BoringTextView和跑马灯效果,有效降低了焦点问题和requestLayout频繁的问题,当然文本的展示并不一定非得用BoringLayout和StaticLayout,也有很多方式可以实现此类优化。文本对齐问题,实际上在一些协议页面和小说页面使用会获得很好的体验,这里我们就不再赘述了。
本文转载自: 掘金