如何在ImageSpan上面绘制文本?

171 阅读3分钟

简介:TextOnImageSpan

前阵子碰到一个需求:在文本中内嵌图标,并在图标上面绘制特定文本。很自然就会想到用SpannableString去实现,但经过一系列的研究捣鼓,发现根本就没有能在图标上绘制文本的span类,于是乎我去翻ImageSpan跟其它主要span类的源码,发现它们的绘制原理跟自定义View差不多,都是在draw方法里画东西,所以我就自己继承ImageSpan,实现了在图标上面绘制特定文本的Span。   我管它叫——TextOnImageSpan。

效果图

使用示例

  在使用上还是与其它Span一致,加入到SpannableString或SpannableStringBuilder后,设置进TextView即可。

	val ssb = SpannableStringBuilder()
		
	var span = TextOnImageSpan(this, R.drawable.location, ALIGN_CENTER)
	span.setFakeBoldText(true)
	span.setTextColor(Color.BLUE)
	span.setTextSize(DisplayUtil.sp2px(this, 16F).toFloat())
	span.setImageHeight(DisplayUtil.dip2px(this, 50f))
	span.mOffsetX = DisplayUtil.dip2px(this, 15f).toFloat()
	span.mOffsetY = DisplayUtil.dip2px(this, 15f).toFloat()
	ssb.append("定位", span, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
	
	textView.text = ssb

源码

  • 重写了getSize方法,根据设置的文本与图像之间的最大宽高计算出span最终的尺寸。重写了draw方法,修改了原有的部分图片绘制逻辑,然后再加入了文本的绘制逻辑。
  • 图片的垂直方向位置计算继承了原有ImageSpan的计算规则,而水平方向暂时默认居中绘制,不支持修改。而文本的位置则是相对于图片的位置居中。
  • 支持设置Image的height属性,设置之后内部将自动等比缩放drawable。
  • 对于文本的参数配置:可以修改文字大小、文字颜色、是否加粗,另外还提供了mOffsetX和mOffsetY两个参数,允许使用者对文本的绘制位置进行自定义的偏移(在相对于图片位置居中的前提下)。
  • 最重要的就是draw方法,大概的工作逻辑如下
    //先计算出该Span的文本进行measure,得到文本的宽度;
    //根据文本宽度进行X轴偏移量计算,得到transX;
    //再根据mVerticalAlignment的模式,进行Y轴的偏移量计算,得到transY;
    //如果说文本的高度大于该Span高度(112行),则会先进行一次Y轴的偏移,让图片与文本都能绘制在正确的位置上;
    //然后根据对transX与transY,对canvas进行偏移;
    //偏移之后首先绘制底部image,然后绘制顶部文本;
    //最终将canvas归位。
class TextOnImageSpan : ImageSpan {

    private var mDrawable : Drawable? = null
    private val mTextPaint: Paint = Paint()
    private var mTextHeight = 0f
    private var mBaseLineOffset = 0f
    var mOffsetX = 0f
    var mOffsetY = 0f

    constructor(context: Context, bitmap : Bitmap) : super(context, bitmap)

    constructor(context: Context, bitmap : Bitmap, verticalAlignment : Int)
            : super(context, bitmap, verticalAlignment)

    constructor(draw : Drawable) : super(draw)

    constructor(draw : Drawable, verticalAlignment : Int) : super(draw, verticalAlignment)

    constructor(context: Context, uri : Uri) : super(context, uri)

    constructor(context: Context, uri : Uri, verticalAlignment : Int)
            : super(context, uri, verticalAlignment)

    constructor(context: Context, @DrawableRes resourceId: Int) : super(context, resourceId)

    constructor(context: Context, @DrawableRes resourceId: Int, verticalAlignment : Int)
            : super(context, resourceId, verticalAlignment)

    init {
        mTextPaint.color = Color.WHITE
        mTextPaint.textSize = 20f
        computerTextParams()
    }

    private fun computerTextParams() {
        val fm = mTextPaint.fontMetrics
        mTextHeight = fm.descent - fm.ascent
        mBaseLineOffset = abs(fm.ascent)
    }

    fun setTextSize(size: Float) {
        mTextPaint.textSize = size
        computerTextParams()
    }

    fun setTextColor(color: Int) {
        mTextPaint.color = color
    }

    fun setFakeBoldText(bold: Boolean) {
        mTextPaint.isFakeBoldText = bold
    }

    fun setImageHeight(height : Int) {
        drawable?.run {
            if (bounds.height() != height) {
                val scale = height.toFloat() / bounds.height()
                setBounds(bounds.left, bounds.top,
                    bounds.left + (bounds.width() * scale).roundToInt(), bounds.top + height)
            }
            mDrawable = this
        }
    }

    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int,
                         fm: Paint.FontMetricsInt?): Int {

        val d = (mDrawable ?: drawable) ?: return 0

        val rect = d.bounds

        fm?.run {
            ascent = -(max(mTextHeight.toInt(), rect.bottom))
            descent = 0
            top = ascent
            bottom = 0
        }

        var size = rect.right

        text?.takeIf { it.isNotEmpty() }?.let {
            size = max(size, (mTextPaint.measureText(text, start, end)).roundToInt())
        }

        return size
    }

    override fun getDrawable(): Drawable? {
        return mDrawable ?: super.getDrawable()
    }

    override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int,
        x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {

        val draw = (mDrawable ?: drawable) ?: return

        canvas.save()

        val totalWidth = draw.bounds.width().toFloat()
        val textWidth = mTextPaint.measureText(text, start, end)

        var transX = x
        if (textWidth > draw.bounds.width()) {
            transX += (textWidth - draw.bounds.width()) / 2
        }
        var transY = bottom - draw.bounds.bottom
        if (mVerticalAlignment == ALIGN_BASELINE) {
            transY -= paint.fontMetricsInt.descent
        } else if (mVerticalAlignment == ALIGN_CENTER) {
            transY = top + (bottom - top) / 2 - draw.bounds.height() / 2
        }
        if (mTextHeight > draw.bounds.height()) {
            canvas.translate(0f, -(mTextHeight - draw.bounds.height()) / 2)
        }
        canvas.translate(transX, transY.toFloat())

        //drawImage
        draw.draw(canvas)

        //drawText
        text?.takeIf { it.isNotEmpty() }?.let {
            val startX = (totalWidth - textWidth) / 2 + mOffsetX

            val mTextBaseLine = (draw.bounds.height() - mTextHeight) / 2 + mBaseLineOffset
            val startY = mTextBaseLine + mOffsetY

            canvas.drawText(text, start, end, startX, startY, mTextPaint)
        }

        canvas.restore()
    }

}

总结

整体的逻辑较为简单,就没写什么注释了,如果有感兴趣的同学,可以上github拉源码下来自己试试看。创作不易,觉得不错的话,就来个一键三连吧,感谢大家!
github链接github.com/Coder-Huang…