超能力文本:TextView.setText(CharSequence, BufferType)中的秘密

setText(CharSequence text, BufferType type)

TextView.setText() 有两个重载方法用于设置文本内容:

java 复制代码
//TextView.java
private BufferType mBufferType = BufferType.NORMAL;
//1
public final void setText(CharSequence text) {
   setText(text, mBufferType); //mBufferType默认是BufferType.NORMAL类型
 }
 
//2
public void setText(CharSequence text, BufferType type) {
   .......
}

public enum BufferType {
  NORMAL, SPANNABLE, EDITABLE
}

setText(CharSequence text) 最终也是调用了setText(CharSequence text, BufferType type) 方法将 text 设置为 TextView 的文本内容,并使用指定的 BufferType 类型来处理文本。

根据不同的类型,TextView 会采取不同的方式处理文本内容。两个参数的含义:

  • textCharSequence类型,可以是字符串或其他 CharSequence 的实现类。
  • type :文本的处理类型,类型为 BufferType 枚举,可选值为NORMALSPANNABLEEDITABLE

BufferType 枚举类型包含以下三个常量:

  • NORMAL:表示普通文本类型,不支持任何样式或效果,默认设置。
  • SPANNABLE:表示可使用插入 Span 的文本类型,可以使用 Spannable 或其子类(如 SpannableStringSpannableStringBuilder)来设置文本的样式和效果,例如颜色、字体、点击事件等。
  • EDITABLE:表示可编辑的文本类型,用于 EditTextView 中。

示例:

java 复制代码
TextView textView = findViewById(R.id.textView);

// 1、使用默认的 BufferType.NORMAL 处理文本
textView.setText("Hello, World!");

// 2、使用 BufferType.SPANNABLE 处理文本,支持设置样式和效果
SpannableString spannableString = new SpannableString("Hello, World!");
spannableString.setSpan(new ForegroundColorSpan(Color.RED), 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString, TextView.BufferType.SPANNABLE);

// 如果setText被以BufferType.SPANNABLE方式调用,那么文字可被转为 Spannable:
val spannableText = textView.text as Spannable
// 现在我们可以继续设置或删除 span
spannableText.setSpan(
     ForegroundColorSpan(color), 
     0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE)

// 3、使用 BufferType.EDITABLE 处理文本,支持编辑功能
Editable editable = Editable.Factory.getInstance().newEditable("Editable text");
textView.setText(editable, TextView.BufferType.EDITABLE);

下面主要介绍BufferType.SPANNABLE在设置Span时的一些优化建议。

官方推荐使用Span的最佳做法

已知SpannedString 实现的 Spanned接口,SpannableString、SpannableStringBuilder 实现的 Spannable接口,而他们又都继承了CharSequence接口,如下:

TextView 中设置文本时可以根据不同需求采用多种节省内存的方式。

1、不更改文本,只附加或分离 Span

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

kotlin 复制代码
textView.setText(spannableObject)

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

如需表示这些 Span 可变,您可以改为使用 setText(CharSequence text, TextView.BufferType type),如下例所示:

kotlin 复制代码
textView.setText(spannable, BufferType.SPANNABLE)
//将textView.text重新转变为Spannable
val spannableText = textView.text as Spannable
spannableText.setSpan(
     ForegroundColorSpan(color),
     8, spannableText.length,
     SPAN_INCLUSIVE_INCLUSIVE
)

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

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

2、在TextView 中多次设置文本

在某些情况下(例如使用 RecyclerView.ViewHolder 时),可能想要重复使用 TextView 并多次设置文本。默认情况下,无论是否设置 BufferTypeTextView 都会创建 CharSequence 对象的副本并将其保留在内存中,这意味着每次设置新的文本时,TextView 都会创建一个新对象。

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

csharp 复制代码
//默认的Spannable.Factory
public static class Factory {
        private static Spannable.Factory sInstance = new Spannable.Factory();

        /**
         * Returns the standard Spannable Factory.
         */
        public static Spannable.Factory getInstance() {
            return sInstance;
        }

        /**
         * Returns a new SpannableString from the specified CharSequence.
         * You can override this to provide a different kind of Spannable.
         */
        public Spannable newSpannable(CharSequence source) {
            return new SpannableString(source);
        }
    }
kotlin 复制代码
//自定义Spannable.Factory
val spannableFactory = object : Spannable.Factory() {
    override fun newSpannable(source: CharSequence?): Spannable {
        return source as Spannable
    }
}

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

在替换 newSpannable() 之后,需要告知 TextView 使用新的 Factory

kotlin 复制代码
textView.setSpannableFactory(spannableFactory)

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

3、更改内部 Span 属性

如果只需更改可变 Span 的内部属性(例如,自定义项目符号 Span 中的项目符号颜色),可以通过在创建 Span 时保留对该 Span 的引用来避免多次调用 setText() 所产生的开销。当需要修改 Span 时,可以修改引用,然后根据更改的属性类型,对 TextView 调用 invalidate()requestLayout()

在下面的代码示例中,自定义项目符号实现的默认颜色为红色,在点击按钮时会变为灰色:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    // keeping the span as a field
    val bulletSpan = BulletPointSpan(color = Color.RED)

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val spannable = SpannableString("Text is spantastic")
        // setting the span to the bulletSpan field
        spannable.setSpan(
            bulletSpan,
            0, 4,
            Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
        styledText.setText(spannable)
        button.setOnClickListener {
            // change the color of our mutable span
            bulletSpan.color = Color.GRAY
            // color won't be changed until invalidate is called
            styledText.invalidate()
        }
    }
}

资料

【1】https://developer.android.com/guide/topics/text/spans?hl=zh-cn#best-practices

相关推荐
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王8 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡8 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道8 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库9 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道10 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe10 小时前
Android Hook - 动态加载so库
android
居居飒10 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He13 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗14 小时前
Android笔试面试题AI答之Android基础(1)
android