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

ForegroundColorSpan
. Поскольку диапазон задается с помощью Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, он расширяется, включая вставленный текст на границах диапазона, как показано в следующем примере:
Котлин
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Ява
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
.К одному тексту можно прикрепить несколько диапазонов. В следующем примере показано, как создать жирный текст красного цвета:
Котлин
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 )
Ява
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 span
В Android представлено более 20 типов элементов span в пакете android.text.style . Android классифицирует элементы span двумя основными способами:
- Как диапазон влияет на текст: диапазон может влиять на внешний вид текста или его метрики.
- Область охвата: некоторые охваты можно применять к отдельным символам, в то время как другие должны применяться ко всему абзацу.

В следующих разделах эти категории описаны более подробно.
Пробелы, влияющие на внешний вид текста
Некоторые диапазоны, применяемые на уровне символов, влияют на внешний вид текста, например, меняют цвет текста или фона, а также добавляют подчеркивания или зачёркивания. Эти диапазоны расширяют класс CharacterStyle
.
В следующем примере кода показано, как применить UnderlineSpan
для подчеркивания текста:
Котлин
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Ява
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

UnderlineSpan
. Классы 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)
Ява
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

RelativeSizeSpan
.Применение диапазона, влияющего на метрики текста, заставляет наблюдающий объект повторно измерять текст для корректной компоновки и отображения — например, изменение размера текста может привести к тому, что слова будут отображаться на разных строках. Применение предыдущего диапазона запускает повторное измерение, перерасчёт компоновки текста и перерисовку текста.
Диапазоны, влияющие на метрики текста, расширяют класс MetricAffectingSpan
— абстрактный класс, который позволяет подклассам определять, как диапазон влияет на измерение текста, предоставляя доступ к TextPaint
. Поскольку MetricAffectingSpan
расширяет CharacterStyle
, подклассы влияют на внешний вид текста на уровне символов.
Промежутки, которые влияют на абзацы
Элемент span также может влиять на текст на уровне абзаца, например, изменяя выравнивание или поля блока текста. Элементы span, влияющие на целые абзацы, реализуют ParagraphStyle
. Чтобы использовать эти элементы span, их необходимо прикрепить ко всему абзацу, за исключением завершающего символа новой строки. Если вы попытаетесь применить элемент span к чему-либо, кроме целого абзаца, Android не применит его вообще.
На рисунке 8 показано, как Android разделяет абзацы в тексте.

\n
). В следующем примере кода стиль QuoteSpan
применяется к абзацу. Обратите внимание: если вы прикрепите этот стиль к любой позиции, кроме начала или конца абзаца, Android вообще не применит этот стиль.
Котлин
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Ява
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

QuoteSpan
, примененный к абзацу.Создание пользовательских диапазонов
Если вам требуется больше функциональности, чем предусмотрено в существующих элементах span в Android, вы можете реализовать собственный элемент span. При реализации собственного элемента span определите, влияет ли он на текст на уровне символов или на уровне абзацев, а также на расположение или внешний вид текста. Это поможет вам определить, какие базовые классы вы можете расширить и какие интерфейсы, возможно, потребуется реализовать. Используйте следующую таблицу для справки:
Сценарий | Класс или интерфейс |
---|---|
Ваш диапазон влияет на текст на уровне символов. | CharacterStyle |
Ваш диапазон влияет на внешний вид текста. | UpdateAppearance |
Ваш диапазон влияет на метрики текста. | UpdateLayout |
Ваш диапазон влияет на текст на уровне абзаца. | ParagraphStyle |
Например, если вам нужно реализовать пользовательский диапазон, изменяющий размер и цвет текста, расширьте RelativeSizeSpan
. Благодаря наследованию RelativeSizeSpan
расширяет CharacterStyle
и реализует два интерфейса Update
. Поскольку этот класс уже предоставляет обратные вызовы для updateDrawState
и updateMeasureState
, вы можете переопределить эти обратные вызовы для реализации своего собственного поведения. Следующий код создаёт пользовательский диапазон, расширяющий 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 } }
Ява
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); } }
В этом примере показано, как создать произвольный диапазон. Того же эффекта можно добиться, применив к тексту свойства RelativeSizeSpan
и ForegroundColorSpan
.
Использование тестового диапазона
Интерфейс Spanned
позволяет как задавать, так и извлекать интервалы из текста. При тестировании реализуйте тест Android JUnit, чтобы убедиться, что правильные интервалы добавляются в правильные места. Пример приложения Text Styling содержит интервал, который применяет разметку к маркерам, прикрепляя 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()) }
Ява
@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, проверив следующее:
- Если вы правильно применили диапазон, на холсте появится маркер указанного размера и цвета, а между левым полем и маркером будет необходимое пространство.
- Если не применять диапазон, никакое пользовательское поведение не отобразится.
Реализацию этих тестов можно увидеть в примере TextStyling на GitHub.
Вы можете протестировать взаимодействие с холстом, имитируя холст, передавая имитированный объект методу drawLeadingMargin()
и проверяя, вызываются ли правильные методы с правильными параметрами.
Дополнительные примеры тестов диапазона можно найти в BulletPointSpanTest .
Лучшие практики использования интервалов
Существует несколько эффективных с точки зрения памяти способов установки текста в TextView
, в зависимости от ваших потребностей.
Присоединить или отсоединить промежуток без изменения основного текста
TextView.setText()
содержит несколько перегрузок, которые по-разному обрабатывают элементы span. Например, вы можете установить текстовый объект Spannable
с помощью следующего кода:
Котлин
textView.setText(spannableObject)
Ява
textView.setText(spannableObject);
При вызове этой перегрузки setText()
TextView
создаёт копию Spannable
как SpannedString
и сохраняет её в памяти как CharSequence
. Это означает, что текст и интервалы неизменяемы, поэтому, когда вам нужно обновить текст или интервалы, создайте новый объект Spannable
и снова вызовите setText()
, что также запустит повторное измерение и перерисовку макета.
Чтобы указать, что интервалы должны быть изменяемыми, можно вместо этого использовать 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 )
Ява
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
, теперь имеет изменяемую разметку и неизменяемый текст. Чтобы обновить диапазон, извлеките текст как Spannable
, а затем обновите диапазоны по мере необходимости.
При присоединении, отсоединении или изменении положения элементов 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
, что приведет к тому, что newSpannable()
выдаст исключение ClassCastException
.
После переопределения newSpannable()
сообщите TextView
о необходимости использовать новую Factory
:
Котлин
textView.setSpannableFactory(spannableFactory)
Ява
textView.setSpannableFactory(spannableFactory);
Установите объект Spannable.Factory
один раз ViewHolder
сразу после получения ссылки на TextView
. Если вы используете RecyclerView
, установите объект Factory
при первом добавлении представлений. Это позволит избежать создания дополнительных объектов при привязке нового элемента к RecyclerView
.
Изменить внутренние атрибуты диапазона
Если вам нужно изменить только внутренний атрибут изменяемого диапазона, например, цвет маркера в пользовательском диапазоне, вы можете избежать накладных расходов, связанных с многократным вызовом setText()
, сохранив ссылку на диапазон в том виде, в котором он был создан. Когда вам нужно изменить диапазон, вы можете изменить ссылку, а затем вызвать метод 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() } } }
Ява
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 также содержит функции расширения, упрощающие работу с диапазонами. Подробнее см. в документации к пакету androidx.core.text .