Android中强大的标记对象-Span

一、前言

    在TextView中对显示的文本进行某些格式化的时候,很多人会首相想到使用html文本,对于简单一点的可能会用多个TextView进行拼合。然而这些都有很多的局限性,比如html文本可编辑性很差,多个TextView拼接的方式对于组合不定也是非常不方便的。Android提供了一种强大的标记对象-Span,可用于在字符或段落级别对文本设置样式。通过将 Span 附加到文本对象上,您能够以各种方式更改文本,包括添加颜色、使文本可点击、调整文本大小以及以自定义方式绘制文本。Span 还可以更改TextPaint 属性、在 Canvas 上绘制,甚至更改文本布局。

    Android 提供多种类型的 Span,其中涵盖各种常见的文本样式格式。您也可以创建自己的 Span,以应用自定义样式。

喜欢阅读官方文档的也可以参考官方文档:Google官方文档入口

二、在应用中使用标记(Span)

2.1 标记(Span)的类型

    在Android中,提供了几种Span,他们主要区分在于文本本身是否可变、文本标记是否可变、以及包含Span数据的底层数据结构差异,主要分为以下几种:

文本可变标记可变底层数据结构
SpannedString线性数组
SpannableString线性数组
SpannableStringBuilder区间树

所有这些类都实现 Spanned 接口。SpannableStringSpannableStringBuilder 同时实现了 Spannable 接口。

2.2 如何使用标记(Span)

那么,该如何选择使用Span的类型呢?

  • 如果再创建后不需要对文本或者标记进行修改,使用SpannedString
  • 如果文本本身只读不变,并且需要将少量的标记动态附加到单个文本对象上,使用SpannableString
  • 如果需要在创建后修改文本,并且需要将标记动态附加到文本,使用SpannableStringBuilder
  • 如果需要将大量的标记附加到文本,无论文本本身是否只读,都是用SpannableStringBuilder

    要应用 Span,先要创建一个Spannable对象,然后对 Spannable 对象调用 setSpan(Object what, int start, int end, int flags)。接口参数说明如下:

参数名类型说明备注
whatObject应用于文本的Spna类型android在android.text.style包名下定义了很多预定义的Span类型,开发者也可以自定义自己的Span
start intSpan应用的开始位置索引从0开始,标记生效于该位置的字符
end intSpan应用的结束位置标记生效到该位置前一个字符,不包括该位置的字符
flagsint标记值该标记值在 Spanned 接口中定义,用来指定在Span边界(及在 startend索引处)处插入文本时,Span是否展开并将插入的文本包含在内。字段定义格式类似SPAN_<start>_<end>,分为 INCLUSIVE (包含) 和 EXCLUSIVE (不包含)两种,如Spanned.SPAN_EXCLUSIVE_INCLUSIVE表示在 start处插入字符,Span不会扩展包含插入的字符,插入的字符不会拥有Span样式,end处插入字符,Span将会扩展包含插入的字符,插入的字符拥有Span样式。更多详情参考对应字段声明。
SpannableStringBuilder(nums).also {
    // 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符
    it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

执行的效果如下图:
前景色Span

  • 前面的例子是SPAN_INCLUSIVE_EXCLUSIVE,当在start出插入文本时,Span会扩展到新插入的文本
SpannableStringBuilder(nums).also {
    // 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符
    it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    it.insert(0, "ABC")
    tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

执行的效果如下图:
Span临界处插入字符

同样可以用验证在end处插入字符,Span不会扩展到新插入的字符。

  • 同时设置多个Span
    可以使用多个Span叠加事项想要的效果,例如:前景色+加粗字体
SpannableStringBuilder(nums).also {
    // 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符
    it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    it.setSpan(StyleSpan(Typeface.BOLD), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

同时设置多个Span

三、Android Span的类型

Android 在 android.text.style 软件包中提供了超过 20 种 Span 类型。Android对Span的分类主要有两种方式:

  • Span 如何影响文本:Span 可能会影响文本外观(如:前景色、背景色)或文本指标(如:字号,字体)。
  • Span 作用范围:一些 Span 可应用于单个字符,还有一些 Span 必须应用于整个段落。
    Span分类:字符与段落,外观与指标

3.1 影响文本外观的 Span

有些Span会改变文本的外观,例如更改文本前景色或背景颜色以及添加下划线或删除线,这些Span都会扩展 CharacterStyle 类。

// 设置文本的前景色
SpannableStringBuilder(nums).also {
    // 这里的start是3,end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符
    it.setSpan(UnderlineSpan(), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

3.2 影响文本指标的 Span

Span 还会影响文本指标,例如行高和文本大小。这些 Span 都会扩展 MetricAffectingSpan 类。

// 基于原来的字体大小扩大1.5倍
SpannableStringBuilder(nums).also {
    // 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符
    it.setSpan(RelativeSizeSpan(1.5f), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

3.3 影响单个字符的 Span

有些Span 会影响字符级别的文本。例如,您可以更新背景颜色、样式或大小等。影响单个字符的 Span 会扩展 CharacterStyle 类。

// 设置文字的背景色
SpannableStringBuilder(nums).also {
    // 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符
    it.setSpan(BackgroundColorSpan(Color.GREEN), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

以下代码示例将 BackgroundColorSpan 附加到了文本中的部分字符:

3.4 影响段落的 Span

有些Span 会影响段落级别的文本,例如更改整个文本块的对齐方式或边距。这些 Span 实现 ParagraphStyle接口。

注意:使用影响段落的 Span 时,您必须将它们附加到整个段落(不包括末尾换行符)。如果您尝试将段落 Span 应用于除段落以外的其他内容,则这些 Span不会是生效。

// 设置段落文字对齐方式
SpannableStringBuilder(nums + "\n" + "34567" + "\n" + "123").also {
    // Span应用于整个段落
    it.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, it.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    tvContent.setText(it)
}

四、创建自定义的 Span

    如果Android提供的 Span 无法满足您的需求,那么您可以实现自定义的 Span。实现自定义的 Span 时,您需要确定您的 Span 的类型(是否会影响字符或段落级别的文本,以及它是否会影响文本的布局或外观),这有助于您确定自定义的 Span 类需要扩展的基类以及可能需要实现的接口。请参考下表:

使用场景类或接口备注
您的 Span 会影响字符级别的文本CharacterStyle
您的 Span 会影响段落级别的文本ParagraphStyle接口
您的 Span 会影响文本外观UpdateAppearance接口
您的 Span 会影响文本指标UpdateLayout接口

下面是一个简单的自定义 Span 的例子(文本扩大并且设置前景色):

/**
 * 自定义 Span:文本扩大并且设置前景色
 * 实现方案:Android已有文本扩大的 Span,所以只需要扩展文本扩大的 Span (RelativeSizeSpan)即可
 */
class RelativeSizeColorSpan(relativeSize: Float, @ColorInt val color: Int): RelativeSizeSpan(relativeSize) {
    override fun updateDrawState(ds: TextPaint) {
        super.updateDrawState(ds)

        ds?.color = color
    }
}

// 使用自定义 Span
SpannableStringBuilder(nums).also {
    // 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符
    it.setSpan(RelativeSizeColorSpan(1.5f, Color.RED), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)

    tvContent.setText(it)
}

说明:其实,大家注意到上面的例子发现,这个自定义的 Span 是将基于文本字体放大和前景色(字体颜色)两个基本 Span的组合,因为 Span 是可以组合使用的,所以可以直接使用两个 Span 组合使用来替代自定义 Span 。另外,上面的例子也可以改为继承自ForegroundColorSpan,在自定义代码中改变字体大小也可以实现一样的效果。

五、使用 Span 的最佳做法

在 TextView 中设置文本,有多种节省内存的方式,选择哪种方式取决于您的需求。

5.1 在TextView多次附加或分离 Span,而不更改底层文本

    TextView.setText() 包含多种能够以不同方式处理 Span 的重载。例如,您可以使用以下代码设置 Spannable 文本对象:

textView.setText(spannableObject) // textView.text = spannableObject

当调用 setText() 的此重载方法时,TextView 会创建 Spannable 的副本作为 SpannedString,并将其作为 CharSequence 保留在内存中。这意味着您的文本和 Span 不可变,因此当您需要更新文本或 Span 时,您需要创建一个新的 Spannable 对象并再次调用 setText(),而这也会触发TextView重新测量和重新绘制布局。

如果要表明这些 Span 是可变的,您可以改为使用 setText(CharSequence text, TextView.BufferType type)这个重载方法,如下例所示:

tvContent.setText(SpannableStringBuilder(nums), TextView.BufferType.SPANNABLE)

// 通过在setText()时设置的缓存类型,可以在改变了TextView内部的Span之后,不用再次调用setText()就可以更新Span显示
val s = tvContent.text as Spannable
s.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(StyleSpan(Typeface.BOLD), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
// tvContent.setText(s) 这里无需再调用setText()

    在上面示例中,由于 BufferType.SPANNABLE 参数,TextView 创建了 SpannableString,而由 TextView 保留的 CharSequence 对象现在具有了可变标记和不可变文本。要更新 Span,我们可以将该文本作为 Spannable 进行检索,然后根据需要更新这些 Span。

    当您附加、分离或重新定位 Span 时,TextView 会自动更新以反映对文本的更改。不过请注意,如果您更改现有 Span 的内部属性,您还需要调用 invalidate()(如果进行与外观相关的更改)或 requestLayout()(如果进行与指标相关的更改)。

5.2 在TextView中多次设置文本

    在某些情况下(例如使用 RecyclerView.ViewHolder)时,您可能想要重复使用 TextView 并多次设置文本。默认情况下,无论您是否设置 BufferTypeTextView 都会创建 CharSequence 对象的副本并将其保留在内存中。这样可确保所有 TextView 更新均已经过深思熟虑 - 您无法通过简单地更新原始 CharSequence 对象来更新文本。这意味着每次设置新的文本时,TextView 都会创建一个新对象。

    如果您希望更好地控制此过程并避免创建额外的对象,您可以实现自己的 Spannable.Factory 并重写 newSpannable() 方法。您可以简单地对现有的 CharSequence 进行类型转换,并将其作为 Spannable 返回,而不是创建新的文本对象,如下所示:

val spannableFactory = object : Spannable.Factory() {
    override fun newSpannable(source: CharSequence?): Spannable {
        return source as Spannable
    }
}

注意:TextView在设置文本时,必须使用 TextView.setText(spannableObject, BufferType.SPANNABLE)。否则,源 CharSequence 将作为 Spanned 实例进行创建,并且无法转换为 Spannable,从而导致 newSpannable() 抛出 ClassCastException

    在自定义Spannable.Factory并重写 newSpannable() 之后,您需要调用 TextView.setSpannableFactory() 告知 TextView 使用新的 Factory

textView.setSpannableFactory(spannableFactory)

注意:如果需要自定义TextViewSpannable.Factory,请务必在获得 TextView 的引用后立即设置 Spannable.Factory 对象。如果您使用的是 RecyclerView,请在首次扩充视图时设置 Spannable.Factory 对象。这可避免 RecyclerView 在将新的项绑定到 ViewHolder 时创建额外的对象。

5.3 更改内部 Span 属性

    如果您只需更改可变 Span 的内部属性(例如改变某个 Span 的颜色),则可以通过在创建 Span 时保持对该 Span 的引用来避免多次调用 setText() 所产生的开销。当您需要修改 Span 时,您可以直接对引用进行修改,然后根据您更改的属性类型,在 TextView 上调用 invalidate()(改变外观) 或 requestLayout()(改变指标)。

val relativeSizeColorSpan = RelativeSizeColorSpan(1.5f, Color.RED)

// ....

override fun onClick(v: View?) {
    when(v?.id) {
        R.id.btnChangeSpanProperties1 -> {
            relativeSizeColorSpan.color = Color.RED
            textView?.setText(SpannableStringBuilder(nums).also {
                // 设置 Span
                it.setSpan(relativeSizeColorSpan, 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
            }, TextView.BufferType.SPANNABLE)
            textView?.invalidate()
        }
        R.id.btnChangeSpanProperties2 -> {
            // 改变 Span的属性
            relativeSizeColorSpan.color = Color.GREEN
            textView?.invalidate()
        }
        R.id.btnChangeSpanProperties3 -> {
            // 改变 Span的属性
            relativeSizeColorSpan.color = Color.YELLOW
            textView?.invalidate()
        }
    }
}

注意:内置的 Span 属性都不支持在创建之后进行更改,如果要使用这种方法,只能重写对应的 Span 类,并重写属性值覆盖。

六、使用 Android KTX 扩展功能

    Android KTX 还包含扩展功能,该功能可确保能够更加轻松地与 Span 结合使用。要了解详情,请参阅有关 androidx.core.text 软件包的文档。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Element-UI的badge标记组件是用于在某个元素上显示一个小红点或者数字标记的组件。通过对该组件进行特定的样式控制,可以实现闪烁的效果。 具体实现闪烁的方法是通过使用CSS的opacity属性和@keyframes动画属性来控制元素的透明度。通过规定不同时间点的透明度值,可以使元素在一定时间内循环变化透明度,从而实现闪烁的效果。同时,使用animation属性可以控制动画的播放方式,使其循环播放。 因此,可以通过对Element-UI的badge标记组件应用适当的样式和动画属性,来实现badge标记的闪烁效果。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [css实现图标闪烁(Element-UI el-badge标记组件为例)](https://blog.csdn.net/weixin_44220970/article/details/129232441)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Element-UI 使用手册文档 V2.4.6 (Vue版本).pdf](https://download.csdn.net/download/jiezishu1005/11982923)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值