Android 自定義View 一行顯示不下?lián)Q行顯示

1675150565652.jpg

今天擼一個(gè) 文字顯示不下?lián)Q行顯示的view
首先聊天頁面顯示文本 有一個(gè)最低高度 和最大寬度,這里直接就寫死,或者寫屏幕尺寸比例均可。
先定義需要的變量如:最大寬度、 view的寬高、畫筆、間距、x軸邊距等等

    // 顯示聊天內(nèi)容的畫筆
    private lateinit var mTextPaint: TextPaint

    // 顯示時(shí)間 和 繪制圖標(biāo)的畫筆
    private lateinit var mPaint: Paint

    // 顯示文本內(nèi)容
    private lateinit var staticLayout: StaticLayout

    // 點(diǎn)擊的文本類型
    companion object {
        const val TEXT_TYPE_LINK = 1
        const val TEXT_TYPE_AT = 2
//        const val TEXT_TYPE_PHONE = 3
    }

    private lateinit var onClickListener: (str: String?, textType: Int) -> Unit

    // view的寬高
    private var mWidth = 0
    private var mHeight = 0
    private var textWidth = 0
//    private var textHeight = 0

    // 最大總寬度
    private val mMaxWidth = 242.dp2Px()

    // 繪制時(shí)間兩側(cè)圖標(biāo)的間隔
    private val space = 5.dp2Px()

    // 距離左邊X軸的邊距
    private var leftX = 0

    // 距離上邊Y軸的邊距
    private var topY = 0

    // 發(fā)送狀態(tài)的圖標(biāo)
    private lateinit var readStateBitmap: Bitmap

    // 發(fā)送的狀態(tài)
    private var sendState = 0

    // 顯示已讀的狀態(tài)
    private var readState = 0

    // 置頂?shù)膱D標(biāo)
    private lateinit var topBitmap: Bitmap

    // 是否置頂
    private var isTopMsg: Boolean = false

    // 如果是true 隱藏
    private var isTopReadState = false

    // 繪制的文本
    private var textContent: CharSequence = ""
    private var textContentClick: CharSequence = ""

    // 最后一行文本的寬度
    private var lineWidth: Float = 0f

在設(shè)置顯示內(nèi)容時(shí),處理一下表情顯示異常問題,還有特殊文本顯示問題例如 @某某某,鏈接等,在繪制的時(shí)候還要處理字符加粗還是正常顯示,畫筆需要自己實(shí)現(xiàn)

fun setTimePaint(paint: Paint): ChatTextViewLayout {
     this.mPaint = paint
     return this
}

fun setTextPaint(textPaint: TextPaint): ChatTextViewLayout {
     this.mTextPaint = textPaint
     return this
}

fun setTextContent(text: CharSequence): ChatTextViewLayout {
        val spannableStringBuilder = SpannableStringBuilder(text.trim())
        // 判斷是否包含表情
        if (EmojiUtils.containsEmoji(spannableStringBuilder.toString())) {
            val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetrics
            val defaultEmojiSize = fontMetrics.descent - fontMetrics.ascent
            // 表情符號(hào)大小為55f
            EmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, 55f)
//            EmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, defaultEmojiSize)
        }
        this.textContentClick = spannableStringBuilder
        this.textContent = AtUserHelper.parseAtUserLinkJx(spannableStringBuilder,
            ContextCompat.getColor(context, R.color.color_at), object : AtUserLinkOnClickListener {
                override fun ulrLinkClick(str: String?) {
//                    onClickListener.invoke(str, TEXT_TYPE_LINK)
                }

                override fun atUserClick(str: String?) {
//                    onClickListener.invoke(str, TEXT_TYPE_AT)
                }

                override fun phoneClick(str: String?) {
                }
            }).trim()
        return this
    }

然后是測(cè)量文本內(nèi)容的寬高,在這里用的是StaticLayout,如果一行可以顯示下,就正常顯示 在右側(cè)繪制出顯示的時(shí)間和狀態(tài)圖標(biāo),如果顯示不下,那么添加一行高度,在最右側(cè)繪制;如果是多行,就計(jì)算出最后一行的文本寬度,邏輯如此。

private fun createLayout() {
        val textWidthRect = mTextPaint.measureText(textContent.toString())
        val staticLayoutWidth =
            (if (textWidthRect >= mMaxWidth) mMaxWidth else textWidthRect).toInt()
        // 先計(jì)算發(fā)送狀態(tài)的寬度
        val sendStateWidth =
            if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0
        // 右側(cè)時(shí)間發(fā)送狀態(tài)布局的寬度 = 發(fā)送狀態(tài)的寬度 + 時(shí)間寬度 + 間距 + 置頂寬度
        val timeLayoutWidth =
            sendStateWidth + timeWidth + space * 2 + if (isTopMsg) topBitmap.width + space else 0
        // 字符串不包含換行 并且寬度小于等于最大寬度  那么就是一行
        staticLayout = StaticLayout.Builder
            .obtain(textContent, 0, textContent.length, mTextPaint, mMaxWidth)
            .setText(textContent)
            .setAlignment(Layout.Alignment.ALIGN_NORMAL)
            .setLineSpacing(0.0f, 1.0f)
            .setIncludePad(false)
            .build()
        try {
            textWidth = 0
            for (i in 0 until staticLayout.lineCount) {
                try {
                    lineWidth = staticLayout.getLineWidth(i)
                    if (lineWidth >= staticLayoutWidth) {
                        lineWidth = staticLayoutWidth.toFloat()
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                    break
                }
                textWidth = max(textWidth.toDouble(), ceil(lineWidth.toDouble())).toInt()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        /**
         * 總寬度 如果超出一行 那么取最大寬度
         * 如果是一行 那么計(jì)算 總寬度 = 文本 + 右側(cè)時(shí)間發(fā)送狀態(tài)布局的寬度
         */
        mWidth = if (staticLayout.lineCount > 1) {
            // 取最大寬度
            val width = max(textWidth.toFloat(), lineWidth)
            // 如果最后一行 加上時(shí)間寬度 小于最大寬度
            if (lineWidth + timeLayoutWidth <= mMaxWidth) {
                // 如果最后一行 加上時(shí)間寬度 小于最大寬度
                if (lineWidth + timeLayoutWidth < width) {
                    // 文本寬度小于時(shí)間寬度
                    if (staticLayoutWidth <= timeLayoutWidth) {
                        (staticLayoutWidth + timeLayoutWidth).toInt()
                    } else {
                        staticLayoutWidth
                    }
                } else {
                    (lineWidth + timeLayoutWidth).toInt()
                }
            } else {
                width.toInt()
            }
        } else {
            if (lineWidth > mMaxWidth - timeLayoutWidth) {
                staticLayoutWidth
            } else {
                (lineWidth + timeLayoutWidth).toInt()
            }
        }
        /**
         * 高度取決于最后一行文本的寬度 如果時(shí)間和圖標(biāo)顯示不下  那么就添加一行高度
         * 顯示不下:
         *         高度 = 文本高度 +  + 上下邊距 + (單行文本高度和間距)
         * 一行顯示:
         *         高度 = 文本高度 + 上下邊距
         */
        // 先判斷最后一行文本寬度是否能顯示下,總寬度 - 左右間距 - 右側(cè)時(shí)間和左右圖標(biāo)的寬度間距
        mHeight = if (lineWidth > mMaxWidth - timeLayoutWidth) {
            staticLayout.height / staticLayout.lineCount + staticLayout.height - space
        } else {
            staticLayout.height
        }
        leftX = 0
        topY = 0
    }

剩下的就簡(jiǎn)單了,計(jì)算繪制就可以了

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        createLayout()
        // 先繪制文本
        canvas.save()
        canvas.translate(leftX.toFloat(), topY.toFloat())
        staticLayout.draw(canvas)
        if (!isTopReadState && sendState == 1) {
            // 繪制右側(cè)發(fā)送狀態(tài)的圖標(biāo)
            leftX = mWidth - readStateBitmap.width
            // 右側(cè)發(fā)送狀態(tài)圖標(biāo)較大 稍微偏下一點(diǎn)點(diǎn)
            topY = mHeight - readStateBitmap.height + space / 2
            canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
        // 繪制時(shí)間
        leftX = if (leftX == 0) {
            mWidth - timeWidth.toInt() - space
        } else {
            leftX - timeWidth.toInt() - space
        }
        topY = mHeight
        canvas.drawText(time, 0, time.length, leftX.toFloat(), topY.toFloat(), mPaint)
        // 如果置頂繪制置頂
        if (isTopMsg) {
            leftX = leftX - topBitmap.width - space
            topY = mHeight - topBitmap.height
            canvas.drawBitmap(topBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
    }

最后處理點(diǎn)擊事件,因?yàn)镾taticLayout繪制,SpannableStringBuilder樣式可以顯示,但點(diǎn)擊事件并不行(這里我試過好多次,也換好幾種方式,都不支持點(diǎn)擊事件,不知道是不是我的姿勢(shì)不對(duì),如果有人實(shí)現(xiàn)了那么請(qǐng)@我,留下代碼,讓我學(xué)習(xí)學(xué)習(xí)),因?yàn)轱@示的時(shí)候是SpannableStringBuilder,但是點(diǎn)擊的時(shí)候計(jì)算的位置,所以點(diǎn)擊處理用的是原始沒有處理過的文本數(shù)據(jù),然后拆分判斷點(diǎn)擊的是某個(gè)@或鏈接,(當(dāng)時(shí)都要吐血了) 先正則判斷是什么,在進(jìn)行替換,然后計(jì)算字符,響應(yīng)點(diǎn)擊事件。

override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_UP -> {
                if (event.x >= 0f && event.x <= staticLayout.width && event.y >= 0f && event.y <= staticLayout.height) {
                    val line: Int = staticLayout.getLineForVertical(event.y.toInt())
                    val off: Int = staticLayout.getOffsetForHorizontal(line, event.x)
                    // 進(jìn)行正則匹配[文字](鏈接)
                    val spannableString = SpannableStringBuilder(textContentClick)
                    clickTextContentUrl(
                        clickTextContentAt(textContentClick, off, spannableString),
                        off,
                        spannableString
                    )
                }
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 處理點(diǎn)擊的是At
     */
    private fun clickTextContentAt(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder
    ): SpannableStringBuilder {
        try {
            val matcherAt = Pattern.compile(AT_PATTERN).matcher(text)
            var replaceOffsetAt = 0 //每次替換之后matcher的偏移量
            while (matcherAt.find()) {
                // 解析鏈接  格式是[文字](鏈接)
                val name = matcherAt.group(0)
                val uid = name?.substring(2, name.length - 1)
                // 把匹配成功的串a(chǎn)ppend進(jìn)結(jié)果串中, 并設(shè)置點(diǎn)擊效果
                val groupMemberBean = uid?.let { getGroupDb().getAllMemberById(it) }
                if (groupMemberBean != null) {
                    val atName = "@" + groupMemberBean.name + " "
                    val clickSpanStartAt = matcherAt.start() - replaceOffsetAt
                    val clickSpanEndAt = clickSpanStartAt + atName.length
                    spannableString.replace(
                        matcherAt.start() - replaceOffsetAt,
                        matcherAt.end() - replaceOffsetAt,
                        atName
                    )
                    replaceOffsetAt += matcherAt.end() - matcherAt.start() - atName.length
                    val clickableSpan = object : ClickableSpan() {
                        override fun onClick(view: View) {
                              // 點(diǎn)擊事件并不靈
                        }
                    }
                    spannableString.setSpan(
                        clickableSpan,
                        clickSpanStartAt,
                        clickSpanEndAt,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                    )
                    // 點(diǎn)擊回調(diào)
                    if (clickSpanStartAt <= off && off <= clickSpanEndAt) {
                        postDelayed({ onClickListener.invoke(uid, TEXT_TYPE_AT) }, 100)
                        break
                    }
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return spannableString
    }

    /**
     * 處理點(diǎn)擊的是鏈接
     */
    private fun clickTextContentUrl(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder
    ) {
        try {
            //超鏈接轉(zhuǎn)化
            val matcher = Pattern.compile(AtUserHelper.URL_PATTERN).matcher(text)
            var replaceOffset = 0 //每次替換之后matcher的偏移量
            while (matcher.find()) {
                // 解析鏈接  格式是[文字](鏈接)
                val name = matcher.group(0)
                val clickSpanStart = matcher.start() - replaceOffset
                val clickSpanEnd = clickSpanStart + (name?.length ?: 0)
                spannableString.replace(
                    matcher.start() - replaceOffset,
                    matcher.end() - replaceOffset,
                    name
                )
                replaceOffset += matcher.end() - matcher.start() - (name?.length ?: 0)
                val clickableSpan = object : ClickableSpan() {
                    override fun onClick(view: View) {
                    }
                }
                spannableString.setSpan(
                    clickableSpan,
                    clickSpanStart,
                    clickSpanEnd,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                )
                if (clickSpanStart <= off && off <= clickSpanEnd) {
                    postDelayed({ onClickListener.invoke(name, TEXT_TYPE_LINK) }, 100)
                    break
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

下面是完整代碼

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.text.*
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.*
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.StringUtils
import com.vanniktech.emoji.EmojiManager
import com.ym.base.ext.dp2Px
import com.ym.chat.R
import com.ym.chat.db.ChatDao.getGroupDb
import com.ym.chat.ext.ORIENTATION_LEFT
import com.ym.chat.utils.EmojiUtils
import com.ym.chat.utils.StringExt.AT_PATTERN
import com.ym.chat.widget.ateditview.AtUserHelper
import com.ym.chat.widget.ateditview.AtUserLinkOnClickListener
import java.util.regex.Pattern
import kotlin.math.ceil
import kotlin.math.max


/**
 *  description:
 *
 *  @author  Db_z
 *  @Date    2023/1/16 13:12
 */
class ChatTextViewLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : ViewGroup(context, attrs, defStyleAttr) {

    // 顯示聊天內(nèi)容的畫筆
    private lateinit var mTextPaint: TextPaint

    // 顯示時(shí)間 和 繪制圖標(biāo)的畫筆
    private lateinit var mPaint: Paint

    // 顯示文本內(nèi)容
    private lateinit var staticLayout: StaticLayout

    // 點(diǎn)擊的文本類型
    companion object {
        const val TEXT_TYPE_LINK = 1
        const val TEXT_TYPE_AT = 2
//        const val TEXT_TYPE_PHONE = 3
    }

    private lateinit var onClickListener: (str: String?, textType: Int) -> Unit

    // view的寬高
    private var mWidth = 0
    private var mHeight = 0
    private var textWidth = 0
//    private var textHeight = 0

    // 最大總寬度
    private val mMaxWidth = 242.dp2Px()

    // 繪制時(shí)間兩側(cè)圖標(biāo)的間隔
    private val space = 5.dp2Px()

    // 距離左邊X軸的邊距
    private var leftX = 0

    // 距離上邊Y軸的邊距
    private var topY = 0

    // 發(fā)送狀態(tài)的圖標(biāo)
    private lateinit var readStateBitmap: Bitmap

    // 發(fā)送的狀態(tài)
    private var sendState = 0

    // 顯示已讀的狀態(tài)
    private var readState = 0

    // 置頂?shù)膱D標(biāo)
    private lateinit var topBitmap: Bitmap

    // 是否置頂
    private var isTopMsg: Boolean = false

    // 如果是true 隱藏
    private var isTopReadState = false

    // 繪制的文本
    private var textContent: CharSequence = ""
    private var textContentClick: CharSequence = ""

    // 最后一行文本的寬度
    private var lineWidth: Float = 0f

    // 繪制的時(shí)間
    private var time: String = "00:00"

    // 時(shí)間的文本寬度
    private var timeWidth: Float = 0f

    fun setSendState(sendState: Int): ChatTextViewLayout {
        this.sendState = sendState
        return this
    }

    fun setReadState(readState: Int, isTop: Boolean): ChatTextViewLayout {
        this.readState = readState
        isTopReadState = isTop
        readStateBitmap = if (readState == 1) {
            BitmapFactory.decodeResource(context.resources, R.drawable.iv_text_read)
        } else {
            BitmapFactory.decodeResource(context.resources, R.drawable.iv_text_unread)
        }
        return this
    }

    fun setTimePaint(paint: Paint): ChatTextViewLayout {
        this.mPaint = paint
        return this
    }

    fun setTextPaint(textPaint: TextPaint): ChatTextViewLayout {
        this.mTextPaint = textPaint
        return this
    }

    fun setTime(time: String): ChatTextViewLayout {
        this.time = time
        return this
    }

    fun showTopMsg(isTopMsg: Boolean, orientation: Int = ORIENTATION_LEFT): ChatTextViewLayout {
        this.isTopMsg = isTopMsg
        topBitmap = if (orientation == ORIENTATION_LEFT) {
            BitmapFactory.decodeResource(context.resources, R.drawable.icon_top_grey)
        } else {
            BitmapFactory.decodeResource(context.resources, R.drawable.icon_top_blue)
        }
        return this
    }

    fun setOnClickListener(onClickListener: (str: String?, textType: Int) -> Unit): ChatTextViewLayout {
        this.onClickListener = onClickListener
        return this
    }

    fun setTextContent(text: CharSequence): ChatTextViewLayout {
        val spannableStringBuilder = SpannableStringBuilder(text.trim())
        // 判斷是否包含表情
        if (EmojiUtils.containsEmoji(spannableStringBuilder.toString())) {
            val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetrics
            val defaultEmojiSize = fontMetrics.descent - fontMetrics.ascent
            // 表情符號(hào)大小為55f
            EmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, 55f)
//            EmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, defaultEmojiSize)
        }
        this.textContentClick = spannableStringBuilder
        this.textContent = AtUserHelper.parseAtUserLinkJx(spannableStringBuilder,
            ContextCompat.getColor(context, R.color.color_at), object : AtUserLinkOnClickListener {
                override fun ulrLinkClick(str: String?) {
//                    onClickListener.invoke(str, TEXT_TYPE_LINK)
                }

                override fun atUserClick(str: String?) {
//                    onClickListener.invoke(str, TEXT_TYPE_AT)
                }

                override fun phoneClick(str: String?) {
                }
            }).trim()
        return this
    }

    fun build() {
        if (StringUtils.isEmpty(time) || StringUtils.isEmpty(textContent)) return
        timeWidth = mPaint.measureText(time)
        createLayout()
        setWillNotDraw(false)
        requestLayout()
    }

    /**
     * 處理點(diǎn)擊的是At
     */
    private fun clickTextContentAt(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder,
    ): SpannableStringBuilder {
        try {
            val matcherAt = Pattern.compile(AT_PATTERN).matcher(text)
            var replaceOffsetAt = 0 //每次替換之后matcher的偏移量
            while (matcherAt.find()) {
                // 解析鏈接  格式是[文字](鏈接)
                val name = matcherAt.group(0)
                val uid = name?.substring(2, name.length - 1)
                // 把匹配成功的串a(chǎn)ppend進(jìn)結(jié)果串中, 并設(shè)置點(diǎn)擊效果
                val groupMemberBean = uid?.let { getGroupDb().getAllMemberById(it) }
                if (groupMemberBean != null) {
                    val atName = "@" + groupMemberBean.name + " "
                    val clickSpanStartAt = matcherAt.start() - replaceOffsetAt
                    val clickSpanEndAt = clickSpanStartAt + atName.length
                    spannableString.replace(
                        matcherAt.start() - replaceOffsetAt,
                        matcherAt.end() - replaceOffsetAt,
                        atName
                    )
                    replaceOffsetAt += matcherAt.end() - matcherAt.start() - atName.length
                    val clickableSpan = object : ClickableSpan() {
                        override fun onClick(view: View) {
                        }
                    }
                    spannableString.setSpan(
                        clickableSpan,
                        clickSpanStartAt,
                        clickSpanEndAt,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                    )
                    if (off in clickSpanStartAt..clickSpanEndAt) {
                        postDelayed({ onClickListener.invoke(uid, TEXT_TYPE_AT) }, 100)
                        break
                    }
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return spannableString
    }

    /**
     * 處理點(diǎn)擊的是鏈接
     */
    private fun clickTextContentUrl(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder,
    ) {
        try {
            //超鏈接轉(zhuǎn)化
            val matcher = Pattern.compile(AtUserHelper.URL_PATTERN).matcher(text)
            var replaceOffset = 0 //每次替換之后matcher的偏移量
            while (matcher.find()) {
                // 解析鏈接  格式是[文字](鏈接)
                val name = matcher.group(0)
                val clickSpanStart = matcher.start() - replaceOffset
                val clickSpanEnd = clickSpanStart + (name?.length ?: 0)
                spannableString.replace(
                    matcher.start() - replaceOffset,
                    matcher.end() - replaceOffset,
                    name
                )
                replaceOffset += matcher.end() - matcher.start() - (name?.length ?: 0)
                val clickableSpan = object : ClickableSpan() {
                    override fun onClick(view: View) {
                    }
                }
                spannableString.setSpan(
                    clickableSpan,
                    clickSpanStart,
                    clickSpanEnd,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                )
                if (off in clickSpanStart..clickSpanEnd) {
                    postDelayed({ onClickListener.invoke(name, TEXT_TYPE_LINK) }, 100)
                    break
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_UP -> {
                if (event.x >= 0f && event.x <= staticLayout.width && event.y >= 0f && event.y <= staticLayout.height) {
                    val line: Int = staticLayout.getLineForVertical(event.y.toInt())
                    val off: Int = staticLayout.getOffsetForHorizontal(line, event.x)
                    // 進(jìn)行正則匹配[文字](鏈接)
                    val spannableString = SpannableStringBuilder(textContentClick)
                    clickTextContentUrl(
                        clickTextContentAt(textContentClick, off, spannableString),
                        off,
                        spannableString
                    )
                }
            }
        }
        return super.onTouchEvent(event)
    }

    private fun createLayout() {
        val textWidthRect = mTextPaint.measureText(textContent.toString())
        val staticLayoutWidth =
            (if (textWidthRect >= mMaxWidth) mMaxWidth else textWidthRect).toInt()
        // 先計(jì)算發(fā)送狀態(tài)的寬度
        val sendStateWidth =
            if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0
        // 右側(cè)時(shí)間發(fā)送狀態(tài)布局的寬度 = 發(fā)送狀態(tài)的寬度 + 時(shí)間寬度 + 間距 + 置頂寬度
        val timeLayoutWidth =
            sendStateWidth + timeWidth + space * 2 + if (isTopMsg) topBitmap.width + space else 0
        // 字符串不包含換行 并且寬度小于等于最大寬度  那么就是一行
        staticLayout = StaticLayout.Builder
            .obtain(textContent, 0, textContent.length, mTextPaint, mMaxWidth)
            .setText(textContent)
            .setAlignment(Layout.Alignment.ALIGN_NORMAL)
            .setLineSpacing(0.0f, 1.0f)
            .setIncludePad(false)
            .build()
        try {
            textWidth = 0
            for (i in 0 until staticLayout.lineCount) {
                try {
                    lineWidth = staticLayout.getLineWidth(i)
                    if (lineWidth >= staticLayoutWidth) {
                        lineWidth = staticLayoutWidth.toFloat()
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                    break
                }
                textWidth = max(textWidth.toDouble(), ceil(lineWidth.toDouble())).toInt()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        /**
         * 總寬度 如果超出一行 那么取最大寬度
         * 如果是一行 那么計(jì)算 總寬度 = 文本 + 右側(cè)時(shí)間發(fā)送狀態(tài)布局的寬度
         */
        mWidth = if (staticLayout.lineCount > 1) {
            // 取最大寬度
            val width = max(textWidth.toFloat(), lineWidth)
            // 如果最后一行 加上時(shí)間寬度 小于最大寬度
            if (lineWidth + timeLayoutWidth <= mMaxWidth) {
                // 如果最后一行 加上時(shí)間寬度 小于最大寬度
                if (lineWidth + timeLayoutWidth < width) {
                    // 文本寬度小于時(shí)間寬度
                    if (staticLayoutWidth <= timeLayoutWidth) {
                        (staticLayoutWidth + timeLayoutWidth).toInt()
                    } else {
                        staticLayoutWidth
                    }
                } else {
                    (lineWidth + timeLayoutWidth).toInt()
                }
            } else {
                width.toInt()
            }
        } else {
            if (lineWidth > mMaxWidth - timeLayoutWidth) {
                staticLayoutWidth
            } else {
                (lineWidth + timeLayoutWidth).toInt()
            }
        }
        /**
         * 高度取決于最后一行文本的寬度 如果時(shí)間和圖標(biāo)顯示不下  那么就添加一行高度
         * 顯示不下:
         *         高度 = 文本高度 +  + 上下邊距 + (單行文本高度和間距)
         * 一行顯示:
         *         高度 = 文本高度 + 上下邊距
         */
        // 先判斷最后一行文本寬度是否能顯示下,總寬度 - 左右間距 - 右側(cè)時(shí)間和左右圖標(biāo)的寬度間距
        mHeight = if (lineWidth > mMaxWidth - timeLayoutWidth) {
            staticLayout.height / staticLayout.lineCount + staticLayout.height - space
        } else {
            staticLayout.height
        }
        leftX = 0
        topY = 0
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(mWidth, mHeight)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        createLayout()
        // 先繪制文本
        canvas.save()
        canvas.translate(leftX.toFloat(), topY.toFloat())
        staticLayout.draw(canvas)
        if (!isTopReadState && sendState == 1) {
            // 繪制右側(cè)發(fā)送狀態(tài)的圖標(biāo)
            leftX = mWidth - readStateBitmap.width
            // 右側(cè)發(fā)送狀態(tài)圖標(biāo)較大 稍微偏下一點(diǎn)點(diǎn)
            topY = mHeight - readStateBitmap.height + space / 2
            canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
        // 繪制時(shí)間
        leftX = if (leftX == 0) {
            mWidth - timeWidth.toInt() - space
        } else {
            leftX - timeWidth.toInt() - space
        }
        topY = mHeight
        canvas.drawText(time, 0, time.length, leftX.toFloat(), topY.toFloat(), mPaint)
        // 如果置頂繪制置頂
        if (isTopMsg) {
            leftX = leftX - topBitmap.width - space
            topY = mHeight - topBitmap.height
            canvas.drawBitmap(topBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

    }
}

基本上就是全部代碼了,其中有自己不需要的進(jìn)行剔除。
好久沒更新,等有時(shí)間會(huì)進(jìn)行整理,然后在給出git。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容