美文网首页
Android 自定义View 一行显示不下换行显示

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

作者: Db_z | 来源:发表于2023-01-30 18:00 被阅读0次
    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。

    相关文章

      网友评论

          本文标题:Android 自定义View 一行显示不下换行显示

          本文链接:https://www.haomeiwen.com/subject/bvimhdtx.html