这篇文章能够帮你实现下面的效果。末尾固定显示“查看更多”,或者任何你自定义的文字,并且高效实现了一个展开折叠动画。 java
不关心原理,只须要实现的参考这里: 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过程放在子线程?还有这一个个文本的尝试实在是有点蠢,有没有更优化的算法?
查看TextView源码,咱们能够发现layout第一次被建立是在onMeasure里,那咱们在onCreate里setText后固然是拿不到Layout的,那怎么办?个人作法是判断到当Layout为null的时候添加LayoutChangeListener,在LayoutChangeListener中从新拿Layout。
若是你用过新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,能够大大下降性能消耗,更详细具体的原理回头有空了再分析。
整个过程就介绍结束了,有什么其余疑问欢迎给我留言评论。