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

1675150565652.jpg

今天撸一个 文字显示不下换行显示的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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容