亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android中TextView限制最大行數(shù)并在最后用顯示...全文

 更新時間:2022年04月25日 10:53:01   作者:笑慢  
TextView在android開發(fā)中是一個經(jīng)常用到的基礎(chǔ)控件,功能也很強大,限制輸入字符類型,字數(shù)什么的,下面這篇文章主要給大家介紹了關(guān)于Android中TextView限制最大行數(shù)并在最后用顯示...全文的相關(guān)資料,需要的朋友可以參考下

一、場景

我們知道通常在列表頁面會有很多內(nèi)容,而且每條內(nèi)容可能會很長,如果每條內(nèi)容都全部顯示用戶體驗就很不好。所以,我們通常的處理方案是限制每條內(nèi)容的行數(shù),這個時候如果想更加明顯的提示用戶該條內(nèi)容有更多的內(nèi)容,可以進入詳情頁查看時會在內(nèi)容最后加上“全文”之類的字眼。尤其是社區(qū)內(nèi)的APP里經(jīng)常會看到這樣的場景,比如:微博。

二、方案的實現(xiàn)

那如果我們想限制最大行數(shù)且在最后顯示...全文該怎么實現(xiàn)呢?我們知道我們通常設(shè)置TextView的最大行數(shù)是設(shè)置maxLines屬性,并設(shè)置android:ellipsize="end"表示在內(nèi)容最后顯示...。但是類似"全文“這樣的文字怎么顯示呢?我想這時大家肯定會想到:在內(nèi)容最后拼上去??!沒錯,是需要拼上去,那要怎么拼?怎么拼上去正好在內(nèi)容的最后,既不提前、又完整顯示”全文“?

1、”常規(guī)”方案

網(wǎng)上大多關(guān)于這個需求的實現(xiàn)方案都是在textView.setText()之后調(diào)用textView.post方法,偽代碼:

textView.post(new Runnable() {
            @Override
            public void run() {
                //進行內(nèi)容的截取和拼接
            }
     });

或者是設(shè)置addOnGlobalLayoutListener監(jiān)聽,偽代碼:

textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                    //進行內(nèi)容的截取和拼接
                }
            }
        });

其本質(zhì)和核心都是為了獲取內(nèi)容的行數(shù),來判斷是否大于我們想設(shè)置的最大行數(shù),來進行內(nèi)容的截取和”全文“的拼接。

但是該方案是在setText()之后進行的截取,也就是TextView已經(jīng)顯示了內(nèi)容然后再進行內(nèi)容的處理再次setText()。那么會有以下明顯的缺點:

1:在性能差的設(shè)備上會有閃現(xiàn)全部內(nèi)容然后再顯示處理后的內(nèi)容。

2:這樣做會有兩次的setText()操作,在內(nèi)容很多的列表頁會加大性能的損耗。

2、"優(yōu)化"的處理方案

這個時候可能有人會說既然繪制完成后再處理會有問題,提前獲取到textView的行數(shù)進行處理不就好了嗎?沒錯,我們可以設(shè)置addOnPreDrawListener監(jiān)聽提前獲取行數(shù)來進行處理,偽代碼:

textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
     @Override
     public boolean onPreDraw() {
        //進行內(nèi)容的截取和拼接
        return false
    }
});

但是這種方案只適合單獨一條內(nèi)容,不適合在列表中使用,因為這樣只有在第一屏有效,且滑動多屏后回到第一屏也會重置為原始數(shù)據(jù)。

3、最終方案

既然設(shè)置addOnPreDrawListener監(jiān)聽提前獲取行數(shù)來進行處理的方案在列表中不可行還有沒有其他方法呢?那當然是在TextView的onMeasure()中測量textView的高度時進行內(nèi)容的處理,并設(shè)置相對應(yīng)的高度了,這樣就可以保證性能問題又能保證列表中的每條內(nèi)容都能得到處理。先上代碼:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    if (lineCount > maxLine) {
        //如果大于設(shè)置的最大行數(shù)
        val (layout, stringBuilder, sb) = clipContent()
        stringBuilder.append(sb)
        setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
        text = stringBuilder
    }
}

/**
 * 裁剪內(nèi)容
 */
private fun clipContent(): Triple<Layout, SpannableStringBuilder, SpannableString> {
    var offset = 1
    val layout = layout
    val staticLayout = StaticLayout(
            text,
            layout.paint,
            layout.width,
            Layout.Alignment.ALIGN_NORMAL,
            layout.spacingMultiplier,
            layout.spacingAdd,
            false
    )
    val indexEnd = staticLayout.getLineEnd(maxLine - 1)
    val tempText = text.subSequence(0, indexEnd)
    var offsetWidth =
            layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
    val moreWidth =
            ceil(layout.paint.measureText(moreText).toDouble()).toInt()
    //表情字節(jié)個數(shù)
    var countEmoji = 0
    while (indexEnd > offset && offsetWidth <= moreWidth ) {
        //當前字節(jié)是否位表情
        val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
        if (isEmoji){
            countEmoji += 1
        }
        offset++
        val pair = getOffsetWidth(
                indexEnd,
                offset,
                tempText,
                countEmoji,
                offsetWidth,
                layout,
                moreWidth
        )
        offset = pair.first
        offsetWidth = pair.second
    }
    val ssbShrink = tempText.subSequence(0, indexEnd - offset)
    val stringBuilder = SpannableStringBuilder(ssbShrink)
    val sb = SpannableString(moreText)
    sb.setSpan(
            ForegroundColorSpan(moreTextColor), 3, sb.length,
            Spanned.SPAN_INCLUSIVE_INCLUSIVE
    )
    //設(shè)置字體大小
    sb.setSpan(
            AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    if (moreCanClick){
        //設(shè)置點擊事件
        sb.setSpan(
                MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
                Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
    }
    return Triple(layout, stringBuilder, sb)
}

private fun getOffsetWidth(
        indexEnd: Int,
        offset: Int,
        tempText: CharSequence,
        countEmoji: Int,
        offsetWidth: Int,
        layout: Layout,
        moreWidth: Int
): Pair<Int, Int> {
    var offset1 = offset
    var offsetWidth1 = offsetWidth
    if (indexEnd > offset1) {
        val text = tempText[indexEnd - offset1 - 1].toString().trim()
        if (text.isNotEmpty() && countEmoji % 2 == 0) {
            val charText = tempText[indexEnd - offset1]
            offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
            //一個表情兩個字符,避免截取一半字符出現(xiàn)亂碼或者顯示不全...全文
            if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
                offset1++
            }
        }
    } else {
        val charText = tempText[indexEnd - offset1]
        offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
    }
    return Pair(offset1, offsetWidth1)
}

/**
 * 獲取內(nèi)容高度
 */
private fun getDesiredHeight(layout: Layout?): Int {
    if (layout == null) {
        return 0
    }
    val lineTop: Int
    val lineCount = layout.lineCount
    val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
    lineTop = when {
        lineCount > maxLine -> {
            //文字行數(shù)超過最大行
            layout.getLineTop(maxLine)
        }
        else -> {
            layout.getLineTop(lineCount)
        }
    }
    return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
}

大概思路就是判斷內(nèi)容行數(shù)大于我們想要的內(nèi)容行數(shù)時進行內(nèi)容的裁剪,內(nèi)容最后顯示的文案moreText可以按照需求配置,我們測量出moreText的寬度,從最大行數(shù)的最后一個文字向前遍歷截取,直至截取文字的寬度大于等于moreText的寬度,然后我們通過使用SpannableString來拼接moreText文案和moreText的點擊事件。這里還處理了截取到表情字符的情況,我們知道一個表情兩個字符,如果正好截取到表情的一半可以放下moreText就會導(dǎo)致表情變成一個?的亂碼。 另外這里,我們設(shè)置了moreText的點擊事件,那如果textView本身需要設(shè)置點擊事件怎么辦?這個時候就需要處理觸摸事件了,代碼如下:

    val text = text
    val spannable = Spannable.Factory.getInstance().newSpannable(text)

    if (event.action == MotionEvent.ACTION_DOWN) {
        //手指按下
        onDown(spannable, event)
    }

    if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
        //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
        return MyLinkMovementMethod.instance
                .onTouchEvent(this, text as Spannable, event)
    }

    if (event.action == MotionEvent.ACTION_MOVE) {
        //手指移動
        val mClickSpan = getPressedSpan(this, spannable, event)
        if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
            mPressedSpan = null
            Selection.removeSelection(spannable)
        }
    }
    if (event.action == MotionEvent.ACTION_UP) {
        //手指抬起
        onUp(event, spannable)
    }
    return result
}

/**
 * 手指按下邏輯
 */
private fun onDown(spannable: Spannable, event: MotionEvent) {
    //按下時記下clickSpan
    mPressedSpan = getPressedSpan(this, spannable, event)
    if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
        result = true
        Selection.setSelection(
                spannable, spannable.getSpanStart(mPressedSpan),
                spannable.getSpanEnd(mPressedSpan)
        )
    } else {
        result = if (moreCanClick){
            super.onTouchEvent(event)
        }else{
            false
        }
    }
}

/**
 * 手指抬起邏輯
 */
private fun onUp(event: MotionEvent, spannable: Spannable?) {
    result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
        (mPressedSpan as MyClickSpan).onClick(this)
        true
    } else {
        if (moreCanClick) {
            super.onTouchEvent(event)
        }
        false
    }
    mPressedSpan = null
    Selection.removeSelection(spannable)
}

/**
 * 設(shè)置尾部...全文點擊事件
 */
fun setOnAllSpanClickListener(
        onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
) {
    this.onAllSpanClickListener = onAllSpanClickListener
}

private fun getPressedSpan(
        textView: TextView, spannable: Spannable,
        event: MotionEvent
): ClickableSpan? {
    var mTouchSpan: ClickableSpan? = null

    var x = event.x.toInt()
    var y = event.y.toInt()
    x -= textView.totalPaddingLeft
    x += textView.scrollX
    y -= textView.totalPaddingTop
    y += textView.scrollY
    val layout = layout
    val line = layout.getLineForVertical(y)
    val off = layout.getOffsetForHorizontal(line, x.toFloat())

    val spans: Array<MyClickSpan> =
            spannable.getSpans(
                    off, off,
                    MyClickSpan::class.java
            )
    if (spans.isNotEmpty()) {
        mTouchSpan = spans[0]
    } else {
        val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
        if (linkSpans != null && linkSpans.isNotEmpty()) {
            mTouchSpan = linkSpans[0]
        }
    }
    return mTouchSpan
}

其中 if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) { //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent return MyLinkMovementMethod.instance .onTouchEvent(this, text as Spannable, event) }是對鏈接的兼容處理,如果對這個有疑問請看我的上一篇關(guān)于鏈接描述的文章#Android 仿微博正文鏈接交互

三、完整代碼

class ListMoreTextView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = R.attr.MoreTextViewStyle
) :
        AppCompatTextView(context, attrs, defStyleAttr) {

    /**
     * 最大行數(shù)
     */
    private var maxLine: Int

    private val moreTextSize: Int

    /**
     * 尾部更多文字
     */
    private val moreText: String?

    /**
     * 尾部更多文字顏色
     */
    private val moreTextColor: Int

    /**
     * 是否可以點擊尾部更多文字
     */
    private val moreCanClick : Boolean

    private var mPaint: Paint? = null

    /**
     * 尾部更多文字點擊事件接口回調(diào)
     */
    private var onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener? = null

    /**
     * 實現(xiàn)span的點擊
     */
    private var mPressedSpan: ClickableSpan? = null
    private var result = false


    init {
        val array = getContext().obtainStyledAttributes(
            attrs,
            R.styleable.ListMoreTextView, defStyleAttr, 0
        )
        maxLine = array.getInt(R.styleable.MoreTextView_more_action_text_maxLines, Int.MAX_VALUE)
        moreText = array.getString(R.styleable.MoreTextView_more_action_text)
        moreTextSize = array.getInteger(R.styleable.MoreTextView_more_action_text_size, 13)
        moreTextColor = array.getColor(R.styleable.MoreTextView_more_action_text_color, Color.BLACK)
        moreCanClick = array.getBoolean(R.styleable.MoreTextView_more_can_click,false)
        array.recycle()
        init()
    }

    private fun init() {
        mPaint = paint
    }

    /**
     * 設(shè)置最大行數(shù)
     */
    fun setMaxLine (maxLine : Int){
        this.maxLine = maxLine
    }

    /**
     * 使用者主動調(diào)用
     * 如果有顯示鏈接需求一定要調(diào)用此方法
     */
    fun setMovementMethodDefault() {
        movementMethod = MyLinkMovementMethod.instance
        highlightColor = Color.TRANSPARENT
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (lineCount > maxLine) {
            //如果大于設(shè)置的最大行數(shù)
            val (layout, stringBuilder, sb) = clipContent()
            stringBuilder.append(sb)
            setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
            text = stringBuilder
        }
    }

    /**
     * 裁剪內(nèi)容
     */
    private fun clipContent(): Triple<Layout, SpannableStringBuilder, SpannableString> {
        var offset = 1
        val layout = layout
        val staticLayout = StaticLayout(
                text,
                layout.paint,
                layout.width,
                Layout.Alignment.ALIGN_NORMAL,
                layout.spacingMultiplier,
                layout.spacingAdd,
                false
        )
        val indexEnd = staticLayout.getLineEnd(maxLine - 1)
        val tempText = text.subSequence(0, indexEnd)
        var offsetWidth =
                layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
        val moreWidth =
                ceil(layout.paint.measureText(moreText).toDouble()).toInt()
        //表情字節(jié)個數(shù)
        var countEmoji = 0
        while (indexEnd > offset && offsetWidth <= moreWidth ) {
            //當前字節(jié)是否位表情
            val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
            if (isEmoji){
                countEmoji += 1
            }
            offset++
            val pair = getOffsetWidth(
                    indexEnd,
                    offset,
                    tempText,
                    countEmoji,
                    offsetWidth,
                    layout,
                    moreWidth
            )
            offset = pair.first
            offsetWidth = pair.second
        }
        val ssbShrink = tempText.subSequence(0, indexEnd - offset)
        val stringBuilder = SpannableStringBuilder(ssbShrink)
        val sb = SpannableString(moreText)
        sb.setSpan(
                ForegroundColorSpan(moreTextColor), 3, sb.length,
                Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
        //設(shè)置字體大小
        sb.setSpan(
                AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        if (moreCanClick){
            //設(shè)置點擊事件
            sb.setSpan(
                    MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
                    Spanned.SPAN_INCLUSIVE_INCLUSIVE
            )
        }
        return Triple(layout, stringBuilder, sb)
    }

    private fun getOffsetWidth(
            indexEnd: Int,
            offset: Int,
            tempText: CharSequence,
            countEmoji: Int,
            offsetWidth: Int,
            layout: Layout,
            moreWidth: Int
    ): Pair<Int, Int> {
        var offset1 = offset
        var offsetWidth1 = offsetWidth
        if (indexEnd > offset1) {
            val text = tempText[indexEnd - offset1 - 1].toString().trim()
            if (text.isNotEmpty() && countEmoji % 2 == 0) {
                val charText = tempText[indexEnd - offset1]
                offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
                //一個表情兩個字符,避免截取一半字符出現(xiàn)亂碼或者顯示不全...全文
                if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
                    offset1++
                }
            }
        } else {
            val charText = tempText[indexEnd - offset1]
            offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
        }
        return Pair(offset1, offsetWidth1)
    }

    /**
     * 獲取內(nèi)容高度
     */
    private fun getDesiredHeight(layout: Layout?): Int {
        if (layout == null) {
            return 0
        }
        val lineTop: Int
        val lineCount = layout.lineCount
        val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
        lineTop = when {
            lineCount > maxLine -> {
                //文字行數(shù)超過最大行
                layout.getLineTop(maxLine)
            }
            else -> {
                layout.getLineTop(lineCount)
            }
        }
        return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val text = text
        val spannable = Spannable.Factory.getInstance().newSpannable(text)

        if (event.action == MotionEvent.ACTION_DOWN) {
            //手指按下
            onDown(spannable, event)
        }

        if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
            //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
            return MyLinkMovementMethod.instance
                    .onTouchEvent(this, text as Spannable, event)
        }

        if (event.action == MotionEvent.ACTION_MOVE) {
            //手指移動
            val mClickSpan = getPressedSpan(this, spannable, event)
            if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
                mPressedSpan = null
                Selection.removeSelection(spannable)
            }
        }
        if (event.action == MotionEvent.ACTION_UP) {
            //手指抬起
            onUp(event, spannable)
        }
        return result
    }

    /**
     * 手指按下邏輯
     */
    private fun onDown(spannable: Spannable, event: MotionEvent) {
        //按下時記下clickSpan
        mPressedSpan = getPressedSpan(this, spannable, event)
        if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
            result = true
            Selection.setSelection(
                    spannable, spannable.getSpanStart(mPressedSpan),
                    spannable.getSpanEnd(mPressedSpan)
            )
        } else {
            result = if (moreCanClick){
                super.onTouchEvent(event)
            }else{
                false
            }
        }
    }

    /**
     * 手指抬起邏輯
     */
    private fun onUp(event: MotionEvent, spannable: Spannable?) {
        result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
            (mPressedSpan as MyClickSpan).onClick(this)
            true
        } else {
            if (moreCanClick) {
                super.onTouchEvent(event)
            }
            false
        }
        mPressedSpan = null
        Selection.removeSelection(spannable)
    }

    /**
     * 設(shè)置尾部...全文點擊事件
     */
    fun setOnAllSpanClickListener(
            onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
    ) {
        this.onAllSpanClickListener = onAllSpanClickListener
    }

    private fun getPressedSpan(
            textView: TextView, spannable: Spannable,
            event: MotionEvent
    ): ClickableSpan? {
        var mTouchSpan: ClickableSpan? = null

        var x = event.x.toInt()
        var y = event.y.toInt()
        x -= textView.totalPaddingLeft
        x += textView.scrollX
        y -= textView.totalPaddingTop
        y += textView.scrollY
        val layout = layout
        val line = layout.getLineForVertical(y)
        val off = layout.getOffsetForHorizontal(line, x.toFloat())

        val spans: Array<MyClickSpan> =
                spannable.getSpans(
                        off, off,
                        MyClickSpan::class.java
                )
        if (spans.isNotEmpty()) {
            mTouchSpan = spans[0]
        } else {
            val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
            if (linkSpans != null && linkSpans.isNotEmpty()) {
                mTouchSpan = linkSpans[0]
            }
        }
        return mTouchSpan
    }
}
<declare-styleable name="ListMoreTextView">
    <attr name="more_action_text_maxLines" format="integer"/>
    <attr name="more_action_text" format="string"/>
    <attr name="more_action_text_color" format="color"/>
    <attr name="more_action_text_size" format="integer"/>
    <attr name="more_can_click" format="boolean"/>
</declare-styleable>

注意:如果是有鏈接需求要主動調(diào)用該方法,否則鏈接的觸摸交互無效。

/**
 * 使用者主動調(diào)用
 * 如果有顯示鏈接需求一定要調(diào)用此方法
 */
fun setMovementMethodDefault() {
    movementMethod = MyLinkMovementMethod.instance
    highlightColor = Color.TRANSPARENT
}

另外,這里沒有對內(nèi)容連續(xù)換行的處理,因為個人覺得列表數(shù)據(jù)是對主要內(nèi)容的顯示,另外客戶端不要做太多的數(shù)據(jù)處理的耗時操作,應(yīng)該是由后端的同學(xué)或者產(chǎn)品設(shè)計時避免這種情況的產(chǎn)生。

四、效果

五、代碼地址

點擊獲取


作者:笑慢
鏈接:https://juejin.cn/post/7037416456782348295
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

相關(guān)文章

最新評論