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版用法markdown

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行。app

textView.maxLines = 2
複製代碼

這時候textView最多顯示兩行。這時候你能夠經過設置ellipsize,讓文字超過兩行的時候用「...」代替最後幾個字。設置以後,TextView依然只有兩行。ide

textView.maxLines = 2
    textView.ellipsize = TextUtils.TruncateAt.END
複製代碼

那問題來了,我以爲...的提示不夠明顯,我想換成相似查看更多這樣的文字怎麼辦?並且我但願給查看更多加上顏色,怎麼辦?好比顯示成文章開頭第一個圖。這個需求其實仍是很是常見的,Android原生並提供相似的實現,那咱們只能本身動手了。oop

分析問題

咱們用下面的文本做爲textView要顯示的文本,咱們把這段文字取個名字叫:mainContent性能

"歐陽峯(獨白):離開白駝山以後,我去了這個沙漠,開始了另外一種生活。歐陽峯(獨白):初六日,驚蟄。每一年這個時候,都會有一我的來找我喝酒,他的名字叫黃藥師。這我的很奇怪,每次總從東邊而來,這習慣已經維持了好多年。今年,他給我帶了一份手信。黃藥師:不久前,我趕上一我的,送給我一罈酒,她說那叫"醉生夢死",喝了以後,能夠叫你忘掉以作過的任何事。我很奇怪,爲何會有這樣的酒。她說人最大的煩惱,就是記性太好,若是什麼均可以忘掉,之後的每一天將會是一個新的開始,那你說這有多開心。這壇酒原本打算送給你的,看起來,咱們要分來喝了。歐陽峯(獨白):對於太古怪的東西,我向來很難接受,因此這壇"醉生夢死"我一直沒有喝。可能這酒真的有效,從那天晚上開始,黃藥師開始忘記了不少事情。優化

要顯示成下面這個圖的效果,咱們能夠把問題拆成兩部分。

效果圖

  • 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,能夠大大下降性能消耗,更詳細具體的原理回頭有空了再分析。

結束

整個過程就介紹結束了,有什麼其餘疑問歡迎給我留言評論。

相關文章
相關標籤/搜索