前言

在开发过程中,TextView中会出现一些特殊内容(如:部分内容颜色、字体、大小不同,并且部分字体可点击),写多个TextView又会显得麻烦,那又怎么解决这个问题呢?首页我们来看看TextView的源码,在android.text.style包下,有很多Span类,那么我们可以使用SpannableStringBuilder来设置Span,先看看实现效果:

ClickableSpan设置下划线颜色 给span加下划线_下划线

使用方式:
val builder = SpannableStringUtil.create(context!!)
            .setText("类似于HTML中的<li>标签的圆点效果")
            .setBullet(android.R.color.holo_green_dark)//添加圆点
            .setTextColor(android.R.color.black)
            .setText("\n设置文字左侧显示引用样式")
            .setQuote(android.R.color.holo_green_dark)
            .setText("\n设置下划线以及删除线")
            .setTextUnderline()//下划线
            .setTextStrikeThrough()//删除线
            .setTextColor(android.R.color.holo_red_dark)
            .setTextSize(20)
            .setTextStyle(Typeface.ITALIC)
            .setText("这个可以点击")
            .setTextColor(android.R.color.holo_green_dark)
            .setTextSize(21)
            .setTextStyle(Typeface.BOLD)//字体样式
            .setClick(tv_span1,{
                Log.e("data", it)
            },true)//设置点击
            .setText("这里设置下标")
            .setTextColor(android.R.color.holo_blue_dark)
            .setTextSubscript(6)//下标
            .setTextColor(android.R.color.holo_orange_dark)
            .setText("这里可以设置上标")
            .setScaleX(1.5f)
            .setTextColor(android.R.color.holo_green_dark)
            .setTextSuperscript(6)//上标
            .setTextColor(android.R.color.holo_blue_dark)
            .setDrawable(R.mipmap.ic_launcher,100,100)
            .setAlign(Layout.Alignment.ALIGN_OPPOSITE)
            .build()
        tv_span1.text = builder

        val builder1 = SpannableStringUtil.create(context!!)
            .setText("人的一生,有许多事情,是需要放在心里慢慢回味的,过去的就莫要追悔,一切向前看吧 任何打击都不足以成为你堕落的借口,即使你改变不了这个世界,你却依然可以改变自己,选择条正确的路永远走下去。")
            .setTextColor(android.R.color.holo_orange_dark)
            .setTextSize(17)
            .setTextLeadingMargin(30)//设置文本缩进
            .build()
        tv_span2.text = builder1
SpannableStringUtil
class SpannableStringUtil {

    private var builder = SpannableStringBuilder()
    private val infoList = mutableListOf<StringInfo>()


    companion object {
        private lateinit var context: Context
        fun create(mContext: Context): SpannableStringUtil{
            context=mContext
            return SpannableStringUtil()
        }
    }

    /**
     * 设置文字
     */
    fun setText(text:String):SpannableStringUtil{
        builder.append(text)
        infoList.add(StringInfo())
        val lastInfo=getLastInfo()
        lastInfo.startIndex=builder.length- text.length
        lastInfo.endIndex=builder.length
        return this
    }

    /**
     * 设置文字颜色
     */
    fun setTextColor(color:Int):SpannableStringUtil{
        val span= ForegroundColorSpan(ContextCompat.getColor(context,color))
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字背景颜色
     */
    fun setTextBgColor(color:Int):SpannableStringUtil{
        val span= BackgroundColorSpan(ContextCompat.getColor(context,color))
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字大小
     */
    fun setTextSize(size:Int):SpannableStringUtil{
        //true为size的单位是dip,false为px
        val span= AbsoluteSizeSpan(size,true)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字删除线
     */
    fun setTextStrikeThrough():SpannableStringUtil{
        val span= StrikethroughSpan()
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字下划线
     */
    fun setTextUnderline():SpannableStringUtil{
        val span= UnderlineSpan()
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文本缩进
     * @param first 首行的 margin left 偏移量。
     * @param rest 其他行的 margin left 偏移量。
     */
    fun setTextLeadingMargin(first:Int,rest:Int=0):SpannableStringUtil{
        val span= LeadingMarginSpan.Standard(dip2px(first.toFloat()), dip2px(rest.toFloat()))
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字下标,化学公式中用到
     */
    fun setTextSubscript(number:Int):SpannableStringUtil{
        val parcel = Parcel.obtain()
        parcel.writeInt(number)
        val span= SubscriptSpan(parcel)
        builder.append(number.toString())
        infoList.add(StringInfo())
        val stringInfo = getLastInfo()
        stringInfo.startIndex=builder.length- number.toString().length
        stringInfo.endIndex=builder.length
        stringInfo.styleList.add(span)
        parcel.recycle()
        return this
    }

    /**
     * 设置文字上标,数学公式中用到
     */
    fun setTextSuperscript(number:Int):SpannableStringUtil{
        val parcel = Parcel.obtain()
        parcel.writeInt(number)
        val span= SuperscriptSpan(parcel)
        builder.append(number.toString())
        infoList.add(StringInfo())
        val stringInfo = getLastInfo()
        stringInfo.startIndex=builder.length- number.toString().length
        stringInfo.endIndex=builder.length
        stringInfo.styleList.add(span)
        parcel.recycle()
        return this
    }

    /**
     * 设置文字横向缩放比例
     * @param proportion 缩放比例
     */
    fun setScaleX(proportion:Float):SpannableStringUtil{
        val span= ScaleXSpan(proportion)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字相对大小
     * @param proportion 文字大小比例
     */
    fun setTextRelativeSize(proportion:Float):SpannableStringUtil{
        val span= RelativeSizeSpan(proportion)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字对齐方式
     */
    fun setAlign(align:Layout.Alignment):SpannableStringUtil{
        val span= AlignmentSpan.Standard(align)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置文字左侧显示引用样式(一条竖线)
     * @param color 竖线颜色
     * @param stripeWidth 竖线宽度
     * @param gapWidth 文字与竖线距离
     */
    @SuppressLint("NewApi")
    fun setQuote(color:Int,stripeWidth:Int=5,gapWidth:Int=20):SpannableStringUtil{
        val span= QuoteSpan(ContextCompat.getColor(context,color),stripeWidth,gapWidth)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 类似于HTML中的<li>标签的圆点效果
     * @param gapWidth 圆点与文本的间距
     * @param color 圆点颜色
     * @param gapRadius 圆点半径
     */
    @SuppressLint("NewApi")
    fun setBullet(color:Int,gapWidth:Int=10,gapRadius:Int=10):SpannableStringUtil{
        val span= BulletSpan(gapWidth, ContextCompat.getColor(context,color),gapRadius)
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 模糊效果
     * @param radius 模糊半径(需大于0)
     * @param style  模糊样式
     */
    fun setBlur(radius:Float,style:BlurMaskFilter.Blur):SpannableStringUtil{
        val span= MaskFilterSpan(BlurMaskFilter(radius,style))
        val stringInfo = getLastInfo()
        stringInfo.styleList.add(span)
        return this
    }

    /**
     * 设置图片
     * @param drawableWidth 宽
     * @param drawableHeight 高
     */
    fun setDrawable(drawableId:Int,drawableWidth:Int,drawableHeight:Int):SpannableStringUtil{
        val drawable = ContextCompat.getDrawable(context,drawableId)!!
        drawable.setBounds(0, 0, drawableWidth, drawableHeight)
        val span=ImageSpan(drawable)
        builder.append("0")
        infoList.add(StringInfo())
        val lastInfo = getLastInfo()
        lastInfo.startIndex=builder.length -1
        lastInfo.endIndex=builder.length
        lastInfo.styleList.add(span)
        return this
    }

    /**
     * 设置点击事件
     * @param isUnderline 是否去掉下划线,不传值默认去掉
     */
    fun setClick(textView:TextView, listener: (clickStr:String) -> Unit,isUnderline:Boolean=false):SpannableStringUtil{
        textView.movementMethod = LinkMovementMethod.getInstance()
        textView.highlightColor = 0x00000000
        val lastInfo = getLastInfo()
        val span=object : ClickableSpan() {
            override fun updateDrawState(ds: TextPaint) {
                super.updateDrawState(ds)
                if (!isUnderline) {
                    // 去除下划线
                    ds.isUnderlineText = false
                    ds.bgColor = Color.TRANSPARENT
                }
            }
            override fun onClick(view: View) {
                listener.invoke(builder.subSequence(lastInfo.startIndex,lastInfo.endIndex).toString())
            }
        }
        lastInfo.styleList.add(span)

        return this
    }

    /**
     * 设置文字样式
     * @param style
     * Typeface.NORMAL 正常
     * Typeface.BOLD 粗体
     * Typeface.ITALIC 斜体
     * Typeface.BOLD_ITALIC 粗体+斜体
     */
    fun setTextStyle(style:Int):SpannableStringUtil{
        val stringInfo = getLastInfo()
        val span = StyleSpan(style)
        stringInfo.styleList.add(span)
        return this
    }

    fun build(): SpannableStringBuilder {
        infoList.forEach {
            if (it.styleList.size > 0) {
                for (i in 0 until it.styleList.size) {
                    val style = it.styleList[i]
                    builder.setSpan(
                        style,
                        it.startIndex,
                        it.endIndex,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE
                    )
                }
            }
        }
        return builder
    }

    private fun getLastInfo(): StringInfo {
        return infoList[infoList.size - 1]
    }

    private class StringInfo{
        var startIndex=0
        var endIndex=0
        var styleList= mutableListOf<Any>()
    }

    private  fun dip2px(dpValue: Float):Int{
       return (dpValue *  context.resources.displayMetrics.density + 0.5f).toInt()
    }
}