超能力文本:探索Span机制的多彩世界(二)

一 写在开头

上一篇文章中介绍了一些 Span理论知识,包括

  • Span 的定义、分类;
  • 富文本处理类:SpannedString、SpannableString 、SpannableStringBuilder
  • setSpan(Object what, int start, int end, int flags)中各个参数的含义及使用示例。

文章地址:超能力文字:探索Span机制的多彩世界(一),本篇继续通过示例介绍各个Span的用法。坐稳了,开始发车

二 Span示例

文本如下:

ini 复制代码
      private const val SPAN_STR: String = 
             "北国风光,千里冰封,万里雪飘。\n\n" +
             "望长城内外,惟余莽莽;大河上下,顿失滔滔。\n\n山舞银蛇,原驰蜡象,欲与天公试比高。\n\n" +
             "须晴日,看红装素裹,_ 分外妖娆。\n\n江山如此多娇,引无数英雄竞折腰。\n\n" +
             "惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。\n\n" +
              "一代天骄,成吉思汗,只识弯弓射大雕。\n\n" + 
              "俱往矣,数风流人物,还看今朝。"

        const val SPAN_STR2 = "锄禾日当午,\n\t汗滴禾下土。\n谁知盘中餐,\n\t粒粒皆辛苦。"
        const val SEG_1 = "北国风光"
        const val SEG_2 = "千里冰封"
        const val SEG_3 = "万里雪飘"
        const val SEG_4 = "望长城内外"
        const val SEG_5 = "惟余莽莽"
        const val SEG_6 = "大河上下"
        const val SEG_7 = "顿失滔滔"
        const val SEG_8 = "山舞银蛇"
        const val SEG_9 = "原驰蜡象"
        const val SEG_10 = "欲与天公试比高"
        const val SEG_11 = "须晴日"
        const val SEG_12 = "看红装素裹"
        const val SEG_13 = "_ 分外妖娆"
        const val SEG_14 = "江山如此多娇"
        const val SEG_15 = "引无数英雄竞折腰"
        const val SEG_16 = "惜秦皇汉武"
        const val SEG_17 = "略输文采"
        const val SEG_18 = "唐宗宋祖"
        const val SEG_19 = "稍逊风骚"
        const val SEG_20 = "一代天骄"
        const val SEG_21 = "成吉思汗"
        const val SEG_22 = "只识弯弓射大雕"
        const val SEG_23 = "俱往矣"
        const val SEG_24 = "数风流人物"
        const val SEG_25 = "还看今朝。"

通过 TextView 原始展示如下:

ini 复制代码
val spanBuilder = SpannableStringBuilder(SPAN_STR)
tvSpan.text = spanBuilder

2.1 影响外观的Span

整体示例效果图:

2.1.1、ForegroundColorSpan

字体颜色样式,用于改变字体颜色

less 复制代码
spanBuilder.setSpan(
            ForegroundColorSpan(Color.RED),
            index1, index1 + SEG_1.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

效果图:

2.1.2、BackgroundColorSpan

背景色样式,可以用来设定文本的背景色;

less 复制代码
spanBuilder.setSpan(
            BackgroundColorSpan(Color.GRAY),
            index2, index2 + SEG_2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

效果图:

2.1.3、UnderlineSpan

下划线样式,给文本添加下划线;

less 复制代码
spanBuilder.setSpan(
     UnderlineSpan(), index3, index3 + SEG_3.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
   )

效果图:

2.1.4、MaskFilterSpan

应用指定的 MaskFilter 到文本,可以实现一些特殊的效果,如模糊(BlurMaskFilter)等。

MaskFilterSpan(MaskFilter filter)设置滤镜Span,参数filter是MaskFilter类型。 BlurMaskFilter是MaskFilter子类,可以应用于Paint的一个滤镜。可以将绘制的图形进行模糊处理,以达到一些特定的视觉效果。

1、radius : 模糊半径。数值越大,模糊的范围越广。

2、style是BlurMaskFilter.Blur枚举,表示模糊的类型,包括以下四种:

--- a、NORMAL:普通模糊,应用于整个图像。

--- b、SOLID:只绘制模糊的区域,其他区域为透明。

--- c、OUTER:外部模糊,只在图像外部模糊。

--- d、INNER:内部模糊,只在图像内部模糊。

less 复制代码
 spanBuilder.setSpan(
            //1、NORMAL:在原始边界内外都进行模糊处理。
            MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.NORMAL)),
            index4, index4 + SEG_4.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
 spanBuilder.setSpan(
            //2、SOLID:在原始边界内部填充实心颜色,在边界外进行模糊处理。
            MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.SOLID)),
            index5, index5 + SEG_5.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
 spanBuilder.setSpan(
            //3、OUTER:在边界内部不进行绘制,在边界外进行模糊处理。
            MaskFilterSpan(BlurMaskFilter(2F, BlurMaskFilter.Blur.OUTER)),
            index6, index6 + SEG_6.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
 spanBuilder.setSpan(
            //4、INNER:在边界内部进行模糊处理,在边界外不进行绘制。
            MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.INNER)),
            index7, index7 + SEG_7.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

执行结果:

2.1.5、StrikethroughSpan

删除线样式,给文本添加删除线效果;

less 复制代码
spanBuilder.setSpan(
            StrikethroughSpan(),
            index8, index8 + SEG_8.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
 )

效果图:

2.1.6、ClickableSpan

设置点击事件,注意点击相关的必须设置textView.movementMethod = LinkMovementMethod.getInstance(),否则点击不生效。

kotlin 复制代码
spanBuilder.setSpan(
      CustomClickSpan {showToast("自定义ClickableSpan点击: $SEG_9") },
      index9, index9 + SEG_9.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
  )

//自定义ClickableSpan
class CustomClickSpan(private val click: (View) -> Unit) : ClickableSpan() {

    override fun onClick(widget: View) {
        click.invoke(widget)
    }

    /**
     * 通过修改TextPaint来修改文本状态,如修改文本颜色、下划线是否展示等
     * @param paint
     */
    override fun updateDrawState(paint: TextPaint) {
        paint.color = Color.BLUE
        paint.isUnderlineText = false
    }
}

通过自定义 ClickableSpan 并重写 updateDrawState() 修改 TextPaint ,去掉了默认的下划线、修改了文字颜色。执行结果:

,点击文字时就会触发 onClick 回调了。

2.1.7、URLSpan

点击文字时进行链接跳转

less 复制代码
spanBuilder.setSpan(
        URLSpan("https://www.baidu.com"),
        index10, index10 + SEG_10.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
   )

效果图:

2.2 影响度量的Span

整体示例效果:

2.2.1、StyleSpan

字体样式,如通过传入的 Typeface 设置正常、粗体、斜体、加粗并倾斜等样式;

less 复制代码
//设置StyleSpan:加粗、斜体等,如Typeface.NORMAL、BOLD、ITALIC、BOLD_ITALIC
spanBuilder.setSpan(
            StyleSpan(Typeface.BOLD_ITALIC),
            index11, index11 + SEG_11.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

执行结果:

2.2.2、TypefaceSpan

设置不同样式的文本字体

scss 复制代码
val customTypeface = Typeface.create(
            ResourcesCompat.getFont(this, R.font.iconfont_guide),
            Typeface.BOLD_ITALIC
        )
/**
 * TypefaceSpan构造函数有两个:
 * 1、TypefaceSpan(String family)字体族,最终通过Typeface.create(family, style)创建Typeface
 * 2、TypefaceSpan(Typeface typeface)直接传入Typeface
 * 当通过TypefaceSpan(String family)创建时,以前的Typeface都会保留并与新创建的合并;而通过TypefaceSpan(Typeface typeface)创建的会直接覆盖之前的Typeface设置。
*/
spanBuilder.setSpan(
            //TypefaceSpan(customTypeface), //API>=28以上才有
            TypefaceSpan("serif"), //系统自带的有normal,sans,monospace,serif
            index12, index12 + SEG_12.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

执行结果:

2.2.3、ImageSpan

图片样式,用于在文本中插入图片;

kotlin 复制代码
val imgDrawable =
    ResourcesCompat.getDrawable(resources, R.drawable.icon_flower, null)

imgDrawable?.let { 
    it.setBounds(0, 0, 40.dp2px(), 40.dp2px())
    spanBuilder.setSpan(
           CenterImageSpan(it),
           index13, index13 + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
       )
 }
        
//自定义ImageSpan
class CenterImageSpan(drawable: Drawable, verticalAlignment: Int = ALIGN_CENTER) :
    ImageSpan(drawable, verticalAlignment) {

 private var mDrawableRef: WeakReference<Drawable>? = null

    /**
     * @param paint Paint instance.
     * @param text Current text.
     * @param start Start character index for span.
     * @param end End character index for span.
     * @param fm Font metrics, can be null.
     * @return Width of the span.
     */
override fun getSize(
        paint: Paint,
        text: CharSequence?,
        start: Int,
        end: Int,
        fm: Paint.FontMetricsInt?,
    ): Int {
        val d = getCachedDrawable()
        val rect = d?.bounds
        rect?.let {
            if (fm != null) {
                val fmInt = paint.fontMetricsInt
                val imgHeight = d.bounds.height() //图片高度
                val textHeight = fm.bottom - fm.top //文字行高度
                val halfDiffer = (imgHeight - textHeight) / 2
                if (imgHeight > textHeight && verticalAlignment == ALIGN_CENTER) {
                    fm.ascent = fmInt.ascent - halfDiffer
                    fm.descent = fmInt.descent + halfDiffer
                    fm.top = fmInt.top - halfDiffer
                    fm.bottom = fmInt.bottom + halfDiffer
                } else {
                    fm.ascent = -rect.bottom
                    fm.descent = 0
                    fm.top = fm.ascent
                    fm.bottom = 0
                }
            }
            return rect.right
        }
        return 0
    }

private fun getCachedDrawable(): Drawable? {
        val wr: WeakReference<Drawable>? = mDrawableRef
        var d: Drawable? = null
        if (wr != null) {
            d = wr.get()
        }
        if (d == null) {
            d = drawable
            mDrawableRef = WeakReference<Drawable>(d)
        }
        return d
    }
}

执行结果:

注意,这里使用的是自定义ImageSpan,目的是不管图片的大小,图片都能够正常的和文字进行居中展示;而如果使用默认ImageSpan时,如果图片的高度大于文字本身,那么居中展示就会失效。

2.2.4、RelativeSizeSpan

相对大小样式,相对于原始大小调整文本的大小;

kotlin 复制代码
//自定义RelativeSizeSpan
spanBuilder.setSpan(
         RelativeSizeColorSpan(0.8f, Color.RED),
         index14, index14 + SEG_14.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
 )

/**
 * 自定义Span
 * @param size : 文本被缩放的比例
 */
class RelativeSizeColorSpan(size: Float, @ColorInt val color: Int = Color.GRAY) :
    RelativeSizeSpan(size) {

    /**
     * 在绘制文本时更新样式的绘制状态,例如文本颜色、下划线等。
     */
    override fun updateDrawState(paint: TextPaint) {
        super.updateDrawState(paint)
        paint.color = color
        paint.isUnderlineText = true
    }
}

这里不仅想改变大小,还想改变样式(文本颜色、下划线),所以直接通过简单的自定义RelativeSizeSpan,然后在复写的 updateDrawState() 里设置即可,执行结果(箭头方向):

2.2.5、AbsoluteSizeSpan

绝对大小样式,以指定的像素大小设置文本的大小;

java 复制代码
 spanBuilder.setSpan(
            /**
             * @param size 文本的绝对值大小,单位是像素px
             * @param dip  如果为true,则size变以为dp为单位;否则还是以像素px为单位。
             */
            AbsoluteSizeSpan(20, true),
            index15, index15 + SEG_15.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
   )

执行结果(箭头方向):

2.2.6、ScaleXSpan

在水平方向上缩放文本;

less 复制代码
spanBuilder.setSpan(
            ScaleXSpan(2f),
            index16, index16 + SEG_16.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

执行结果:

2.2.7、SubscriptSpan

将文本baseline的位置向下移动,用于显示下标文本;

less 复制代码
//将文本baseline的位置向下移动
spanBuilder.setSpan(
     SubscriptSpan(),
     index17 + SEG_17.length / 2, index17 + SEG_17.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
  )

执行结果:

2.2.8、SuperscriptSpan

将文本baseline的位置向上移动,用于显示上标文本;

less 复制代码
//将文本baseline的位置向上移动
spanBuilder.setSpan(
    SuperscriptSpan(),
     index18 + SEG_18.length / 2, index18 + SEG_18.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
 )

执行结果:

2.2.9、TextAppearanceSpan

设置文本字体、大小、样式和颜色等

javascript 复制代码
spanBuilder.setSpan(
            TextAppearanceSpan(this, R.style.textAppearanceSpan),
            index19, index19 + SEG_19.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )

//styles.xml
<resources>
    <style name="textAppearanceSpan" parent="@android:style/TextAppearance">
        <item name="android:textColor">@color/result_points</item>
        <item name="android:textSize">20sp</item>
        <!-- italic、bold、normal -->
        <item name="android:textStyle">italic</item>
        <!-- monospace、serif、sans-serif -->
        <item name="fontFamily">serif</item>
    </style>
<resources>

执行结果(箭头方向):

2.3 影响段落的Span

2.3.1、LeadingMarginSpan

段落缩进,即为文本添加指定的行首缩进;

less 复制代码
spanBuilder.setSpan(
            /**
             * first: 每个段落的首行缩进
             * mRest: 每个段落的其他行缩进
             */
            LeadingMarginSpan.Standard(20.dp2px(), 5.dp2px()),
            0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
  )

执行结果: 可以看到每个段落的首行都所进行20dp的缩进,因为每个段落都只有一行,所以针对一个段落中mRest 其他行的缩进没有体现出来而已。

2.3.2、LineBackgroundSpan

LineBackgroundSpan改变行的背景色,如果[start,end)不够一行按一行处理

less 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    spanBuilder.setSpan(
                LineBackgroundSpan.Standard(Color.YELLOW),
                0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
      )
}

执行结果:

2.3.3、LineHeightSpan

改变段落的行高,可用于调整行与行之间的间距。注意,LineHeightSpan 改变的是整个段落的行高,即使它只覆盖段落的一部分。

less 复制代码
//LineHeightSpan改变段落的行高
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    //注意,LineHeightSpan将改变整个段落的行高,即使它只覆盖段落的一部分。
    spanBuilder.setSpan(
           LineHeightSpan.Standard(20.dp2px()),
           0, SPAN_STR2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
      )
 }

执行结果:

2.3.4、DrawableMarginSpan

在文本的行首添加一个带有指定边距的 Drawable

ini 复制代码
val imgDrawable = ResourcesCompat.getDrawable(resources, R.drawable.icon_arrow_pull, null)

spanBuilder.setSpan(
            DrawableMarginSpan(imgDrawable!!, 10.sp2px()),
            0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

执行结果:

2.3.5、IconMarginSpan

IconMarginSpanDrawableMarginSpan类似,在文本的行首添加一个带有指定边距的Bitmap。

scss 复制代码
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_arrow_pull), 10.sp2px()

spanBuilder.setSpan(
     IconMarginSpan(bitmap),
     0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
2.3.6、QuoteSpan

QuoteSpan 可以在文本开始的地方添加引用样式(一个垂直的线条)。

scss 复制代码
val quoteSpan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            /**
             * QuoteSpan(int color, int stripeWidth, int gapWidth)
             * @param color 垂直线条颜色。缺省情况下,条带颜色为0xff0000ff
             * @param stripeWidth 线条的宽度,以像素为单位。默认值是2px。
             * @param gapWidth 线条和段落之间的距离,以像素为单位。默认值是2px。
             */
            QuoteSpan(Color.RED, 20, 40)
        } else {
            QuoteSpan(Color.RED)
        }
spanBuilder.setSpan(quoteSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

执行结果:

2.3.7、BulletSpan

给文本添加项目符号(圆点样式)

kotlin 复制代码
private fun getBulletSpan(): BulletSpan {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            /**
             * BulletSpan(int gapWidth, int color, int bulletRadius)
             * @param gapWidth: 项目符号和段落之间的距离,单位是px,默认2px
             * @param color:项目符号颜色,默认是文本颜色
             * @param bulletRadius:符号半径,单位是px,默认是4px
             */
            BulletSpan(20, Color.RED, 10)
        } else {
            BulletSpan(20, Color.RED)
        }
    }

//第一行设置圆点
spanBuilder.setSpan(getBulletSpan(), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val index23 = SPAN_STR.indexOf(SEG_23)
//最后一行设置圆点
spanBuilder.setSpan(getBulletSpan(), 
     index23, index23 + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)

执行结果:

2.3.8、AlignmentSpan

文本对齐方式,AlignmentSpan.Standard是其默认实现,其构造函数AlignmentSpan.Standard(Layout.Alignment align) 中的 alignLayout.Alignment枚举类型参数,包括三种情况:ALIGN_CENTER居中、 ALIGN_NORMAL正常、ALIGN_OPPOSITE相反方向。如:

less 复制代码
spanBuilder.setSpan(
    AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 
 )

可以看到文本已经居中展示了。

2.3.9、TabStopSpan

段落中第一行的左侧偏移量(与\t有关系),默认实现为TabStopSpan.Standard

bash 复制代码
val SPAN_STR2 = "锄禾日当午,\n\t汗滴禾下土。\n谁知盘中餐,\n\t粒粒皆辛苦。"
val spanBuilder = SpannableStringBuilder(SPAN_STR2)
spanBuilder.setSpan(TabStopSpan.Standard(40.dp2px()), 
         0, SPAN_STR2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
     )
tvSpan.text = spanBuilder     

执行结果:

相关推荐
每次的天空28 分钟前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭1 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日2 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安2 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑2 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟6 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡7 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi008 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil9 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你9 小时前
Android View的绘制原理详解
android