Элементы `<span>` — это мощные объекты разметки, которые можно использовать для оформления текста на уровне символов или абзацев. Прикрепляя элементы `<span>` к текстовым объектам, вы можете изменять текст различными способами, включая добавление цвета, создание кликабельного текста, масштабирование размера текста и рисование текста в соответствии с вашими предпочтениями. Элементы `<span>` также могут изменять свойства TextPaint , рисовать на Canvas ) и изменять расположение текста.
В Android доступно несколько типов элементов <span>, охватывающих множество распространенных шаблонов оформления текста. Вы также можете создавать собственные элементы <span> для применения пользовательских стилей.
Создайте и примените тег <span>
Для создания элемента <span> можно использовать один из классов, перечисленных в следующей таблице. Классы различаются в зависимости от того, является ли сам текст изменяемым, является ли изменяемой разметка текста и какая базовая структура данных содержит данные элемента <span>.
| Сорт | Изменяемый текст | Изменяемая разметка | Структура данных |
|---|---|---|---|
SpannedString | Нет | Нет | Линейный массив |
SpannableString | Нет | Да | Линейный массив |
SpannableStringBuilder | Да | Да | Интервальное дерево |
Все три класса наследуют интерфейс Spanned . SpannableString и SpannableStringBuilder также наследуют интерфейс Spannable .
Вот как выбрать, какой из них использовать:
- Если вы не собираетесь изменять текст или разметку после создания, используйте
SpannedString. - Если вам нужно прикрепить небольшое количество тегов <span> к одному текстовому объекту, а сам текст доступен только для чтения, используйте
SpannableString. - Если вам нужно изменить текст после его создания и добавить к нему теги <span>, используйте
SpannableStringBuilder. - Если вам нужно добавить большое количество тегов `<span>` к текстовому объекту, независимо от того, является ли сам текст доступным только для чтения, используйте
SpannableStringBuilder.
Чтобы применить тег <span>, вызовите setSpan(Object _what_, int _start_, int _end_, int _flags_) для объекта Spannable . Параметр what указывает, к какому тегу <span> вы применяете тег <span>, а параметры start и end указывают, к какой части текста применяется тег <span>.
Если вы вставляете текст внутри границ элемента `<span>`, элемент автоматически расширяется, чтобы включить вставленный текст. При вставке текста на границах элемента `<span>` — то есть, в начальной или конечной точке — параметр `flags` определяет, будет ли элемент `<span>` расширяться, чтобы включить вставленный текст. Используйте флаг Spannable.SPAN_EXCLUSIVE_INCLUSIVE , чтобы включить вставленный текст, и флаг Spannable.SPAN_EXCLUSIVE_EXCLUSIVE , чтобы исключить вставленный текст.
В следующем примере показано, как прикрепить ForegroundColorSpan к строке:
Котлин
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 );

ForegroundColorSpan . Поскольку параметр span задается с помощью Spannable.SPAN_EXCLUSIVE_INCLUSIVE , span расширяется, включая вставленный текст по границам span, как показано в следующем примере:
Котлин
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)");

Spannable.SPAN_EXCLUSIVE_INCLUSIVE область расширения включает дополнительный текст.К одному и тому же тексту можно прикрепить несколько элементов <span>. В следующем примере показано, как создать текст, выделенный жирным шрифтом и красным цветом:
Котлин
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 );

ForegroundColorSpan(Color.RED) и StyleSpan(BOLD) .типы сегментов Android
В Android доступно более 20 типов элементов <span> в пакете android.text.style . Android классифицирует элементы <span> двумя основными способами:
- Как элемент `<span>` влияет на текст: элемент `<span>` может влиять на внешний вид текста или на его метрики.
- Область действия тега `<span>`: некоторые теги `<span>` могут применяться к отдельным символам, в то время как другие должны применяться ко всему абзацу.

В следующих разделах эти категории описаны более подробно.
Элементы, влияющие на внешний вид текста.
Некоторые элементы <span>, применяемые на уровне символов, влияют на внешний вид текста, например, изменяют цвет текста или фона, добавляют подчеркивание или зачеркивание. Эти элементы <span> наследуют класс CharacterStyle .
В следующем примере кода показано, как использовать UnderlineSpan для подчеркивания текста:
Котлин
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 . Элементы <span>, влияющие только на внешний вид текста, вызывают перерисовку текста без пересчета макета. Эти элементы <span> реализуют UpdateAppearance и наследуют CharacterStyle . Подклассы CharacterStyle определяют способ отрисовки текста, предоставляя доступ к обновлению TextPaint .
Участки текста, влияющие на метрики текста.
Другие диапазоны, применяемые на уровне символов, влияют на метрики текста, такие как высота строки и размер текста. Эти диапазоны расширяют класс MetricAffectingSpan .
В следующем примере кода создается элемент RelativeSizeSpan , который увеличивает размер текста на 50%:
Котлин
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 .Применение элемента span, влияющего на метрики текста, приводит к тому, что наблюдающий объект перемеряет текст для корректной компоновки и отображения — например, изменение размера текста может привести к тому, что слова будут отображаться на разных строках. Применение предыдущего элемента span запускает перемер, перерасчет компоновки текста и перерисовку текста.
Элементы, влияющие на метрики текста, наследуют класс MetricAffectingSpan — абстрактный класс, позволяющий подклассам определять, как элемент влияет на измерение текста, предоставляя доступ к TextPaint . Поскольку MetricAffectingSpan наследует CharacterStyle , подклассы влияют на внешний вид текста на уровне символов.
Промежутки, влияющие на абзацы
Элемент `<span>` также может влиять на текст на уровне абзаца, например, изменяя выравнивание или поля блока текста. Элементы `<span>`, влияющие на целые абзацы, реализуют ParagraphStyle . Чтобы использовать такие элементы, их нужно прикрепить ко всему абзацу, за исключением символа новой строки в конце. Если вы попытаетесь применить элемент `<span>` к чему-либо, кроме всего абзаца, Android вообще не применит его.
На рисунке 8 показано, как Android разделяет абзацы в тексте.

\n ). В следующем примере кода применяется элемент QuoteSpan к абзацу. Обратите внимание, что если вы добавите элемент `span` в любое другое место, кроме начала или конца абзаца, Android вообще не применит стиль.
Котлин
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 , примененный к абзацу.Создание пользовательских диапазонов
Если вам требуется больше функциональности, чем предоставляют существующие элементы <span> в Android, вы можете реализовать собственный элемент <span>. При создании собственного элемента <span> определите, влияет ли он на текст на уровне символов или на уровне абзаца, а также влияет ли он на расположение или внешний вид текста. Это поможет вам определить, какие базовые классы вы можете расширить и какие интерфейсы вам, возможно, потребуется реализовать. Используйте следующую таблицу для справки:
| Сценарий | Класс или интерфейс |
|---|---|
| Использование тега `<span>` влияет на текст на уровне символов. | CharacterStyle |
| Элемент `<span>` влияет на внешний вид текста. | UpdateAppearance |
| Размер вашего фрагмента текста влияет на метрики текста. | UpdateLayout |
| Использование оператора span влияет на текст на уровне абзаца. | ParagraphStyle |
Например, если вам нужно реализовать пользовательский элемент span, изменяющий размер и цвет текста, расширьте RelativeSizeSpan . Благодаря наследованию, RelativeSizeSpan расширяет CharacterStyle и реализует два интерфейса Update . Поскольку этот класс уже предоставляет коллбэки для updateDrawState и updateMeasureState , вы можете переопределить эти коллбэки для реализации собственного поведения. Следующий код создает пользовательский элемент span, который расширяет RelativeSizeSpan и переопределяет коллбэк updateDrawState для установки цвета TextPaint :
Котлин
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>`. Того же эффекта можно добиться, применив к тексту ` RelativeSizeSpan и ForegroundColorSpan .
использование тестового диапазона
Интерфейс Spanned позволяет как устанавливать элементы Span, так и извлекать их из текста. При тестировании реализуйте тест Android JUnit , чтобы убедиться, что правильные элементы Span добавляются в правильные места. В примере приложения «Стилизация текста» содержится элемент Span, который применяет разметку к маркированным спискам, прикрепляя к тексту объект BulletPointSpan . Следующий пример кода показывает, как проверить, отображаются ли маркированные списки должным образом:
Котлин
@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)); }
Дополнительные примеры тестов см. в репозитории MarkdownBuilderTest на GitHub.
Тестирование пользовательских диапазонов
При тестировании элементов <span> убедитесь, что TextPaint содержит ожидаемые изменения и что на вашем Canvas отображаются правильные элементы. Например, рассмотрим пользовательскую реализацию элемента <span>, которая добавляет маркер списка к некоторому тексту. Маркер списка имеет заданный размер и цвет, а между левым полем области рисования и маркером списка имеется зазор.
Вы можете проверить поведение этого класса, реализовав тест AndroidJUnit, который проверяет следующее:
- Если правильно применить тег <span>, на холсте появится маркер указанного размера и цвета, а между левым полем и маркером будет правильное расстояние.
- Если не использовать тег <span>, никакое из пользовательских действий не отобразится.
Реализацию этих тестов можно увидеть в примере TextStyling на GitHub.
Вы можете протестировать взаимодействие с Canvas, создав фиктивный объект на холсте, передав этот фиктивный объект в метод drawLeadingMargin() и убедившись, что вызываются правильные методы с правильными параметрами.
Больше примеров тестов на определение длины шага вы найдете в BulletPointSpanTest .
Рекомендации по использованию пролетов
Существует несколько способов эффективного использования памяти для установки текста в TextView , в зависимости от ваших потребностей.
Прикрепляйте или отсоединяйте элемент `<span>` без изменения исходного текста.
TextView.setText() содержит несколько перегрузок, которые по-разному обрабатывают элементы типа Span. Например, вы можете установить объект Spannable с помощью следующего кода:
Котлин
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
При вызове этой перегрузки метода setText() , TextView создает копию вашего Spannable в виде объекта SpannedString и хранит ее в памяти как CharSequence . Это означает, что ваш текст и элементы span являются неизменяемыми, поэтому, когда вам нужно обновить текст или элементы span, создайте новый объект Spannable и снова вызовите setText() , что также запускает переизмерение и перерисовку макета.
Чтобы указать, что элементы <span> должны быть изменяемыми, можно использовать setText(CharSequence text, TextView.BufferType type) , как показано в следующем примере:
Котлин
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 , и объект CharSequence хранящийся в TextView теперь имеет изменяемую разметку и неизменяемый текст. Чтобы обновить span, получите текст в виде объекта Spannable , а затем обновите span по мере необходимости.
При добавлении, удалении или перемещении элементов <span>, TextView автоматически обновляется, отражая изменение текста. Если вы изменяете внутренний атрибут существующего элемента <span>, вызовите invalidate() для внесения изменений, связанных с внешним видом, или requestLayout() для внесения изменений, связанных с метриками.
Установить текст в TextView несколько раз
В некоторых случаях, например, при использовании RecyclerView.ViewHolder , может потребоваться повторное использование TextView и установка текста несколько раз. По умолчанию, независимо от того, установлен ли BufferType , TextView создает копию объекта CharSequence и хранит ее в памяти. Это делает все обновления TextView преднамеренными — вы не можете обновить исходный объект CharSequence , чтобы обновить текст. Это означает, что каждый раз, когда вы устанавливаете новый текст, TextView создает новый объект.
Если вы хотите получить больший контроль над этим процессом и избежать создания дополнительных объектов, вы можете реализовать собственную Spannable.Factory и переопределить метод newSpannable() . Вместо создания нового текстового объекта вы можете привести существующий CharSequence к типу Spannable и вернуть его, как показано в следующем примере:
Котлин
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 , что приведет к генерации исключения ClassCastException при вызове newSpannable() .
После переопределения метода newSpannable() укажите TextView использовать новую Factory :
Котлин
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Установите объект Spannable.Factory один раз, сразу после получения ссылки на ваш TextView . Если вы используете RecyclerView , установите объект Factory при первом создании представлений. Это позволит избежать создания дополнительных объектов, когда ваш RecyclerView привязывает новый элемент к вашему ViewHolder .
Изменение внутренних атрибутов диапазона
Если вам нужно изменить только внутренний атрибут изменяемого элемента span, например, цвет маркера в пользовательском элементе span, вы можете избежать накладных расходов, связанных с многократным вызовом setText() , сохраняя ссылку на элемент span при его создании. Когда вам нужно изменить элемент span, вы можете изменить ссылку, а затем вызвать invalidate() или requestLayout() для TextView , в зависимости от типа измененного атрибута.
В приведенном ниже примере кода реализован пользовательский маркер списка, цвет которого по умолчанию — красный, а при нажатии кнопки он меняется на серый:
Котлин
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 .
