自定義View:可伸展摺疊的ExpandTextView

前言

最近工作比較多,寫了幾個小控件跟大家分享一下。首先看圖:效果圖
這種可以顯示展開和摺疊的TextView非常常見,如果全文要當作一個按鈕處理的話,就沒辦法和文字混排,所以以前就和產品商量折中方案,就沒太細細研究這個效果。今天算是補上了。

正文

其實實現這個效果非常簡單,總共就100行代碼。

首先我們要解決的問題是按鈕和文字的混排問題,首選方案肯定是SpannableString,文字的大小,顏色,下劃線都可以定製,用它肯定沒毛病。剩下的就是字符串的截取,如果是摺疊我們就截取字符串的前幾行,再減去幾個字符給全文留出位置,展開就全部展示。

因爲要保存設置的文字,所以我們需要寫一個setText方法:

// 摺疊時顯示的最大行數
private var collapsedMaxLine = 0

// 文字
private var content: String = ""

// 是否需要顯示 全文/收起,不足collapsedMaxLine的行數,沒必要顯示
private var hasMore = false

fun setText(content: String) {
     this.content = content
     super.setText(content)
     // 重置狀態
     maxLines = Int.MAX_VALUE
     hasMore = false
}

字符串的截取就放在onMeasure中:

 var collapsed = true
      set(value) {
          if (field != value) {
              field = value
              requestLayout()
          }
	  }

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (!TextUtils.isEmpty(content)) {
            if (layout != null) {
            	// 如果現在的行數比摺疊的是否大,驗證是否可以展開
                if (layout.lineCount > collapsedMaxLine) {
                    hasMore = true
                }

				// 如果是摺疊
                if (collapsed) {
                	// 行數保持一致
                    if (layout.lineCount > collapsedMaxLine) {
                        maxLines = collapsedMaxLine
                        // layout可以返回指定的行數的最後一個字符的位置,如果不瞭解的話,後續會講
                        val endIndex = layout.getLineEnd(collapsedMaxLine - 1)
                        // 減去文字,拼接 ...全部後綴
                        super.setText(getCollapsedContent(endIndex))
                    }
                } else {
                	// 展開狀態 並且 可以展開
                    if (hasMore) {
                    	// 取消行數限制
                        maxLines = Int.MAX_VALUE
                        // 顯示全部內容,拼接 收起後綴
                        super.setText(getAllContent())
                        hasMore = false
                    }
                }
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

拼接全部和收起按鈕,需要可以點擊,這裏我們還得自定義一個ClickSpan:

private fun getAllContent(): SpannableStringBuilder {
        return SpannableStringBuilder(content)
            .apply {
                append("收起")
                setSpan(ClickableColorSpan(color = collapsedColor, underLine = collapsedUnderLine) {
                    collapsed = !collapsed
                }, length - 2, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
            }
    }


    private fun getCollapsedContent(endIndex: Int): SpannableStringBuilder {
        return SpannableStringBuilder(content.substring(0, endIndex - 7))
            .apply {
                append("...全文")
                setSpan(ClickableColorSpan(color = collapsedColor, underLine = collapsedUnderLine) {
                    collapsed = !collapsed
                }, length - 5, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
            }
    }


    class ClickableColorSpan(
        private val color: Int,
        private val underLine: Boolean,
        private val click: () -> Unit
    ) : ClickableSpan() {

        override fun updateDrawState(ds: TextPaint) {
            ds.color = color
            ds.isUnderlineText = underLine
            // 要注意這句代碼不可以打開,否則無法顯示設置的UI
//        super.updateDrawState(ds)
        }

        override fun onClick(widget: View) {
            click.invoke()
        }
    }

爲什麼ClickSpan的super.updateDrawState(ds)不可以打開,因爲這個方法會覆蓋我們的color和下劃線

@Override
public void updateDrawState(@NonNull TextPaint ds) {
    ds.setColor(ds.linkColor);
    ds.setUnderlineText(true);
}

最後千萬別忘了設置最重要的一步,TextView設置

init {
	// 不設置此方法,點擊無效
    movementMethod = LinkMovementMethod.getInstance()
}

到此效果完成。

神奇的Layout

在上面的代碼中,我們使用了TextView的一個屬性:layout。它在android.text包下,專門輔助TextView繪製文字的類。在上一次開發歌詞變色的功能中,發揮了非常重要的作用。

下面介紹一下Layout的常用方法:

// 返回某一行開始的文字的索引,如果line等於lineCount,會返回字符串的長度
public abstract int getLineStart(int line);
// 返回某一行最後一個字符的索引
public final int getLineEnd(int line) 
// 返回某一個距離頂部的top
public abstract int getLineTop(int line);
// 返回某一行左邊的間距
public float getLineLeft(int line)
// 返回某一行右邊的間距
public float getLineRight(int line)
// 返回某一行下面的間距
public final int getLineBottom(int line)
// 獲得某一行的寬度
public float getLineWidth(int line)// 返回某個位置所在的行數
public int getLineForOffset(int offset)

當TextView繪製文字後,我們可以通過Layout得到和文字相關的所有信息,只能說Layout真的是無敵。

總結

通過今天的小控件,主要還是想安利大家Layout這一神器,有了它處理更復雜的文字效果都不是問題,有時間大家可以研究研究。

GitHub地址:點擊查看源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章