Друзья, отличной всем пятницы. Хотим поделиться с вами переводом статьи, подготовленным специально для студентов курса «Android-разработчик. Продвинутый курс». Приятного прочтения.



Как декларативно стилизовать текст на Android.


Иллюстрация Вирджинии Полтрэк

TextView в Android-приложениях предоставляет несколько атрибутов для стилизация текста и различные способы их применения. Эти атрибуты можно установить непосредственно в layout’e, применить стиль к view или тему к layout’у или, если захотите, установить textAppearance. Но что же из этого следует использовать? И что произойдет, если их скомбинировать?


Что и когда использовать?

В этой статье описываются различные подходы к декларативной стилизации текста (т. е. когда вы определяете стили в XML файле), рассматриваются области применения и приоритет каждого из методов.

tl;dr;


Вам следует прочитать весь пост, но вот краткое изложение.

Помните о порядке приоритетности различных методов стилизации — если вы пытаетесь стилизовать какой-то текст и не видите ожидаемых результатов, то, скорее всего, ваши изменения переопределены чем-то с более высоким приоритетом в этой иерархии:


Иерархия методов стилизации текста

Я бы предложил следующий порядок действий для стилизации:

  1. Установите любой стиль приложения в textViewStyle как стиль по умолчанию для вашей темы.
  2. Установите (небольшой) набор TextAppearance, которые ваше приложение будет использовать (или использовать/наследовать от стилей MaterialComponent), и ссылайтесь на них прямо из ваших view
  3. Создайте style, устанавливая атрибуты, не поддерживаемые TextAppearance (которые сами собой будут определять один из ваших TextAppearance).
  4. Выполните любую уникальную стилизацию прямо в layout’е.

Продемонстрируйте немного стиля


Вы можете напрямую устанавливать атрибуты TextView в layout’е, но этот подход может быть более утомительным и небезопасным. Представьте себе, что вы таким образом пытаетесь обновить цвет всех TextView в приложении. Как и во всех других view, вы можете (и должны!) вместо этого использовать стили для обеспечения согласованности, повторного использования и простоты обновлений. С этой целью я рекомендую создавать стили для текста всякий раз, когда вы, вероятно, захотите применить один и тот же стиль к нескольким view. Это чрезвычайно просто и в значительной степени поддерживается view-системой Android.

Что происходит под капотом, когда вы задаете стиль для view? Если вы когда-либо писали свой custom view, вы, вероятно, видели вызов context.obtainStyledAttributes(AttributeSet, int[], int, int). Таким образом view-система в Android передает в view атрибуты, указанные в layout’е. Параметр AttributeSet, по сути, можно рассматривать как карту XML параметров, которые вы указываете в вашем layout’е. Если AttributeSet устанавливает стиль, то стиль читается первым, а затем к нему применяются атрибуты, указанные непосредственно во view. Таким образом, мы подошли к первому правилу приоритетности.

View > Style

Атрибуты, определенные непосредственно во view, всегда «превалируют» и переопределяют атрибуты, определенные в стиле. Обратите внимание, что применяется сочетание атрибутов style и view; определение атрибута в view, который также указывается в стиле, не отменяет весь стиль. Также следует отметить, что в ваших view нет реального способа определить, откуда берется стилизация; это решается view системой за вас единожды при подобном вызове. Вы не можете получить оба варианта и выбрать.

Хотя стили чрезвычайно полезны, у них есть свои ограничения. Одним из них является то, что вы можете применить только один стиль к view (в отличие от чего-то вроде CSS, где вы можете применять несколько классов). У TextView, однако, есть хитрость, он предоставляет атрибут TextAppearance, который работает аналогично style. Если вы стилизуете текст через TextAppearance, то оставляете атрибут style свободным для других стилей, что выглядит практично. Давайте подробнее рассмотрим, что такое TextAppearance и как он работает.

TextAppearance


В TextAppearance нет ничего волшебного (например, секретного режима для применения нескольких стилей, о котором вы не должны знать!!!!), TextView избавляет вас от некоторой лишней работы. Давайте посмотрим на конструктор TextView, чтобы понять, что происходит.

TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
TypedArray appearance = null;
int ap = a.getResourceId(com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
a.recycle();
if (ap != -1) {
 appearance = theme.obtainStyledAttributes(ap, com.android.internal.R.styleable.TextAppearance);
}
if (appearance != null) {
 readTextAppearance(context, appearance, attributes, false);
 appearance.recycle();
}
// a little later
a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
readTextAppearance(context, a, attributes, true);

Итак, что здесь происходит? По сути, TextView сначала смотрит, указали ли вы android:textAppearance, если это так, он загружает этот стиль и применяет все свойства, которые там указаны. Позже он загружает все атрибуты из view (которые помнит, включая стиль) и применяет их. Так мы подходим ко второму правилу приоритетности:

View > Style > TextAppearance

Поскольку внешний вид текста (text appearance) проверяется первым, любые атрибуты, определенные либо непосредственно во view, либо в style, будут переопределять его.

С TextAppearance следует помнить о еще одном предостережении: он поддерживает подмножество атрибутов стиля, которые предлагает TextView. Чтобы лучше понять, что я имею в виду, давайте вернемся к этой строке:

obtainStyledAttributes(ap, android.R.styleable.TextAppearance);

Мы рассматривали версию receiveStyledAttributes с 4 аргументами, эта 2-аргументная версия немного отличается от нее. Она смотрит на данный стиль (как определено первым параметром id) и фильтрует его только по атрибутам в стиле, которые появляются во втором параметре, массиве attrs. Таким образом, styleable android.R.styleable.TextAppearance определяет область действия TextAppearance. Глядя на это определение, мы видим, что TextAppearance поддерживает многие, но не все атрибуты, которые поддерживает TextView.

<attr name="textColor" />
<attr name="textSize" />
<attr name="textStyle" />
<attr name="typeface" />
<attr name="fontFamily" />
<attr name="textColorHighlight" />
<attr name="textColorHint" />
<attr name="textColorLink" />
<attr name="textAllCaps" format="boolean" />
<attr name="shadowColor" format="color" />
<attr name="shadowDx" format="float" />
<attr name="shadowDy" format="float" />
<attr name="shadowRadius" format="float" />
<attr name="elegantTextHeight" format="boolean" />
<attr name="letterSpacing" format="float" />
<attr name="fontFeatureSettings" format="string" />

Атрибуты стилизации, поддерживаемые TextAppearance

Вот некоторые атрибуты TextView, которые не включены в TextAppearance: lineHeight[Multiplier|Extra], lines, breakStrategy и hyphenationFrequency. TextAppearance работает на уровне символов, а не параграфов, поэтому атрибуты, влияющие на весь layout, не поддерживаются.

Поэтому TextAppearance очень полезен, он позволяет нам определять стиль, ориентированный на атрибуты стилизации текста, и оставляет style в view свободным для других целей. Однако он имеет ограниченную область применения и находится в нижней части цепочки приоритетов, так что не забывайте об ограничениях.

Разумные значения по умолчанию


Когда мы рассматривали то, как view-система Android разрешает атрибуты (context.obtainStyledAttributes), мы на самом деле немного упростили ее. Она вызывает theme.obtainStyledAttributes (используя текущую Theme Context’а). При проверке ссылки показывается порядок приоритетности, который мы рассматривали ранее, и указывается еще 2 места, которые он ищет для разрешения атрибутов: стиль по умолчанию для view и тема.

При определении окончательного значения определенного атрибута в игру вступают четыре входных параметра:

  1. Любые значения атрибутов в данном AttributeSet.
  2. Ресурс стиля, указанный в AttributeSet (с именем «style»).
  3. Стиль по умолчанию, указанный в defStyleAttr и defstyleres
  4. Базовые значения в этой теме.

Порядок приоритетов стилизации из документации Theme

Мы вернемся к темам, но давайте сначала вглянем на стили по умолчанию. Что это еще за стиль по умолчанию? Чтобы ответить на этот вопрос, я думаю, что было бы полезно сделать небольшой выход из темы TextView и взглянуть на простой Button. Когда вы вставляете <Button> в свой layout, он выглядит примерно так.


Стандартный Button

Почему? Если вы посмотрите на исходный код Button, то увидете, что он довольно скудный:

public class Button extends TextView {
 public Button(Context context) {
   this(context, null);
 }
 public Button(Context context, AttributeSet attrs) {
   this(context, attrs, com.android.internal.R.attr.buttonStyle);
 }
 public Button(Context context, AttributeSet attrs, int defStyleAttr) {
   this(context, attrs, defStyleAttr, 0);
 }
 public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
   super(context, attrs, defStyleAttr, defStyleRes);
 }
 @Override public CharSequence getAccessibilityClassName() {
   return Button.class.getName();
 }
 @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
   if (getPointerIcon() == null && isClickable() && isEnabled()) {
     return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
   }
   return super.onResolvePointerIcon(event, pointerIndex);
 }
}

Это все! Вот весь класс (без комментариев). Вы можете сами проверить здесь. Я подожду. Так откуда же берутся фон, заглавные буквы, пульсация и т.д.? Возможно, вы пропустили, но это все определятся в конструкторе с 2 аргументами; тот, который вызывается, когда layout берется из XML. Это последний параметр, который определяет defaultStyleAttr в com.android.internal.R.attr.buttonStyle. Это стиль по умолчанию, по сути, являющийся точкой косвенного обращения, позволяющей вам указать стиль, который будет использоваться по умолчанию. Он не указывает непосредственно на стиль, но позволяет вам указать на один из тех, что указаны в вашей теме, который он будет проверять при разрешении атрибутов. И это именно то, что делают все темы, от которых вы обычно наследуетесь, чтобы обеспечить вид и восприятие стандартных виджетов. Например, если посмотреть на тему Material, она определяет @style/Widget.Material.Light.Button, и именно этот стиль предоставляет все атрибуты, которые передаст theme.obtainStyledAttributes, если вы не указали ничего другого.

Вернемся к TextView, он также предлагает стиль по умолчанию: textViewStyle. Это может быть очень удобно, если вы хотите применить некоторые стили к каждому TextView в вашем приложении. Допустим, вы хотите установить значение межстрочного интервала по умолчанию на 1,2. Вы можете сделать это с помощью style/TextAppearance и попытаться применить во время код-ревью (или, может быть, даже с помощью элегантого пользовательского правила в Lint), но вам нужно быть бдительными и быть уверенными в том, что вы наберете новых членов команды, будете осторожны с рефакторингом и т. д.

Лучшим подходом может быть указание собственного стиля по умолчанию для всех TextView в приложении, задающего желаемое поведение. Вы можете сделать это, установив свой собственный стиль для textViewStyle, который наследуется от платформы или от MaterialComponents/AppCompat по умолчанию.

<style name="Theme.MyApp"
 parent="@style/Theme.MaterialComponents.Light">
 ...
 <item name="android:textViewStyle">@style/Widget.MyApp.TextView</item></style>
<style name="Widget.MyApp.TextView"
 parent="@android:style/Widget.Material.TextView">
 <item name="android:textAppearance">@style/TextAppearance.MyApp.Body</item>
 <item name="android:lineSpacingMultiplier">@dimen/text_line_spacing</item>
</style>

С учетом этого, наше правило приоритетности принимает вид:

View > Style > Default Style > TextAppearance

Как часть разрешения атрибутов view-системы, этот слот заполняется после стилей (так что все в стиле по умолчанию аннулируется примененным стилем или атрибутами view), но все равно будет переопределять text appearance. Стили по умолчанию могут быть очень удобными. Если вы когда-нибудь надумаете писать свой собственный custom view, то они могут стать мощным способом реализации поведения по умолчанию, позволяя легко его настраивать.

Если вы наследуете виджет и не указываете свой собственный стиль по умолчанию, тогда обязательно используйте стиль родительских классов по умолчанию в конструкторах (не передавайте просто 0). Например, если вы наследуетесь от AppCompatTextView и пишете свой собственный конструктор с 2 аргументами, обязательно передайте android.R.attr.textViewStyle как defaultStyleAttr (как здесь), иначе вы потеряете поведение родительского класса.

Темы


Как упоминалось ранее, есть еще один (последний, я обещаю) способ предоставления информации о стилизации. Другое место theme.obtainStyledAttributes будет смотреть прямо в саму тему. То есть, если вы добавите в свою тему атрибут стиля, например android:textColor, view-система выберет его в качестве последнего средства. Как правило, это плохая идея — смешивать атрибуты темы и атрибуты стиля, то есть то, что вы применяете непосредственно к view, как правило, никогда не должно устанавливаться для темы (и наоборот), но есть пара редких исключений.

Одним из примеров может быть ситуация, когда вы пытаетесь изменить шрифт во всем приложении. Вы можете использовать один из методов, описанных выше, но ручная настройка стилей/внешнего вида текста повсюду будет монотонна и небезопасна, а стили по умолчанию будут работать только на уровне виджета; подклассы могут переопределять это поведение, например кнопки определяют свой собственный android:buttonStyle, который не подхватит ваш android:textViewStyle. Вместо этого вы можете указать шрифт в вашей теме:

<style name="Theme.MyApp"
 parent="@style/Theme.MaterialComponents.Light">
 ...
 <item name="android:fontFamily">@font/space_mono</item>
</style>

Теперь любой view, который поддерживает этот атрибут, подхватит его, если это не будет переопределено чем-то с более высоким приоритетом:

View > Style > Default Style > Theme > TextAppearance

Опять же, поскольку это является частью системы стилизации view, она будет переопределять все, что предоставляется в текстовом виде, но будет переопределена любыми более конкретными атрибутами.

Помните об этом приоритете. В нашем примере со шрифтом для всего приложения вы можете ожидать, что Toolbar подхватит этот шрифт, так как он содержит заголовок, который является TextView. Сам класс Toolbar, однако, определяет стиль по умолчанию, содержащий titleTextAppearance, который определяет android:fontFamily, и непосредственно устанавливает его в заголовке TextView, переопределяя значение уровня темы. Стили на уровне темы могут быть полезны, но их легко переопределить, поэтому убедитесь, что они применяются должным образом.

Бонус: Нерешенные вопросы


Вся эта статья была посвящена декларативному оформлению текста на уровне view, то есть тому, как стилизовать весь TextView во время заполнения. Любой стиль, примененный после заполнения (например, textView.setTextColor(…)), переопределит декларативную стилизацию. TextView также поддерживает более мелкие стили через Span. Я не буду вдаваться в эту тему, так как она подробно описана в статьях Флорины Мунтенеску (Florina Muntenescu).

> Spantastic text styling with Spans
> Underspanding spans

Я упомяну это для полноты картины, чтобы иметь в виду, что программная стилизация и span’ы будут находиться в верхней части порядка приоритетности:

Span > Setters > View > Style > Default Style > Theme > TextAppearance

Выберите свой стиль


Несмотря на то, что существует несколько способов стилизации текста, понимание различий между методами и их ограничениями помогает вам найти подходящий инструмент для выполнения конкретной задачи или понять, почему один метод имеет приоритет над другим.

Удачной стилизации текста!

Всех желающих приглашаем на бесплатный вебинар в рамках которого познакомимся с DI фреймворком Dagger 2: изучим, как Dagger2 генерирует код, разберемся с аннотациями JSR 330 и конструкциями Dagger2, будем учиться использовать Dagger2 в многомодульном приложении и рассмотрим Dagger Android Injector.

Комментарии (2)


  1. kloppspb
    15.06.2019 03:07

    Как читатель могу сказать, что есть два типа стиля: можно читать, или кто-то решил его «стилизировать».
    Элементов UI это тоже касается. Стандартный button прекрасен и понятен. До тех пор, пока его не касаются «улучшатели».

    P.S. А почему вы не приложили скриншоты ваших вариантов: что было до, и что после ваших «улучшений»?


    1. MaxRokatansky Автор
      17.06.2019 10:51

      Добрый день. Это перевод авторской статьи, все иллюстрации также взяты из оригинала. Если решим делать свой материал на схожую тему, обязательно приложим)