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

2020-03-14  本文已影响0人  PeytonWu

前言

项目中个人负责的多个列表页用到类似微博及小红书如下图的这种超过缩进行数文末添加" ...全文" 展开的控件。在页面的优化同城中,通过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 三个工具类

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

细节及注意点

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

有了以上思路后,根据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中使用,一次滑动及来回操作便会调用反复调用多次,积累起来便很可观。

尾言

上一篇下一篇

猜你喜欢

热点阅读