1. 程式人生 > 其它 >摺疊文字控制元件FoldTextView

摺疊文字控制元件FoldTextView

說明

本來使用這個專案,但裡面有個bug,修復一下,特此記錄。

屬性

    <declare-styleable name="FoldTextView">
        <attr name="showMaxLine" format="integer" />
        <attr name="tipGravity" format="integer" />
        <attr name="tipColor" format="reference|color" />
        <attr name="tipClickable" format="boolean" />
        <attr name="foldText" format="string" />
        <attr name="expandText" format="string" />
        <attr name="showTipAfterExpand" format="boolean" />
        <attr name="isSetParentClick" format="boolean" />
    </declare-styleable>

實現

class FoldTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) :
    AppCompatTextView(context, attrs, defStyle) {
    companion object {
        val ELLIPSIZE_END = "..."
        val MAX_LINE = 4
        val EXPAND_TIP_TEXT = "收起全文"
        val FOLD_TIP_TEXT = "檢視全文"
        val TIP_COLOR = -0x1
        val END = 0
    }

    var logEnable = false

    /**
     * 顯示最大行數
     */
    var mShowMaxLine: Int = 0

    /**
     * 摺疊文字
     */
    var mFoldText: String = ""

    /**
     * 展開文字
     */
    var mExpandText: String = ""

    /**
     * 原始文字
     */
    var mOriginalText: String = ""

    /**
     * 是否展開
     */
    var isExpand = false
        set(value) {
            if (field != value) {
                field = value
                if (!field) {
                    isOverMaxLine = false
                }
                text = mOriginalText
            }
        }

    /**
     * 全文顯示的位置 0末尾 1下一行
     */
    var mTipGravity = 0

    /**
     * 提示文字顏色
     */
    var mTipColor: Int = 0

    /**
     * 提示是否可點選
     */
    var mTipClickable = false
    var flag = false
    var mPaint: Paint = Paint()

    /**
     * 展開後是否顯示文字提示
     */
    var isShowTipAfterExpand = false

    /**
     * 提示文字座標範圍
     */
    var minX: Float = 0f
    var maxX: Float = 0f
    var minY: Float = 0f
    var maxY: Float = 0f

    /**
     * 收起全文不在同一行時,增加一個變數記錄座標
     */
    var middleY: Float = 0f

    /**
     * 原始文字行數
     */
    var originalLineCount = 0

    /**
     * 是否超過最大行數
     */
    var isOverMaxLine = false

    /**
     * 點選時間
     */
    var clickTime = 0L

    init {
        mShowMaxLine = MAX_LINE
        if (attrs != null) {
            val arr = context.obtainStyledAttributes(attrs, R.styleable.FoldTextView)
            mShowMaxLine = arr.getInt(R.styleable.FoldTextView_showMaxLine, MAX_LINE)
            mTipGravity = arr.getInt(R.styleable.FoldTextView_tipGravity, FoldTextView.END)
            mTipColor = arr.getColor(R.styleable.FoldTextView_tipColor, FoldTextView.TIP_COLOR)
            mTipClickable = arr.getBoolean(R.styleable.FoldTextView_tipClickable, false)
            mFoldText = arr.getString(R.styleable.FoldTextView_foldText) ?: ""
            mExpandText = arr.getString(R.styleable.FoldTextView_expandText) ?: ""
            isShowTipAfterExpand =
                arr.getBoolean(R.styleable.FoldTextView_showTipAfterExpand, false)
            arr.recycle()
        }
        if (TextUtils.isEmpty(mExpandText)) {
            mExpandText = EXPAND_TIP_TEXT
        }
        if (TextUtils.isEmpty(mFoldText)) {
            mFoldText = FOLD_TIP_TEXT
        }
        if (mTipGravity == END) {
            mFoldText = "  " + mFoldText
        }
        mPaint.textSize = textSize
        mPaint.color = mTipColor
    }

    override fun setText(text: CharSequence?, type: BufferType?) {
        if (TextUtils.isEmpty(text) || mShowMaxLine == 0) {
            super.setText(text, type)
        } else if (isExpand) {
            //文字展開
            val spannable = SpannableStringBuilder(mOriginalText)
            if (isShowTipAfterExpand) {
                spannable.append(mExpandText)
                spannable.setSpan(
                    ForegroundColorSpan(mTipColor),
                    spannable.length - mExpandText.length,
                    spannable.length,
                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
                )
            }
            super.setText(spannable, type)
            val mLieCount = lineCount
            val layout = layout
            minX =
                paddingLeft + layout.getPrimaryHorizontal(spannable.lastIndexOf(mExpandText[0]) - 1)
            maxX =
                paddingLeft + layout.getPrimaryHorizontal(spannable.lastIndexOf(mExpandText[mExpandText.length - 1]) + 1)
            val bound = Rect()
            layout.getLineBounds(originalLineCount - 1, bound)
            if (mLieCount > originalLineCount) {
                //不在同一行
                minY = (paddingTop + bound.top).toFloat()
                middleY = minY + paint.fontMetrics.descent - paint.fontMetrics.ascent
                maxY = middleY + paint.fontMetrics.descent - paint.fontMetrics.ascent
            } else {
                //同一行
                minY = (paddingTop + bound.top).toFloat()
                maxY = minY + paint.fontMetrics.descent - paint.fontMetrics.ascent
            }

        } else {
            if (!flag) {
                viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
                    override fun onPreDraw(): Boolean {
                        viewTreeObserver.removeOnPreDrawListener(this)
                        flag = true
                        formatText(text, type)
                        return true
                    }
                })
            } else {
                formatText(text, type)
            }
        }
    }

    fun formatText(text: CharSequence?, type: BufferType?) {
        mOriginalText = text.toString()
        var l = layout
        if (l == null || !l.text.equals(mOriginalText)) {
            super.setText(mOriginalText, type)
            l = layout
        }
        if (l == null) {
            viewTreeObserver.addOnGlobalLayoutListener(object :
                ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        viewTreeObserver.removeOnGlobalLayoutListener(this)
                    }
                    translateText(layout, type)
                }
            })
        } else {
            translateText(l, type)
        }
    }

    private val TAG = "FoldTextView"

    fun log(msg: String) {
        if (logEnable) {
            Log.i(TAG, "log: $msg")
        }
    }

    fun translateText(l: Layout, type: BufferType?) {
        //記錄原始行數
        originalLineCount = l.lineCount
        log("lineCount:$originalLineCount,maxLine:$mShowMaxLine")
        if (l.lineCount > mShowMaxLine) {
            isOverMaxLine = true
            val span = SpannableStringBuilder()
            val start = l.getLineStart(mShowMaxLine - 1)
            var end = l.getLineVisibleEnd(mShowMaxLine - 1)
            if (mTipGravity == END) {
                val builder = StringBuilder(ELLIPSIZE_END).append("  ").append(mFoldText)
                end -= paint.breakText(
                    mOriginalText,
                    start,
                    end,
                    false,
                    paint.measureText(builder.toString()),
                    null
                )
            } else {
                end--;
            }
            val ellipsize = mOriginalText.subSequence(0, end)
            span.append(ellipsize).append(ELLIPSIZE_END)
            if (mTipGravity != END) {
                span.append("\n")
            }
            super.setText(span, type)
        }else{
            isOverMaxLine = false
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        log("onDraw:isOverMaxLine $isOverMaxLine ,isExpand$isExpand")
        if (isOverMaxLine && !isExpand) {
            //摺疊
            if (mTipGravity == END) {
                minX = width - paddingLeft - paddingRight - paint.measureText(mFoldText)
                maxX = (width - paddingLeft - paddingRight).toFloat()
            } else {
                minX = paddingLeft.toFloat()
                maxX = minX + paint.measureText(mFoldText)
            }
            minY = height - (paint.fontMetrics.descent - paint.fontMetrics.ascent) - paddingBottom
            maxY = (height - paddingBottom).toFloat()
            canvas?.drawText(
                mFoldText,
                minX,
                height - paint.fontMetrics.descent - paddingBottom,
                mPaint
            )
        }
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (mTipClickable) {
            when (event?.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    clickTime = System.currentTimeMillis()
                    if (!isClickable && isInRange(event.x, event.y)) {
                        return true
                    }
                }
                MotionEvent.ACTION_CANCEL,
                MotionEvent.ACTION_UP -> {
                    val delTime = System.currentTimeMillis() - clickTime
                    clickTime = 0L
                    if (delTime < ViewConfiguration.getTapTimeout() && isInRange(
                            event.x,
                            event.y
                        )
                    ) {
                        isExpand = !isExpand
                        text = mOriginalText
                        return true
                    }
                }
            }
        }
        return super.onTouchEvent(event)
    }

    private fun isInRange(x: Float, y: Float): Boolean {
        return if (minX < maxX) {
            //同一行
            x in minX..maxX && y in minY..maxY
        } else {
            //兩行
            x <= maxX && y in middleY..maxY || x >= minX && y in minY..middleY
        }
    }
}