Android Span 实现 TextView 标签混排

263 阅读1分钟
class TagSpan(
    private val mTextSize: Float,
    private val mHorizontalPadding: Float,
    private val mVerticalPadding: Float,
    private val mMargin: Float,
    private val mBorderWidth: Float,
    private val mRadius: Float
) : ReplacementSpan() {

    private var mSpanWidth: Float = 0f

    private val mPaint: Paint by lazy {
        Paint(Paint.ANTI_ALIAS_FLAG).apply {
            textSize = mTextSize
            color = Color.RED
            textAlign = Paint.Align.CENTER
        }
    }

    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        val spanText = text?.substring(start, end) ?: return 0
        mSpanWidth = mPaint.measureText(spanText, 0, spanText.length) + 2 * mHorizontalPadding + 2 * mMargin
        return mSpanWidth.toInt()
    }

    override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        // 绘制标签矩形边框
        drawRect(canvas, text, start, end, x, top, y, bottom, paint)
        // 绘制文字
        drawText(canvas, text, start, end, x, top, y, bottom, paint)
    }

    /**
     *
     * @param canvas 画布
     * @param text 整个 TextView 的文字内容
     * @param start 在整个 TextView 中的起始位置
     * @param end span 在整个 TextView 中的结束位置
     * @param x span 起始位置的 x 坐标
     * @param top 整个 TextView 的顶部坐标
     * @param y 基线 y 坐标
     * @param bottom 整个 TextView 的底部坐标
     * @param paint 画笔
     */
    private fun drawText(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        mPaint.style = Paint.Style.FILL
        val spanText = text?.substring(start, end) ?: return
        val tagFontMetrics: Paint.FontMetrics = mPaint.fontMetrics
        val baselineY = (bottom + top) / 2f + (tagFontMetrics.descent - tagFontMetrics.ascent) / 2f - tagFontMetrics.descent
        canvas.drawText(spanText, x + mSpanWidth / 2f, baselineY, mPaint)
    }

    private fun drawRect(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        Log.d("ddd", "drawRect() called with: canvas = $canvas, text = $text, start = $start, end = $end, x = $x, top = $top, y = $y, bottom = $bottom, paint = $paint")
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeWidth = mBorderWidth
        val fontMetrics: Paint.FontMetricsInt = mPaint.fontMetricsInt
        val left = x + mMargin
        val right = x + mSpanWidth - mMargin
        val height = fontMetrics.descent - fontMetrics.ascent
        val rectTop = -height / 2f - mVerticalPadding
        canvas.save()
        // dy = top + (bottom - top) / 2f 即为 (bottom + top) / 2f
        canvas.translate(0f, (bottom + top) / 2f)
        canvas.drawRoundRect(RectF(left, rectTop, right, height / 2f + mVerticalPadding), mRadius, mRadius, mPaint)

        canvas.restore()
    }
}

TagSpan 引用

val textSize = 12f.sp
        val padding = 10f.dp
        val verticalPadding = 3f.dp
        val margin = 15f.dp
        val borderWidth = 1f.dp
        val radius = 5f.dp
        val textView = findViewById<TextView>(R.id.textView)
        val tags = listOf("热门", "最新")
        val content = SpannableStringBuilder("实现图文混排布局实现图文混排布局")
        tags.forEach {
            content.append(it)
            content.setSpan(TagSpan(textSize, padding, verticalPadding, margin, borderWidth, radius), content.length - 2, content.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        }
        textView.text = content