Span

试用 Compose 方式
Jetpack Compose 是推荐在 Android 设备上使用的界面工具包。了解如何在 Compose 中使用文本。
<ph type="x-smartling-placeholder"></ph> 文本中有多种样式 →

Span 是强大的标记对象,可用于在 字符级别或段落级别。通过将 span 附加到文本对象,您可以更改 为文本添加颜色、使文本可点击 缩放文字大小,以及以自定义方式绘制文字。Span 还可以 更改 TextPaint 属性,在 Canvas,然后更改文本布局。

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

创建并应用 Span

如需创建 span,您可以使用下表中列出的某个类。 这些类因文本本身是否可变、文本 标记是可变的,并且哪个底层数据结构包含 span 数据。

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

这三个类都扩展了 Spanned 界面。SpannableStringSpannableStringBuilder 还会扩展 Spannable 接口。

下面介绍了如何决定使用哪一种:

  • 如果您不准备在创建后修改文本或标记,请使用 SpannedString
  • 如果您需要将少量 Span 附加到单个文本对象,并且 文本本身是只读的,请使用 SpannableString
  • 如果您需要在创建后修改文本,并且需要将 span 附加到 文本,使用 SpannableStringBuilder
  • 如果您需要将大量 span 附加到文本对象,无论其 请使用 SpannableStringBuilder

如需应用 Span,请对 Spannable 对象调用 setSpan(Object _what_, int _start_, int _end_, int _flags_)what 参数是指 startend 参数用于指示 文本。

如果您在 Span 的边界内插入文本,该 Span 会自动扩展为 包含插入的文字在 span 处插入文本时 边界(即 startend 索引处)flags 参数确定 span 是否展开即可包含插入的文本。使用 该 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 标志以包含插入的文字,并使用 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 以排除插入的文字。

以下示例展示了如何将 ForegroundColorSpan更改为 字符串:

Kotlin

val spannable = SpannableStringBuilder("Text is spantastic!")
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)

Java

SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
);
显示灰色文本(部分为红色)的图片。
图 1. 文本样式设置为 ForegroundColorSpan

由于此 span 是使用 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 设置的,因此 扩展以包含在跨度边界处插入的文本,如 示例:

Kotlin

val spannable = SpannableStringBuilder("Text is spantastic!")
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
spannable.insert(12, "(& fon)")

Java

SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
);
spannable.insert(12, "(& fon)");
显示使用 SPAN_EXCLUSIVE_INCLUSIVE 时 span 包含更多文本的图片。
图 2. 该 span 将展开,以包含 使用 Spannable.SPAN_EXCLUSIVE_INCLUSIVE

您可以将多个 Span 附加到同一文本。以下示例展示了如何 创建粗体和红色文本:

Kotlin

val spannable = SpannableString("Text is spantastic!")
spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(
    StyleSpan(Typeface.BOLD),
    8,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

Java

SpannableString spannable = new SpannableString("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, 12,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
spannable.setSpan(
    new StyleSpan(Typeface.BOLD),
    8, spannable.length(),
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
显示具有多个 span(“ForegroundColorSpan(Color.RED)”和“StyleSpan(BOLD)”的文本的图片
图 3. 具有多个 span 的文本: ForegroundColorSpan(Color.RED)StyleSpan(BOLD)

Android Span 类型

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

  • Span 如何影响文本:Span 可能会影响文本外观或文本 指标。
  • Span 范围:一些 Span 可应用于单个字符,而其他 Span 必须应用于整个段落。
。 <ph type="x-smartling-placeholder">
</ph> 显示不同 span 类别的图片
图 4.Android span 的类别。

以下部分更详细地介绍了这些类别。

影响文本外观的 Span

一些在字符级别应用的 Span 会影响文本外观,例如 更改文本或背景颜色,以及添加下划线或删除线。这些 span 会扩展 CharacterStyle 类。

以下代码示例展示了如何对下划线应用 UnderlineSpan 文本:

Kotlin

val string = SpannableString("Text with underline span")
string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

SpannableString string = new SpannableString("Text with underline span");
string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
显示如何使用 `UnderlineSpan` 为文本添加下划线的图片
图 5. 使用 UnderlineSpan

仅影响文本外观的 Span 会触发重新绘制文本,而不会触发重新计算布局。这些 span 用于实现 UpdateAppearance 和扩展 CharacterStyleCharacterStyle 子类通过提供对文本的 更新 TextPaint

影响文本指标的 Span

在字符级别应用的其他 span 会影响文本指标,例如 高度和文字大小。这些跨度会扩展 MetricAffectingSpan 类。

以下代码示例创建了一个 RelativeSizeSpan 将文字大小增加 50%:

Kotlin

val string = SpannableString("Text with relative size span")
string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

SpannableString string = new SpannableString("Text with relative size span");
string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
展示 RelativeSizeSpan 用法的图片
图 6. 使用 RelativeSizeSpan

应用影响文本指标的 Span 会导致观察对象 重新测量文本,以实现正确的布局和呈现。例如, 可能会导致字词分行不同。应用上述规则 span 会触发重新测量、重新计算文本布局, 文本。

影响文本指标的 Span 会扩展 MetricAffectingSpan 类, 可让子类定义 span 如何影响文本测量的抽象类 方法是提供对 TextPaint 的访问权限。由于 MetricAffectingSpan 扩展 CharacterSpan,则子类会影响字符处文本的外观 。

影响段落的 Span

Span 还可能影响段落级别的文本,例如更改 对齐方式或文本块的外边距。影响整个段落的 Span 会实现 ParagraphStyle。接收者 使用这些 span,您可以将其附加到整个段落,但不包括结尾 换行符。如果您尝试将段落 span 应用于 整段,Android 根本不会应用 span。

图 8 显示了 Android 如何分隔文本中的段落。

图 7. 在 Android 中,段落结尾处 换行符 (\n)。

以下代码示例将 QuoteSpan 即可应用到段落。请注意, 但如果您将跨度附加到 段落,Android 根本不会应用该样式。

Kotlin

spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
显示 QuoteSpan 示例的图片
图 8. QuoteSpan 应用到段落。

创建自定义 Span

除了现有 Android Span 中提供的功能之外,如果您还需要其他功能,可以实现自定义 Span。在实现自己的 span 时,请确定 您的 span 会影响字符级别还是段落级别的文本, 以及它会影响文本的布局或外观。这有助于您 确定您可以扩展哪些基类以及可能需要哪些接口 实施。请参考下表:

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

例如,如果您需要实现可修改文本大小的自定义 span, color,扩展 RelativeSizeSpan。通过继承,RelativeSizeSpan 扩展 CharacterStyle 并实现两个 Update 接口。由于 类已经为 updateDrawStateupdateMeasureState 提供了回调, 您可以替换这些回调以实现您的自定义行为。通过 以下代码会创建一个自定义 span,以扩展 RelativeSizeSpan 和 替换 updateDrawState 回调以设置 TextPaint 的颜色:

Kotlin

class RelativeSizeColorSpan(
    size: Float,
    @ColorInt private val color: Int
) : RelativeSizeSpan(size) {
    override fun updateDrawState(textPaint: TextPaint) {
        super.updateDrawState(textPaint)
        textPaint.color = color
    }
}

Java

public class RelativeSizeColorSpan extends RelativeSizeSpan {
    private int color;
    public RelativeSizeColorSpan(float spanSize, int spanColor) {
        super(spanSize);
        color = spanColor;
    }
    @Override
    public void updateDrawState(TextPaint textPaint) {
        super.updateDrawState(textPaint);
        textPaint.setColor(color);
    }
}

以下示例说明了如何创建自定义 span。使用同一个 对文本应用 RelativeSizeSpanForegroundColorSpan 来达到同样的效果。

测试 Span 用法

借助 Spanned 接口,您不仅可以设置 span,还可以从 文本。测试时,请实现 Android JUnit 测试,以验证是否添加了正确的 span 正确的位置。文本样式设置示例 应用 包含通过将标记附加到项目符号的 span, BulletPointSpan。以下代码示例展示了如何测试 项目符号是否按预期显示:

Kotlin

@Test fun textWithBulletPoints() {
   val result = builder.markdownToSpans("Points\n* one\n+ two")

   // Check whether the markup tags are removed.
   assertEquals("Points\none\ntwo", result.toString())

   // Get all the spans attached to the SpannedString.
   val spans = result.getSpans<Any>(0, result.length, Any::class.java)

   // Check whether the correct number of spans are created.
   assertEquals(2, spans.size.toLong())

   // Check whether the spans are instances of BulletPointSpan.
   val bulletSpan1 = spans[0] as BulletPointSpan
   val bulletSpan2 = spans[1] as BulletPointSpan

   // Check whether the start and end indices are the expected ones.
   assertEquals(7, result.getSpanStart(bulletSpan1).toLong())
   assertEquals(11, result.getSpanEnd(bulletSpan1).toLong())
   assertEquals(11, result.getSpanStart(bulletSpan2).toLong())
   assertEquals(14, result.getSpanEnd(bulletSpan2).toLong())
}

Java

@Test
public void textWithBulletPoints() {
    SpannedString result = builder.markdownToSpans("Points\n* one\n+ two");

    // Check whether the markup tags are removed.
    assertEquals("Points\none\ntwo", result.toString());

    // Get all the spans attached to the SpannedString.
    Object[] spans = result.getSpans(0, result.length(), Object.class);

    // Check whether the correct number of spans are created.
    assertEquals(2, spans.length);

    // Check whether the spans are instances of BulletPointSpan.
    BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0];
    BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1];

    // Check whether the start and end indices are the expected ones.
    assertEquals(7, result.getSpanStart(bulletSpan1));
    assertEquals(11, result.getSpanEnd(bulletSpan1));
    assertEquals(11, result.getSpanStart(bulletSpan2));
    assertEquals(14, result.getSpanEnd(bulletSpan2));
}

如需查看更多测试示例,请参阅 GitHub 上的 MarkdownBuilderTest

测试自定义 span

测试 span 时,请验证 TextPaint 是否包含预期的 并在 Canvas 上显示正确的元素。假设有一个自定义 Span 实现,它在一些文本前面添加了项目符号,项目符号指定了尺寸和颜色,并且存在间隙 可绘制区域的左边缘与项目符号之间。

您可以通过实现 AndroidJUnit 测试并检查以下内容来测试此类的行为:

  • 如果您正确应用 span,则会使用指定大小的项目符号和 且左边缘之间留有适当的间距 和项目符号
  • 如果您未应用 Span,则不会显示任何自定义行为。

您可以在 TextStyling 中 示例

您可以通过模拟画布并向其传递所模拟的画布来测试画布互动。 对象添加到 drawLeadingMargin() 方法,并验证是否使用正确的方法调用了正确的方法 参数。

您可以在以下位置找到更多 Span 测试示例: BulletPointSpanTest

使用 Span 的最佳做法

您可以通过多种节省内存的方式在 TextView 中设置文本, 。

附加或分离 Span,而不更改底层文本

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

Kotlin

textView.setText(spannableObject)

Java

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)
val spannableText = textView.text as Spannable
spannableText.setSpan(
     ForegroundColorSpan(color),
     8, spannableText.length,
     SPAN_INCLUSIVE_INCLUSIVE
)

Java

textView.setText(spannable, BufferType.SPANNABLE);
Spannable spannableText = (Spannable) textView.getText();
spannableText.setSpan(
     new ForegroundColorSpan(color),
     8, spannableText.getLength(),
     SPAN_INCLUSIVE_INCLUSIVE);

在此示例中, BufferType.SPANNABLE 参数会使 TextView 创建 SpannableString,而 由 TextView 保留的 CharSequence 对象现在具有可变标记,并且 不可更改文本。如需更新 span,请以 Spannable 形式检索文本,然后 根据需要更新 span。

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

在 TextView 中多次设置文本

在某些情况下(例如使用 RecyclerView.ViewHolder 时),您可能想要重复使用 TextView 并多次设置文本。修改者 无论您是否设置 BufferTypeTextView 都会创建 CharSequence 对象的副本并将其保存在内存中。这使得所有 TextView 更新是有意为之 - 您无法更新原始版本 CharSequence 对象来更新文本。也就是说,每次设置新的 文本,TextView 会创建一个新对象。

如果您希望更好地控制此过程,并避免产生额外的对象, 则可以实现自己的 Spannable.Factory 并替换 newSpannable()。 您可以转换并返回现有的文本对象, CharSequence 作为 Spannable,如以下示例所示:

Kotlin

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

Java

Spannable.Factory spannableFactory = new Spannable.Factory(){
    @Override
    public Spannable newSpannable(CharSequence source) {
        return (Spannable) source;
    }
};

在下列情况中,您必须使用 textView.setText(spannableObject, BufferType.SPANNABLE): 设置文本。否则,将创建来源 CharSequence 作为 Spanned 实例且无法转换为 Spannable,导致 newSpannable() 抛出 ClassCastException

替换 newSpannable() 后,告知 TextView 使用新的 Factory

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

设置 Spannable.Factory 对象一次,就在获得对 TextView。如果您使用的是 RecyclerView,请在执行以下操作时设置 Factory 对象: 首先膨胀您的视图这样可以避免在 RecyclerView 会将一个新项绑定到您的 ViewHolder

更改内部 Span 属性

如果您只需要更改可变 span 的内部属性,例如 采用自定义项目符号跨度显示项目符号颜色,可避免因致电而造成开销 setText()。 需要修改 span 时,您可以修改引用,然后调用 针对 TextViewinvalidate()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 the mutable span.
            bulletSpan.color = Color.GRAY
            // Color doesn't change until invalidate is called.
            styledText.invalidate()
        }
    }
}

Java

public class MainActivity extends AppCompatActivity {

    private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        SpannableString spannable = new 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(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // Change the color of the mutable span.
                bulletSpan.setColor(Color.GRAY);
                // Color doesn't change until invalidate is called.
                styledText.invalidate();
            }
        });
    }
}

使用 Android KTX 扩展功能

Android KTX 还包含扩展函数,以便您使用 span 。如需了解详情,请参阅有关 androidx.core.text 软件包的文档。