如何在ImageSpan上面绘制文本?

简介:TextOnImageSpan

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

github链接https://github.com/Coder-HuangBH/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链接https://github.com/Coder-HuangBH/TextOnImageSpan
本文链接https://blog.csdn.net/weixin_44337681/article/details/131062779

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白白猪

感谢兄弟的支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值