---
# System prepended metadata

title: ExpandTextView
tags: [View, Android, Kotlin]

---

###### tags: `Android` `Kotlin` `View`
# ExpandTextView

有時候我們會希望元件預設的規模是小的，當使用者想更了解內容再顯示出來
例如留言訊息的文字很多，但使用者不一定想全盤接受，而是想要的再打開來看
那這樣的情境該怎麼做呢? 就是打造一個可以擴展的TextView，以下將介紹如何實現

## 創建類別 ExpandTextView
```kotlin=
class ExpandTextView : AppCompatTextView {
    /**true：展開 false：收起 */
    private var expandState: Boolean = false
    var mCallback: Callback? = null

    var ellipsizeText = ""

    var expandText = "展開"
    var expandTextColor: Int = Color.parseColor("#1C7FFD")

    var collapseText = "收起"
    var collapseTextColor: Int = Color.parseColor("#1C7FFD")
    var collapseEnable = false
    var underlineEnable = false

    constructor(context: Context): super(context)

    constructor(context: Context, attributes: AttributeSet): super(context, attributes)

    constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int): super(context, attributes, defStyleAttr)

    init {
        movementMethod = LinkMovementMethod.getInstance()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 文字計算輔助工具
        if (text.isEmpty())
            setMeasuredDimension(measuredWidth, measuredHeight)

        val sl = StaticLayout.Builder.obtain(text, 0, text.length, paint, measuredWidth - paddingLeft - paddingRight)
            .setAlignment(Layout.Alignment.ALIGN_CENTER)
            .setLineSpacing(0f, 1f)
            .setIncludePad(true).build()
        // 總行數
        var lineCount = sl.lineCount
        if (lineCount > maxLines) {
            if (expandState) {
                //收起功能
                if (collapseEnable) {
                    // 收起文案和源文字组成的新的文字
                    val newEndLineText = text.toString() + collapseText
                    val spannableString = SpannableString(newEndLineText)

                    spannableString.setSpan(object : ClickableSpan() {
                        override fun onClick(widget: View?) {
                            mCallback?.onCollapseClick()
                        }
                        
                        // 設定下畫線
                        override fun updateDrawState(ds: TextPaint) {
                            super.updateDrawState(ds)
                            ds.isUnderlineText = underlineEnable
                        }
                    }, newEndLineText.length - collapseText.length, newEndLineText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

                    spannableString.setSpan(ForegroundColorSpan(collapseTextColor), newEndLineText.length - collapseText.length, newEndLineText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    text = spannableString
                }

                mCallback?.onExpand()
            } else {
                lineCount = maxLines
                // 省略文字和展開文案的寬度
                val dotWidth = paint.measureText(ellipsizeText + expandText)
                // 找出顯示最後一行的文字
                val start = sl.getLineStart(lineCount - 1)
                val end = sl.getLineEnd(lineCount - 1)
                val lineText = text.substring(start, end)
                // 將最後一行最後文字替換成 ellipsizeText和expandText
                var endIndex = 0
                for (i in lineText.length - 1 downTo 0) {
                    val str = lineText.substring(i, lineText.length)
                    // 找出文字寬度大於 ellipsizeText 的字符
                    if (paint.measureText(str) >= dotWidth) {
                        endIndex = i
                        break
                    }
                }
                // 新的文字
                val newEndLineText = text.substring(0, start) + lineText.substring(0, endIndex) + ellipsizeText + expandText
                val spannableString = SpannableString(newEndLineText)
                //展開文字監聽
                spannableString.setSpan(object : ClickableSpan() {
                    override fun onClick(widget: View?) {
                        mCallback?.onExpandClick()
                    }
                    
                    //設定下畫線
                    override fun updateDrawState(ds: TextPaint) {
                        super.updateDrawState(ds)
                        ds.isUnderlineText = underlineEnable
                    }
                }, newEndLineText.length - expandText.length, newEndLineText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

                spannableString.setSpan(expandTextColor, newEndLineText.length - expandText.length, newEndLineText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                // 最终文字
                text = spannableString

                mCallback?.onCollapse()
            }
        } else
            mCallback?.onLoss()
        // 重新計算高度
        var lineHeight = 0
        for (i in 0 until lineCount) {
            val lineBound = Rect()
            sl.getLineBounds(i, lineBound)
            lineHeight += lineBound.height()
        }
        lineHeight = (paddingTop + paddingBottom + lineHeight * lineSpacingMultiplier).toInt()
        setMeasuredDimension(measuredWidth, lineHeight)
    }

    /**
     * 設置文字與顯示狀態
     * @param text
     * @param expanded true：開啟、false：收起
     * @param callback
     */
    fun setText(text: String, expanded: Boolean, callback: Callback?) {
        expandState = expanded
        mCallback = callback
        // 設置文字(必要)，否則onMeasure寬度測量不正確
        setText(text)
    }

    /**
     * 展開收起狀態變化
     * @param expanded
     */
    fun setChanged(expanded: Boolean) {
        expandState = expanded
        requestLayout()
    }
}

interface Callback {
    /**
     * 展開狀態
     */
    fun onExpand()

    /**
     * 收起狀態
     */
    fun onCollapse()

    /**
     * 行数小于最小行数，不满足展开或者收起条件
     */
    fun onLoss()

    /**
     * 觸發展開
     */
    fun onExpandClick()

    /**
     * 觸發收起
     */
    fun onCollapseClick()
}
```

## 使用方式
```kotlin=
//需要在xml設定最大行數
holder.tv_msg.ellipsizeText = "..." //省略文字
holder.tv_msg.expandText = context.resources.getString(R.string.continue_read) //擴展的顯示文字
holder.tv_msg.expandTextColor = context.resources.getColor(R.color.colorAccent, null) //擴展文字顏色
holder.tv_msg.setText(getItem(position).content, false, null) //設定TextView文字及是否預設為擴展狀態
holder.tv_msg.underlineEnable = true //設定擴展與收起文字是否要有底線
//監聽事件
holder.tv_msg.setText("", false, object : Callback {
    override fun onCollapse() {}
    override fun onExpand() {}
    override fun onLoss() {}
    override fun onCollapseClick() {}
    override fun onExpandClick() {}
})
```

> https://github.com/yinjinyj/ExpandTextView/blob/master/library/src/main/java/com/yinjin/expandtextview/ExpandTextView.kt