摺疊文字控制元件FoldTextView
阿新 • • 發佈:2022-04-03
說明
本來使用這個專案,但裡面有個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 } } }