Пролеты

Попробуйте способ создания композиций.
Jetpack Compose — рекомендуемый набор инструментов для создания пользовательского интерфейса для Android. Узнайте, как использовать текст в Compose.

Элементы `<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
);
Изображение, на котором текст серого цвета, частично красного.
Рисунок 1. Текст, оформленный с помощью 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)");
Изображение, демонстрирующее, как элемент span добавляет больше текста при использовании параметра SPAN_EXCLUSIVE_INCLUSIVE.
Рисунок 2. При использовании 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)`.
Рисунок 3. Текст с несколькими элементами span: ForegroundColorSpan(Color.RED) и StyleSpan(BOLD) .

типы сегментов Android

В Android доступно более 20 типов элементов <span> в пакете android.text.style . Android классифицирует элементы <span> двумя основными способами:

  • Как элемент `<span>` влияет на текст: элемент `<span>` может влиять на внешний вид текста или на его метрики.
  • Область действия тега `<span>`: некоторые теги `<span>` могут применяться к отдельным символам, в то время как другие должны применяться ко всему абзацу.
Изображение, демонстрирующее различные категории размеров проймы.
Рисунок 4. Категории диапазонов Android.

В следующих разделах эти категории описаны более подробно.

Элементы, влияющие на внешний вид текста.

Некоторые элементы <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`.
Рисунок 5. Текст подчеркнут с помощью 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.
Рисунок 6. Текст увеличен с помощью RelativeSizeSpan .

Применение элемента span, влияющего на метрики текста, приводит к тому, что наблюдающий объект перемеряет текст для корректной компоновки и отображения — например, изменение размера текста может привести к тому, что слова будут отображаться на разных строках. Применение предыдущего элемента span запускает перемер, перерасчет компоновки текста и перерисовку текста.

Элементы, влияющие на метрики текста, наследуют класс MetricAffectingSpan — абстрактный класс, позволяющий подклассам определять, как элемент влияет на измерение текста, предоставляя доступ к TextPaint . Поскольку MetricAffectingSpan наследует CharacterStyle , подклассы влияют на внешний вид текста на уровне символов.

Промежутки, влияющие на абзацы

Элемент `<span>` также может влиять на текст на уровне абзаца, например, изменяя выравнивание или поля блока текста. Элементы `<span>`, влияющие на целые абзацы, реализуют ParagraphStyle . Чтобы использовать такие элементы, их нужно прикрепить ко всему абзацу, за исключением символа новой строки в конце. Если вы попытаетесь применить элемент `<span>` к чему-либо, кроме всего абзаца, Android вообще не применит его.

На рисунке 8 показано, как Android разделяет абзацы в тексте.

Рисунок 7. В 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.
Рисунок 8. Элемент 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 .