Android记一次优化文字缩进控件的坑

前言

项目中个人负责的多个列表页用到类似微博及小红书如下图的这种超过缩进行数文末添加" ...全文" 展开的控件。在页面的优化同城中,通过systrace跟踪发现项目中该自定义控件有个方法会反复调用多次(具体以下详解) ,本以为这种控件应该是有比较成熟的解决方案,于是github一顿搜索,唯一一个星数上千的库就是ExpandableTextView,查看后实现原理是:2个控件不是span的形式添加到textview尾部,然后获取4行时textview需显示的高度,按钮点下时动画控制view的height属性;且并无文末添加“...全文”的功能,与需求不符。无奈只能自己动手,优化现有控件。

image.png

image.png

思路及原理

  1. 发现其实这个效果与 TextView 设置 android:maxLines 之后,再设置 android:ellipsize 为 end 很相似,只是 … 替换换成了 …展开 ,遗憾的是系统并没有提供直接替换 … 的API。

但是,在涉及到 android:ellipsize 属性处理的 TextView 的源码中可以看到使用了 StaticLayout 了一个可以帮助我们实现效果的工具类 StaticLayout,StaticLayout 是android中处理文字换行的一个工具类。

有BoringLayout、StaticLayout 和 DynamicLayout 三个工具类

  • BoringLayout 是单行显示时使用的
  • StaticLayout 是针对不可以变的文本(不同系统版本构造方式不大一样,)
  • DynamicLayout 则是针对可编辑改变的文本,并且会更新自身。
    具体这些类及方法本文不详细展开,有兴趣的同学可自行查看相关文档

于是得出最终方案
2:动态截取文字,加上“...全文”后刚好撑满缩进,然后将新的CharSequence设入textview即可。

细节及注意点

  • 一定要先测量一次如果本身文字行数就不会超过锁进行数,则什么都不要再做任何处理,浪费性能,直接走textview的方法即可(需求上也是如此)
  • 记录一份原数据,如果有些地方是显示的是"...展开",点击后的效果是直接展开显示全文的话
  • 新文字设置进来时,对比下当前记录的原数据与设入的新数据是否一致,一致则不再做多余处理,算是性能的优化。还取决于控件实现方式,像原项目中控件的处理,是在onDraw方法(当然做了其它限制,保证每次更改文字只触发一次,不能每次ondraw都去触发,否则性能就废了)时才取拦截,因为onDraw方法已经在measure和layout方法之后,如果不做该操作当控件放在listview或recyclerview列表中,当notify或者滑动操作时,会造成先高度测量差异而抖动一下。

项目中多次调用的方法优化

有了以上思路后,根据systrac显示,多次调用耗时,跟踪项目中调用多次的方法,发现他是一个循环,一直去尝试截取原文中的不同长度的文字去与“...全文”拼成后刚好布满指定缩进行数的文字。

一开始看到此处一脸懵逼,为啥要一直循环遍历去尝试,而不是直接先通过StaticLayout.getLineEnd方法,直接获取缩进行数的末尾offset,然后截取原文字,再对这个截取后的文字,删减“...全文”的长度,最后再将这个删减后的文字拼接上"...全文",不就是我们想要的最终结果,不就可以了
大概如下

...
 int lineEnd = getLayout().getLineEnd(mCollapsedLines - 1);
 CharSequence suffix = "...全文";
 int newEnd = lineEnd - suffix.length() - 1;
int end = newEnd > 0 ? newEnd : lineEnd;
CharSequence finalSequence = note.subSequence(0, end);
...

然而,实际结果令人啪啪打脸[捂脸哭],会有的还有间距,有的超过。因为漏了一个重要因素,就是同样length的文字,在绘制时所占用的宽度不一定一致。

各种搜索网上其它大佬的解决方案,也都是只能遍历一直去尝试,看截到多少能刚好铺满。

1:

TextPaint paint = getPaint();
int maxWidth = mCollapsedLines * (getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
while (paint.measureText(note.substring(0, end) + suffix) > maxWidth)
    end--;
note = note.substring(0, end);

而且这代码还有个问题就是忽律了各种span长度问题
2:ExpandableText-Example

  //计算原文截取位置
                int endPos = layout.getLineEnd(maxLines - 1);
                if (originalText.length() <= endPos) {
                    mCloseSpannableStr = charSequenceToSpannable(originalText);
                } else {
                    mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos));
                }
                SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
                if (mOpenSuffixSpan != null) {
                    tempText2.append(mOpenSuffixSpan);
                }
                //循环判断,收起内容添加展开后缀后的内容
                Layout tempLayout = createStaticLayout(tempText2);
                while (tempLayout.getLineCount() > maxLines) {
                    int lastSpace = mCloseSpannableStr.length() - 1;
                    if (lastSpace == -1) {
                        break;
                    }
                    if (originalText.length() <= lastSpace) {
                        mCloseSpannableStr = charSequenceToSpannable(originalText);
                    } else {
                        mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
                    }
                    tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
                    if (mOpenSuffixSpan != null) {
                        tempText2.append(mOpenSuffixSpan);
                    }
                    tempLayout = createStaticLayout(tempText2);

                }

还有其它多个库,不一一链接,区别在于如何去测量,如果去逼近求出最终字符串而已。所以现在能优化的重点就在于,如何尽量地去减少遍历的次数。
上方库的方法比较简单,也与项目中用到的方法类似。即:通过StaticLayout.getLineEnd方法,直接获取缩进行数的末尾offset,然后截取原文字,直接拼接上"...全文"span,然后依次往前递减字符去逼近。
项目中的是直接全字段二分查找去逼近,以上开源库方法做为备用方案。经试验,在文字长度不是很长时,效率比备用方法高不少;当文字长度过长时,备用方法则优势明显。
其实还可以进一部优化,即二分查找法的起始位置不要全字串二分,从 截取后的文字,删减“...全文”的长度,开始到最后拼接上的 这个小范围去二分查找。
优化后打log方法及效果如下:

         //优化前方式
        CharSequence destStr = tailorText(text,false);
        long newEnd = System.currentTimeMillis();
        //优化后方式
        CharSequence destStrNewMethod = tailorText(text,true);
        long newEnd2 = System.currentTimeMillis();
        Log.d(TAG, ("oldMethod--->"+(newEnd - startTime))+"|NewMethod="+(newEnd2-newEnd) + "ms");
image.png

这几毫秒的时间在一个布局中并无关紧要,但因为项目中是放在listview及recyclerview中使用,一次滑动及来回操作便会调用反复调用多次,积累起来便很可观。

尾言

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

推荐阅读更多精彩内容

  • 1.badgeVaule气泡提示 2.git终端命令方法> pwd查看全部 >cd>ls >之后桌面找到文件夹内容...
    i得深刻方得S阅读 4,525评论 1 9
  • 最近产品汪和运营商讨下来决定要做商品促销活动,然后设计妹子给到最终的效果图。 第一感觉就是 so so easy ...
    lovejjfg阅读 13,478评论 6 32
  • 一六年第一天,没有回家,在杭州,没有一个人陪,自己一个人过。 大学同学,小学同学,一个个陆陆续续结婚生子,自己一点...
    finityho阅读 154评论 0 0
  • 店长要学会怎么去分解指标,怎样去把没完成的指标分解到其他时间里。
    23aae7e9bc8e阅读 41评论 0 0
  • 01 星期三晚上刚放学,陈曼就迫不及待地给男友苏闯打了个电话,问他星期天能不能前来观看她的演出。 苏闯吞吞吐吐,说...
    谁曾遇见风阅读 912评论 7 13