Android TextView用"查看更多"替代自带ellipsize

效果

这篇文章能够帮你实现下面的效果。末尾固定显示“查看更多”,或者任何你自定义的文字,并且高效实现了一个展开折叠动画。 java

效果图
效果动图参考这里: github.com/aesean/Acti…

不关心原理,只须要实现的参考这里: TextViewExtensionsgit

使用示例: ShowMoreAnimationActivity,或者参考下面的使用示例github

Kotlin用法算法

TextViewSuffixWrapper(textView).apply wrapper@{
            this.mainContent = getString(R.string.sample_text)
            this.suffix = "...查看更多"
            this.suffix?.apply {
                suffixColor("...".length, this.length, R.color.md_blue_500, listener = View.OnClickListener { view ->
                    toast("click ${this}")
                })
            }
            this.transition?.duration = 5000
            sceneRoot = this.textView.parent.parent.parent as ViewGroup
            collapse(false)
            this.textView.setOnClickListener {
                toast("click view")
                this@wrapper.toggle()
            }
        }
复制代码

Java版用法app

TextViewSuffixWrapper wrapper = new TextViewSuffixWrapper(textView);
        wrapper.setMainContent("mainContent");
        String suffix = "...查看更多";
        wrapper.setSuffix(suffix);
        wrapper.suffixColor("...".length(), suffix.length(), R.color.colorAccent, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });
        wrapper.collapse(false);
        
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                wrapper.toggle();
            }
        });
复制代码

产生问题

Android的TextView支持最大行数,好比你但愿一个TextView最大行数是2行。ide

textView.maxLines = 2
复制代码

这时候textView最多显示两行。这时候你能够经过设置ellipsize,让文字超过两行的时候用“...”代替最后几个字。设置以后,TextView依然只有两行。性能

textView.maxLines = 2
    textView.ellipsize = TextUtils.TruncateAt.END
复制代码

那问题来了,我以为...的提示不够明显,我想换成相似查看更多这样的文字怎么办?并且我但愿给查看更多加上颜色,怎么办?好比显示成文章开头第一个图。这个需求其实仍是很是常见的,Android原生并提供相似的实现,那咱们只能本身动手了。优化

分析问题

咱们用下面的文本做为textView要显示的文本,咱们把这段文字取个名字叫:mainContent动画

"欧阳峰(独白):离开白驼山以后,我去了这个沙漠,开始了另外一种生活。欧阳峰(独白):初六日,惊蛰。每一年这个时候,都会有一我的来找我喝酒,他的名字叫黄药师。这我的很奇怪,每次总从东边而来,这习惯已经维持了好多年。今年,他给我带了一份手信。黄药师:不久前,我赶上一我的,送给我一坛酒,她说那叫"醉生梦死",喝了以后,能够叫你忘掉以作过的任何事。我很奇怪,为何会有这样的酒。她说人最大的烦恼,就是记性太好,若是什么均可以忘掉,之后的每一天将会是一个新的开始,那你说这有多开心。这坛酒原本打算送给你的,看起来,咱们要分来喝了。欧阳峰(独白):对于太古怪的东西,我向来很难接受,因此这坛"醉生梦死"我一直没有喝。可能这酒真的有效,从那天晚上开始,黄药师开始忘记了不少事情。ui

要显示成下面这个图的效果,咱们能够把问题拆成两部分。

效果图

  • mainContent中找选取多少个字的时候,后面加上...查看更多能够恰好是两行,并且再多加一个字都会致使后面加上...查看更多会换行。
  • 改变最后的查看更多的颜色为蓝色

解决问题

问题已经被抽象出来了。先看第二个问题,改变文字颜色,这个简单,咱们经过SpannableStringBuilder配合ForegroundColorSpan很容易实现。但第一个问题,找前面到底放多少个字,加上后面的后缀恰好能够放两行,这个怎么处理?

假如咱们有个方法能知道放若干个字后的TextView行数,那咱们假如从mainContent里找到这样一个index,可以知足下面的条件

textView.text = mainContent.substring(0, index) + "...查看更多"
    // textView.lineCount == 2

    textView.text = mainContent.substring(0, index+1) + "...查看更多"
    // textView.lineCount == 3
复制代码

(0, index)放到TextView,textView行数是2,(0, index+1)放到TextView,textView行数是3。那么这个index其实就是咱们要到的mainContent应该截断的位置。少一个字或者多一个字都不对。那TextView有提供获取行数的方法吗?

有提供,咱们能够经过下面的方法

textView.text = "111"
    val lineCount = textView.layout.lineCount // lineCount == 1
复制代码

并且这个方法能够同步获取到lineCount,简单说就是只要能获取到layout,那么setText后能够马上读取到lineCount,不须要等待下一次消息循环。那么咱们能够简单的经过下面的代码找到恰好能放下的index。

fun findIndex(textView: TextView, mainContent: CharSequence, suffix: CharSequence): Int {
        for (i in mainContent.length downTo 1) {
            textView.text = mainContent.subSequence(0, i).toString() + suffix.toString()
            val lineCount0 = textView.layout.lineCount
            textView.text = mainContent.subSequence(0, i + 1).toString() + suffix.toString()
            val lineCount1 = textView.layout.lineCount
            if (lineCount0 == 2 && lineCount1 == 3) {
                return i
            }
        }
        return -1
    }
复制代码

逻辑很是简单,一个个去试,直到找到多一个都放不下的index。

优化

到这里已经能够很是粗略的实现出这样效果的demo了,但实际使用还会有很是多问题,好比layout并非总能拿到,onCreate中setText后拿到的layout是null,须要等到TextView的onMeasure以后才能生成layout。另外layout过程是很消耗资源的,我能不能把layout过程放在子线程?还有这一个个文本的尝试实在是有点蠢,有没有更优化的算法?

onCreate中setText后getLayout是null怎么办?

查看TextView源码,咱们能够发现layout第一次被建立是在onMeasure里,那咱们在onCreate里setText后固然是拿不到Layout的,那怎么办?个人作法是判断到当Layout为null的时候添加LayoutChangeListener,在LayoutChangeListener中从新拿Layout。

layout过程很消耗资源,咱们能不能本身new一个Layout,而后把测量过程放子线程?并且setText这么屡次会影响TextChangedListener,怎么办?

若是你用过新TextView的autoTextSize,若是用过的话,你可能会发现这个功能在AppCompatTextView中作了低版本兼容实现。里面有个类叫AppCompatTextViewAutoSizeHelper,经过反射等一系列方法,新建立了一个StaticLayout出来,而后经过新建立的这个StaticLayout配合二分查找来计算最合适的文字大小。

那咱们能不能模仿AppCompatTextViewAutoSizeHelper的实现,咱们也new一个StaticLayout出来?new一个StaticLayout出来没什么问题,但问题是,TextView并不老是使用StaticLayout来layout文本,好比咱们须要改变查看更多的颜色,同时须要能点击查看更多,那么咱们会用到SpannableString,这时候TextView会使用DynamicLayout来layout文本,不一样的Layout得出的layout结果有多是不一样的。那么咱们就须要不光new一个StaticLayout,还须要在合适的时候new一个DynamicLayout出来,这一系列的缘由会让这个问题复杂化。最后我放弃了new一个新DynamicLayout出来。而是直接利用TextView本身的layout,这样能大大下降问题复杂性,但带来的问题就是不能在子线程测量,同时因为会屡次setText,那么就会致使由于屡次TextChangedListener回调。

一个个尝试实在是太蠢了,怎么优化?

优化很简单,直接二分查找。二分的过程仍是比较简单的,具体如何二分这里不详细介绍了,能够在logcat中过滤关键字TextViewLayout,在debug级别的日志中能够看到整个二分查找过程。

添加动画

既然解决了如何折叠,那么你可能也会须要展开。展开很是简单,直接把全部文本设置上去就能够了。若是须要加动画怎么办?能够用Animator吗?或者Animation?

无论用Animator仍是Animation,动画过程当中,须要改变的是什么?是高度,但问题来了,没动以前我怎么知道要动到什么高度?这是个很是棘手的问题。

好在Google给咱们提供了另一个实现动画的方式,TransitionManager

TransitionManager.beginDelayedTransition(sceneRoot, transition)
复制代码

sceneRoot是你但愿动画的目标View,transition是动画方式,transition一般咱们默认使用AutoTransition就能够了。那为啥TransitionManager能这么动画呢?主要是下面一行代码

sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener)
复制代码

这个动画性能很是好,它在改变高度宽度等动画的时候,只触发一次layout,能够大大下降性能消耗,更详细具体的原理回头有空了再分析。

结束

整个过程就介绍结束了,有什么其余疑问欢迎给我留言评论。

相关文章
相关标签/搜索