SpannableString — это класс Android SDK из пакета android.text, который позволяет применять множественные стили к разным участкам одной текстовой строки в TextView. В отличие от HTML-разметки, SpannableString работает на уровне объектов Span, управляя визуальным отображением текста: цветом, размером, начертанием, подчёркиванием и интерактивными элементами. По данным Google Developers, SpannableString используется в системных компонентах Android для форматирования ссылок. Это основной способ стилизации текста без подключения сторонних библиотек.
Главное
SpannableString — это класс Android, реализующий интерфейс Spannable, который хранит текст вместе с набором Span-объектов, управляющих визуальным отображением. В отличие от обычной String, SpannableString позволяет привязать стилевые атрибуты к конкретным диапазонам символов: сделать часть текста красной, увеличить шрифт в заголовке или добавить кликабельную ссылку внутри абзаца.
Класс находится в пакете android.text и доступен с API Level 1. SpannableString неизменяем — после создания его структура фиксируется, и замена текста требует создания нового объекта. Для динамического редактирования используется SpannableStringBuilder, который поддерживает вставку и удаление символов без потери стилей.
CharSequence — это базовый интерфейс для текстовых данных, который реализуют String, StringBuilder и SpannableString. Основное отличие SpannableString от String заключается в поддержке привязки произвольных объектов к подстроке. TextView распознаёт интерфейс Spannable и при отрисовке применяет span-объекты к соответствующим участкам текста. Если передать обычный String в TextView, никакие стили не будут применены.
SpannableString хранит текст в виде массива char[] и отдельный массив span-объектов с метаданными о начальной и конечной позиции. При вызове setSpan(what, start, end, flags) объект what сохраняется в списке вместе с информацией о диапазоне. При отрисовке TextView последовательно применяет все спаны, попадающие в отображаемый диапазон, вызывая методы updateDrawState и updateMeasureState.
CharacterStyle — базовый класс для спанов, влияющих на отдельные символы независимо от их положения в строках. Сюда входят ForegroundColorSpan (цвет текста), RelativeSizeSpan (относительный размер), StyleSpan (жирность и курсив), UnderlineSpan (подчёркивание) и другие. Character-спаны применяются к каждому символу в указанном диапазоне по отдельности.
ParagraphStyle — интерфейс для спанов, влияющих на целые абзацы. Наиболее известный представитель — AlignmentSpan, который выравнивает весь абзац по левому краю, центру или правому краю. Paragraph-спаны должны охватывать весь абзац целиком, иначе Android игнорирует их применение. Это ограничение связано с тем, что выравнивание или отступ имеют смысл только для целого блока текста.
Span flags — это четыре константы, определяющие поведение спана при вставке или удалении текста на границах его диапазона. SPAN_EXCLUSIVE_EXCLUSIVE оставляет спад активным только внутри исходных границ, SPAN_INCLUSIVE_INCLUSIVE расширяет его при добавлении текста к границам. SPAN_EXCLUSIVE_INCLUSIVE и SPAN_INCLUSIVE_EXCLUSIVE дают смешанное поведение для начала и конца диапазона.
| Флаг | Вставка слева | Вставка справа |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | не включает | не включает |
| SPAN_INCLUSIVE_INCLUSIVE | включает | включает |
| SPAN_EXCLUSIVE_INCLUSIVE | не включает | включает |
| SPAN_INCLUSIVE_EXCLUSIVE | включает | не включает |
Правильный выбор флага критичен для Editable текста в EditText, где пользователь может вставлять и удалять символы. Для read-only TextView обычно используют SPAN_EXCLUSIVE_EXCLUSIVE — стиль применяется только к исходному диапазону и не расширяется при программных изменениях.
Android SDK предоставляет более 25 встроенных классов спанов, покрывающих большинство задач стилизации текста. Каждый класс реализует интерфейс CharacterStyle или ParagraphStyle и принимает параметры через конструктор. Все классы находятся в пакете android.text.style и доступны без подключения дополнительных зависимостей.
ForegroundColorSpan задаёт цвет текста для указанного диапазона, принимая цвет в формате int. BackgroundColorSpan окрашивает фон под текстом, что полезно для подсветки поисковых запросов. AbsoluteSizeSpan задаёт точный размер шрифта в пикселях, RelativeSizeSpan — множитель относительно базового размера текста в TextView.
StyleSpan принимает константы Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC или BOLD_ITALIC и меняет начертание символов. UnderlineSpan добавляет подчёркивание, StrikethroughSpan — зачёркивание. SuperscriptSpan и SubscriptSpan создают надстрочный и подстрочный индексы. TypefaceSpan позволяет задать кастомный шрифт через объект Typeface для диапазона текста.
ClickableSpan — абстрактный класс для создания кликабельных участков текста. При нажатии вызывается метод onClick(). Чтобы клики работали, TextView должен иметь setMovementMethod(LinkMovementMethod.getInstance()). URLSpan — подкласс ClickableSpan для гиперссылок с автоматическим открытием браузера. ClickableSpan часто комбинируют с ForegroundColorSpan, чтобы визуально выделить ссылку синим цветом.
val spannable = SpannableString("Open developer.android.com")
spannable.setSpan(
URLSpan("https://developer.android.com"),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(Color.BLUE),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
Без LinkMovementMethod клики по URLSpan не будут обработаны. MovementMethod отвечает за перехват сенсорных событий и поиск ClickableSpan в позиции касания. Цветной спад делает ссылку видимой для пользователя.
Работа с SpannableString начинается с создания экземпляра из текстовой строки и последовательного применения спанов через метод setSpan(). Метод принимает четыре параметра: объект спана, начальную позицию, конечную позицию и флаги. После установки всех спанов объект передаётся в TextView через setText().
Создадим строку, где первое слово будет красного цвета и увеличенного размера. Для этого используется ForegroundColorSpan для цвета и RelativeSizeSpan для масштаба. Оба спана применяются к одному диапазону независимо друг от друга — порядок вызова setSpan не имеет значения.
val text = "Header: remaining text"
val spannable = SpannableString(text)
val colorSpan = ForegroundColorSpan(Color.RED)
val sizeSpan = RelativeSizeSpan(1.5f)
spannable.setSpan(colorSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(sizeSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable
Символы с позиции 0 по 9 получат оба стиля одновременно. TextView автоматически применит все спаны при отрисовке — никаких дополнительных вызовов не требуется. RelativeSizeSpan с множителем 1.5f увеличит размер шрифта на 50% относительно базового.
Html.fromHtml() создаёт Spanned объект из HTML-строки, но набор поддерживаемых тегов ограничен. SpannableString даёт полный контроль над каждым атрибутом без ограничений HTML. Если требуется преобразовать HTML в спаны с последующим добавлением кастомных стилей, можно использовать Html.fromHtml() как основу, а затем дополнить спанами через setSpan().
val htmlText = Html.fromHtml(
"<b>Important:</b> check the data",
Html.FROM_HTML_MODE_LEGACY
)
val spannable = SpannableString(htmlText)
spannable.setSpan(
ForegroundColorSpan(Color.RED),
0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
Результат — текст "Важно:" будет жирным (из HTML) и красным (из спана). Такой подход удобен при работе с серверным контентом, где часть форматирования задаётся HTML, а часть добавляется на стороне клиента программно.
SpannableString позволяет накладывать неограниченное количество спанов на один и тот же или перекрывающийся диапазон. Комбинирование спанов — ключевое преимущество перед HTML-разметкой, где вложенные теги могут конфликтовать. Спаны независимы и применяются последовательно при отрисовке.
Например, можно сделать участок текста одновременно жирным, красным и кликабельным. Для этого создаются три спана — StyleSpan, ForegroundColorSpan и ClickableSpan — и каждый применяется к одному диапазону. Порядок применения не влияет на результат, так как каждый спад отвечает за свою характеристику текста.
Если спаны разных типов перекрываются частично, каждый работает независимо в своих границах. ForegroundColorSpan на диапазоне 0–10 и StyleSpan(BOLD) на диапазоне 5–15 дадут жирный красный текст на участке 5–10 и только жирный на 10–15. Никаких конфликтов не возникает, так как каждый спад изменяет свой атрибут при отрисовке.
Метод getSpans(int start, int end, Class type) возвращает массив спанов, попадающих в указанный диапазон. Это полезно для проверки, какие стили уже применены, или для удаления конкретных спанов. С nextSpanTransition() можно итерироваться по границам изменения спанов — это основа работы кастомных TextView, которым нужно знать, где меняется стиль.
SpannableStringBuilder — это класс для пошагового построения стилизованного текста с возможностью вставки, замены и удаления фрагментов. В отличие от SpannableString, который создаётся из готовой строки и неизменяем, Builder позволяет добавлять части текста последовательно и на лету присваивать им стили. Это идеальный выбор для составных сообщений: логов, чатов, новостных заголовков с динамическими метками.
Builder реализует интерфейсы Spannable и Editable, что делает его совместимым с EditText. Пользователь может редактировать текст, а стили сохраняются и корректно смещаются при вставке новых символов. SpannableString, будучи неизменяемым, не подходит для редактируемых полей.
Метод append() возвращает сам builder, что позволяет выстраивать цепочку вызовов. После добавления текста применяются спаны через setSpan(). Позиции указываются относительно текущей длины builder. Insert() и replace() также доступны для более точного контроля над содержимым.
val builder = SpannableStringBuilder()
.append("New ")
.append("comment")
val blue = ForegroundColorSpan(Color.BLUE)
val gray = ForegroundColorSpan(Color.GRAY)
builder.setSpan(blue, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(gray, 6, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = builder
Текст "Новый " окрашен синим, а "комментарий" — серым. При вставке дополнительных символов между ними спад не затронет новый текст благодаря флагам EXCLUSIVE. Builder автоматически корректирует внутренние индексы при модификации.
Использование большого количества спанов влияет на производительность отрисовки TextView. Каждый спад вызывает метод updateDrawState() или updateMeasureState() при каждой перерисовке. Рекомендуется ограничивать количество спанов на один TextView до 50–100 для комфортной работы на устройствах среднего сегмента. Спаны, изменяющие размер текста (RelativeSizeSpan, AbsoluteSizeSpan), требуют перерасчёта layout при каждом изменении, что значительно дороже, чем спаны только для цвета или подчёркивания.
TextAppearanceSpan позволяет применить целый набор стилей из xml-ресурса android:textAppearance одной операцией setSpan(). Вместо трёх отдельных спанов (цвет, размер, шрифт) используется один TextAppearanceSpan со ссылкой на стиль. Это снижает количество объектов и упрощает поддержку — изменение стиля в ресурсе автоматически применяется ко всем текстам, где используется этот спад.
Создание нового экземпляра спана для каждого setSpan() приводит к дополнительной нагрузке на сборщик мусора. Оптимально создавать константные объекты спанов, если они применяются многократно. Например, ForegroundColorSpan(Color.RED) можно сохранить в companion object и переиспользовать. Однако спаны с состоянием (ClickableSpan с разными обработчиками) должны создаваться индивидуально для каждого случая.
Для диагностики проблем с производительностью спанов используйте Layout Inspector в Android Studio и профилировщик GPU. Если TextView с большим количеством спанов заметно тормозит при скролле, рассмотрите замену части спанов на статические стили через TextAppearanceSpan или сокращение количества спанов путём объединения атрибутов в кастомные реализации UpdateAppearance.
Часто задаваемые вопросы
String — неизменяемая последовательность символов без поддержки стилей. SpannableString хранит те же символы, но дополнительно содержит массив Span-объектов с информацией о форматировании. TextView определяет тип переданного CharSequence и применяет спаны к соответствующим диапазонам при отрисовке. String игнорирует любые стилевые атрибуты и отображается как plain text.
Метод removeSpan(Object span) удаляет конкретный спад. Для полной очистки вызовите getSpans(0, length, Object::class.java), который возвращает массив всех спанов, и затем каждый удалите через removeSpan. Альтернативно создайте новый SpannableString(text.toString()) без спанов. SpannableStringBuilder имеет метод clear(), который удаляет и текст, и спаны.
SpannableString работает в EditText, но для редактируемого текста предпочтительнее SpannableStringBuilder, реализующий Editable. EditText требует Editable интерфейс для отслеживания изменений. Если передать SpannableString в EditText, текст отобразится со стилями, но при редактировании Android преобразует его в Editable, что может сбросить часть спанов.
В Jetpack Compose спаны Android SDK не используются напрямую. Вместо этого Compose предоставляет AnnotatedString — собственный аналог SpannableString с аналогичными возможностями: SpanStyle для стилей отдельных символов и ParagraphStyle для абзацев. Конвертация SpannableString в AnnotatedString возможна через buildAnnotatedString с итерацией по спанам.
Создайте класс, наследующий от CharacterStyle, и переопределите метод updateDrawState(TextPaint tp). Внутри метода измените свойства TextPaint: цвет, толщину линии, эффекты. Для метрических изменений используйте UpdateLayout или MetricAffectingSpan. Кастомные спаны применяются через setSpan() так же, как и встроенные.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также