Android 自定义View 一行显示不下换行显示


今天撸一个 文字显示不下换行显示的view
首先聊天页面显示文本 有一个最低高度 和最大宽度,这里直接就写死,或者写屏幕尺寸比例均可。
先定义需要的变量如:最大宽度、 view的宽高、画笔、间距、x轴边距等等

// 显示聊天内容的画笔
    private lateinit var mTextPaint: TextPaint

    // 显示时间 和 绘制图标的画笔
    private lateinit var mPaint: Paint

    // 显示文本内容
    private lateinit var staticLayout: StaticLayout

    // 点击的文本类型
    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()

    // 绘制时间两侧图标的间隔
    private val space = 5.dp2Px()

    // 距离左边X轴的边距
    private var leftX = 0

    // 距离上边Y轴的边距
    private var topY = 0

    // 发送状态的图标
    private lateinit var readStateBitmap: Bitmap

    // 发送的状态
    private var sendState = 0

    // 显示已读的状态
    private var readState = 0

    // 置顶的图标
    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


在设置显示内容时,处理一下表情显示异常问题,还有特殊文本显示问题例如 @某某某,链接等,在绘制的时候还要处理字符加粗还是正常显示,画笔需要自己实现

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
            // 表情符号大小为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
    }


然后是测量文本内容的宽高,在这里用的是StaticLayout,如果一行可以显示下,就正常显示  在右侧绘制出显示的时间和状态图标,如果显示不下,那么添加一行高度,在最右侧绘制;如果是多行,就计算出最后一行的文本宽度,逻辑如此。

private fun createLayout() {
        val textWidthRect = mTextPaint.measureText(textContent.toString())
        val staticLayoutWidth =
            (if (textWidthRect >= mMaxWidth) mMaxWidth else textWidthRect).toInt()
        // 先计算发送状态的宽度
        val sendStateWidth =
            if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0
        // 右侧时间发送状态布局的宽度 = 发送状态的宽度 + 时间宽度 + 间距 + 置顶宽度
        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()
        }
        /**
         * 总宽度 如果超出一行 那么取最大宽度
         * 如果是一行 那么计算 总宽度 = 文本 + 右侧时间发送状态布局的宽度
         */
        mWidth = if (staticLayout.lineCount > 1) {
            // 取最大宽度
            val width = max(textWidth.toFloat(), lineWidth)
            // 如果最后一行 加上时间宽度 小于最大宽度
            if (lineWidth + timeLayoutWidth <= mMaxWidth) {
                // 如果最后一行 加上时间宽度 小于最大宽度
                if (lineWidth + timeLayoutWidth < width) {
                    // 文本宽度小于时间宽度
                    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()
            }
        }
        /**
         * 高度取决于最后一行文本的宽度 如果时间和图标显示不下  那么就添加一行高度
         * 显示不下:
         *         高度 = 文本高度 +  + 上下边距 + (单行文本高度和间距)
         * 一行显示:
         *         高度 = 文本高度 + 上下边距
         */
        // 先判断最后一行文本宽度是否能显示下,总宽度 - 左右间距 - 右侧时间和左右图标的宽度间距
        mHeight = if (lineWidth > mMaxWidth - timeLayoutWidth) {
            staticLayout.height / staticLayout.lineCount + staticLayout.height - space
        } else {
            staticLayout.height
        }
        leftX = 0
        topY = 0
    }

剩下的就简单了,计算绘制就可以了

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        createLayout()
        // 先绘制文本
        canvas.save()
        canvas.translate(leftX.toFloat(), topY.toFloat())
        staticLayout.draw(canvas)
        if (!isTopReadState && sendState == 1) {
            // 绘制右侧发送状态的图标
            leftX = mWidth - readStateBitmap.width
            // 右侧发送状态图标较大 稍微偏下一点点
            topY = mHeight - readStateBitmap.height + space / 2
            canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
        // 绘制时间
        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)
        }
    }


最后处理点击事件,因为StaticLayout绘制,SpannableStringBuilder样式可以显示,但点击事件并不行(这里我试过好多次,也换好几种方式,都不支持点击事件,不知道是不是我的姿势不对,如果有人实现了那么请@我,留下代码,让我学习学习),因为显示的时候是SpannableStringBuilder,但是点击的时候计算的位置,所以点击处理用的是原始没有处理过的文本数据,然后拆分判断点击的是某个@或链接,(当时都要吐血了) 先正则判断是什么,在进行替换,然后计算字符,响应点击事件。

```
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)
                    // 进行正则匹配[文字](链接)
                    val spannableString = SpannableStringBuilder(textContentClick)
                    clickTextContentUrl(
                        clickTextContentAt(textContentClick, off, spannableString),
                        off,
                        spannableString
                    )
                }
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 处理点击的是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)
                // 把匹配成功的串append进结果串中, 并设置点击效果
                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 (clickSpanStartAt <= off && off <= clickSpanEndAt) {
                        postDelayed({ onClickListener.invoke(uid, TEXT_TYPE_AT) }, 100)
                        break
                    }
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return spannableString
    }

    /**
     * 处理点击的是链接
     */
    private fun clickTextContentUrl(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder
    ) {
        try {
            //超链接转化
            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) {

    // 显示聊天内容的画笔
    private lateinit var mTextPaint: TextPaint

    // 显示时间 和 绘制图标的画笔
    private lateinit var mPaint: Paint

    // 显示文本内容
    private lateinit var staticLayout: StaticLayout

    // 点击的文本类型
    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()

    // 绘制时间两侧图标的间隔
    private val space = 5.dp2Px()

    // 距离左边X轴的边距
    private var leftX = 0

    // 距离上边Y轴的边距
    private var topY = 0

    // 发送状态的图标
    private lateinit var readStateBitmap: Bitmap

    // 发送的状态
    private var sendState = 0

    // 显示已读的状态
    private var readState = 0

    // 置顶的图标
    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

    // 绘制的时间
    private var time: String = "00:00"

    // 时间的文本宽度
    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
            // 表情符号大小为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()
    }

    /**
     * 处理点击的是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)
                // 把匹配成功的串append进结果串中, 并设置点击效果
                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
    }

    /**
     * 处理点击的是链接
     */
    private fun clickTextContentUrl(
        text: CharSequence,
        off: Int,
        spannableString: SpannableStringBuilder,
    ) {
        try {
            //超链接转化
            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)
                    // 进行正则匹配[文字](链接)
                    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()
        // 先计算发送状态的宽度
        val sendStateWidth =
            if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0
        // 右侧时间发送状态布局的宽度 = 发送状态的宽度 + 时间宽度 + 间距 + 置顶宽度
        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()
        }
        /**
         * 总宽度 如果超出一行 那么取最大宽度
         * 如果是一行 那么计算 总宽度 = 文本 + 右侧时间发送状态布局的宽度
         */
        mWidth = if (staticLayout.lineCount > 1) {
            // 取最大宽度
            val width = max(textWidth.toFloat(), lineWidth)
            // 如果最后一行 加上时间宽度 小于最大宽度
            if (lineWidth + timeLayoutWidth <= mMaxWidth) {
                // 如果最后一行 加上时间宽度 小于最大宽度
                if (lineWidth + timeLayoutWidth < width) {
                    // 文本宽度小于时间宽度
                    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()
            }
        }
        /**
         * 高度取决于最后一行文本的宽度 如果时间和图标显示不下  那么就添加一行高度
         * 显示不下:
         *         高度 = 文本高度 +  + 上下边距 + (单行文本高度和间距)
         * 一行显示:
         *         高度 = 文本高度 + 上下边距
         */
        // 先判断最后一行文本宽度是否能显示下,总宽度 - 左右间距 - 右侧时间和左右图标的宽度间距
        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) {
            // 绘制右侧发送状态的图标
            leftX = mWidth - readStateBitmap.width
            // 右侧发送状态图标较大 稍微偏下一点点
            topY = mHeight - readStateBitmap.height + space / 2
            canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)
        }
        // 绘制时间
        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) {

    }
}


基本上就是全部代码了,其中有自己不需要的进行剔除。
好久没更新,等有时间会进行整理,然后在给出git。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值