Урок 4. Файл styles.xml, знакомство с TextAppearance

Код начала урока:

gitzip

Структура урока:

Видео версия урока

Файл styles.xml нужен для того, чтобы объединять повторяющиеся атрибуты элементов в стили. Давайте рассмотрим это сразу на практике.

На прошлом уроке мы закончили на такой структуре файла.

activity_user_info.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp">

    <ImageView
        android:id="@+id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/user_name_text_view"
        android:layout_below="@id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:text="Имя"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/user_nick_text_view"
        android:layout_below="@id/user_name_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:text="Ник"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/user_description_text_view"
        android:layout_below="@id/user_nick_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:text="Описание"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/user_location_text_view"
        android:text="Местоположение"
        android:layout_below="@id/user_description_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/following_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:text="@string/following_hint"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/followers_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_toEndOf="@+id/following_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="5dp"
        android:text="@string/followers_hint"
        android:textSize="16sp"/>

</RelativeLayout>

Представим, что вам необходимо поменять размер текста и цвет. Вам придётся внести изменения в 6 местах. Специально для того, чтобы локализировать изменения в одном месте мы воспользуемся стилем. Мы видим, что во всех TextView повторяются атрибуты:

  • android:layout_width="wrap_content"
  • android:layout_height="wrap_content"
  • android:layout_marginTop="5dp"
  • android:textSize="16sp"

Добавление стилей

Зайдём в файл styles.xml, который располагается по пути app/res/value, и создадим стиль с этими атрибутами. Там же будет находиться AppTheme для нашего приложения, но мы пока оставим его без внимания.

styles.xml

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="TextViewPrimary">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginTop">5dp</item>
    </style>
</resources>

Мы добавили стиль TextViewPrimary. Посмотрим как применить его в нашем layout файле:

activity_user_info.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp">

    <ImageView
        android:id="@+id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/user_name_text_view"
        android:layout_below="@id/user_image_view"
        style="@style/TextViewPrimary"
        android:text="Имя"/>

    <TextView
        android:id="@+id/user_nick_text_view"
        android:layout_below="@id/user_name_text_view"
        style="@style/TextViewPrimary"
        android:text="Ник"/>

    <TextView
        android:id="@+id/user_description_text_view"
        android:layout_below="@id/user_nick_text_view"
        style="@style/TextViewPrimary"
        android:text="Описание"/>

    <TextView
        android:id="@+id/user_location_text_view"
        android:text="Местоположение"
        android:layout_below="@id/user_description_text_view"
        style="@style/TextViewPrimary"/>

    <TextView
        android:id="@+id/following_text_view"
        android:layout_below="@id/user_location_text_view"
        style="@style/TextViewPrimary"
        android:text="@string/following_hint"/>

    <TextView
        android:id="@+id/followers_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_toEndOf="@+id/following_text_view"
        style="@style/TextViewPrimary"
        android:layout_marginStart="10dp"
        android:text="@string/followers_hint"/>

</RelativeLayout>

Вы можете заметить, что старые атрибуты удалились из элементов, а вместо них появился один новый style="@style/TextViewPrimary. Так стили и работают.

Когда вы запустите приложение или посмотрите на панель Preview, то увидите, что все атрибуты применяются как и прежде. Теперь вы можете изменять размер текста, его цвет, отступы в одном месте. Очень важно создавать стили на ранних этапах проектирования layout. Однако они необходимы только в том случае, когда атрибуты действительно должны переиспользоваться в нескольких элементах.

Давайте сделаем наш layout чуть более приближенный к реальности. Помним, что часть TextView должна отображаться чёрным цветом, а часть – серым.

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

Как только вы поняли, что вы копируете часть кода (xml или java, да и вообще любого кода) это признак того, что вы что-то делаете не так.

Наследование стилей

Явное наследование стилей

Именно для таких случаев в Android существует наследование стилей. Мы можем унаследоваться от нашего первого стиля и переопределить (как методы в java) атрибуты, которые мы унаследовали от нашего parent-стиля:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextViewPrimary">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginTop">5dp</item>
    </style>

    <style name="TextViewSecondary" parent="TextViewPrimary">
        <item name="android:textColor">@color/gray_dove_light</item>
    </style>

</resources>

Выглядит уже лучше, но предположим, что нам надо добавить атрибут marginBottom к стилю TextViewPrimary, но в стиле TextViewSecondary нам не нужен этот атрибут. В этом случае нам нужно будет переопределить этот атрибут, поставив ему значение ноль в TextViewSecondary:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextViewPrimary">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginTop">5dp</item>
        <item name="android:layout_marginBottom">10dp</item>
    </style>

    <style name="TextViewSecondary" parent="TextViewPrimary">
        <item name="android:textColor">@color/gray_dove_light</item>

        <!-- встречаете 0dp, значит что-то пошло не так -->
        <item name="android:layout_marginBottom">0dp</item>
    </style>

</resources>

В будущем нам нужно будет делать так с каждым атрибутом. Это опять должно вас наталкивать на мысль, что что-то пошло не так. Для того, чтобы избежать этого, нам необходимо создать базовый стиль с общими атрибутами и унаследовать от него два наших стиля. Также давайте добавим цвет @color/black к нашему основному тексту, чтобы он брался не из темы, а из нашего стиля:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginTop">5dp</item>
    </style>

    <style name="TextViewPrimary" parent="TextView">
        <item name="android:textColor">@color/black</item>
    </style>

    <style name="TextViewSecondary" parent="TextView">
        <item name="android:textColor">@color/gray_dove_light</item>
    </style>

</resources>

Так наши стили организованы намного удобнее с точки зрения чтения кода и сопровождения. Если нам надо добавить какой-то общий атрибут, то мы добавляем его в стиль TextView. Если в какой-то другой, то добавляем его в одно место, нигде ничего не исправляя.

Неявное наследование с помощью символа “точка” (.)

Также стоит отметить, что в стилях, которые мы создали, мы можем использовать неявное наследование. Вместо этого просто дописываем точку после стиля, от которого хотим унаследоваться. Вот как выглядят наши стили, если использовать этот подход наследования:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginTop">5dp</item>
    </style>

    <style name="TextView.Primary">
        <item name="android:textColor">@color/black</item>
    </style>

    <style name="TextView.Secondary">
        <item name="android:textColor">@color/gray_dove_light</item>
    </style>

</resources>

Такой подход выглядит более читабельным. Такие стили мы и оставим с вами для дальнейшего использования.

Важно: мы не можем использовать этот подход, если наследуемся от стилей библиотеки Android.

TextAppearance

В Android нет множественного наследования стилей, хотя иногда это может быть полезным.

Для этого для атрибутов текста в Android можно использовать свойство TextAppearance. Оно по факту представляет из себя ещё один стиль, но принимающий только атрибуты текста.

Чаще всего в приложении текст выглядит схожим в элементах Button, EditText, TextView. Однако все остальные атрибуты у них могут различаться. Поэтому можно создать стили для текста и включать их в стили всех остальных компонентов.

Посмотрим на наш экран ещё раз:

UserInfoLayout.png

Красными прямоугольниками выделен текст, который должен отображаться серым цветом (Secondary). Т.е. у нас в приложении текст будет делиться на Primary, Secondary. Т.к размер у текста пока один и тот же, создадим базовый стиль Text, в котором оставим размер текста. Добавим эти стили:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="Text">
        <item name="android:textSize">16sp</item>
    </style>

    <style name="Text.Primary">
        <item name="android:textColor">@color/black</item>
    </style>

    <style name="Text.Secondary">
        <item name="android:textColor">@color/gray_dove_light</item>
    </style>

</resources>

Теперь давайте добавим использование наших стилей текста в стили TextView:

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginTop">5dp</item>

        <!--включаем стиль текста с помощью атрибута-->
        <item name="android:textAppearance">@style/Text</item>
    </style>

    <style name="TextView.Primary">
        <!--включаем стиль текста с помощью атрибута-->
        <item name="android:textAppearance">@style/Text.Primary</item>
    </style>

    <style name="TextView.Secondary">
        <!--включаем стиль текста с помощью атрибута-->
        <item name="android:textAppearance">@style/Text.Secondary</item>
    </style>

</resources>

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

Теперь добавим использование этих стилей в файл activity_user_info.xml. Помним, что элементы user_nick_text_view, user_location_text_view, following_text_view, followers_text_view отображаются как текст серого цвета (Secondary). А все остальные, как основной (Primary) текст. В результате наш activity_user_info.xml выглядит так:

activity_user_info.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp">

    <ImageView
        android:id="@+id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/user_name_text_view"
        android:layout_below="@id/user_image_view"
        style="@style/TextView.Primary"
        android:text="Имя"/>

    <TextView
        android:id="@+id/user_nick_text_view"
        android:layout_below="@id/user_name_text_view"
        style="@style/TextView.Secondary"
        android:text="Ник"/>

    <TextView
        android:id="@+id/user_description_text_view"
        android:layout_below="@id/user_nick_text_view"
        style="@style/TextView.Primary"
        android:text="Описание"/>

    <TextView
        android:id="@+id/user_location_text_view"
        android:text="Местоположение"
        android:layout_below="@id/user_description_text_view"
        style="@style/TextView.Secondary"/>

    <TextView
        android:id="@+id/following_text_view"
        android:layout_below="@id/user_location_text_view"
        style="@style/TextView.Secondary"
        android:text="@string/following_hint"/>

    <TextView
        android:id="@+id/followers_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_toEndOf="@+id/following_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginStart="10dp"
        android:text="@string/followers_hint"/>

</RelativeLayout>

Поздравляем! Мы пришли к нужному результату:

TextAppearanceResult.png

Можем заметить, что элементы followers_text_view, following_text_view отображаются на английском языке. Это связано с тем, что мы ссылаемся на ресурсы, и во вкладке Preview по умолчанию подтягиваются ресурсы из стандартного файла values/strings.xml. Это можно изменить, в верхнем баре режима Preview выбрав язык, для которого Android Studio будет подтягивать ресурсы. Во время запуска приложения на русском языке всё будет отображаться так, как нам и надо.

Удаление атрибута marginTop из базового стиля

Один момент, который нам лучше решить сейчас, а то в будущем с ним будет много проблем. Мы оставили атрибут marginTop в базовом стиле TextView. Мы будем использовать этот стиль ещё на нескольких экранах нашего приложения, причём атрибут marginTop нам будет мешать.

Обычно в базовые стили редко добавляют атрибуты, которые влияют на местоположение элемента: gravity, layoutGravity, margin. Потому что это вопрос времени, когда появится атрибут, которому этот стиль будет мешать.

У нас есть два варианта, как решить эту проблему:

  • Создать стили TextView.Primary.MarginTop, TextView.Secondary.MarginTop, TextView.Header.MarginTop, TextView.Bold.MarginTop.
  • Указать атрибуты marginTop непосредственно в layout, а не в стилях.

Первый подход выглядит привлекательнее. Однако представьте ситуацию, когда у нас появился ещё один атрибут (допустим padding). Причём у части элементов он есть, а у части отсутствует. Тогда нам придётся создать ещё кучу стилей такого вида: TextView.Primary.MarginTop.Padding, TextView.Primary.Padding и т.д.

Если вы пишите непосредственно название атрибута в стиле, это может наталкивать на мысль, что вам лучше написать этот атрибут непосредственно в layout файле. Зато стили будут более организованными, читабельными.

Давайте удалим из стиля TextView атрибут <item name="android:layout_marginTop">5dp</item> и добавим его к каждому TextView в нашем layout.

styles.xml

<resources>

    <!-- Остальные элементы сверху не изменились-->

    <style name="TextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <!--включаем стиль текста с помощью атрибута-->
        <item name="android:textAppearance">@style/Text</item>
    </style>

    <!-- Остальные элементы снизу не изменились-->

</resources>

activity_user_info.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp">

    <ImageView
        android:id="@+id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/user_name_text_view"
        android:layout_below="@id/user_image_view"
        style="@style/TextView.Primary"
        android:layout_marginTop="5dp"
        android:text="Имя"/>

    <TextView
        android:id="@+id/user_nick_text_view"
        android:layout_below="@id/user_name_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="5dp"
        android:text="Ник"/>

    <TextView
        android:id="@+id/user_description_text_view"
        android:layout_below="@id/user_nick_text_view"
        style="@style/TextView.Primary"
        android:layout_marginTop="5dp"
        android:text="Описание"/>

    <TextView
        android:id="@+id/user_location_text_view"
        android:text="Местоположение"
        android:layout_below="@id/user_description_text_view"
        android:layout_marginTop="5dp"
        style="@style/TextView.Secondary"/>

    <TextView
        android:id="@+id/following_text_view"
        android:layout_below="@id/user_location_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="5dp"
        android:text="@string/following_hint"/>

    <TextView
        android:id="@+id/followers_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_toEndOf="@+id/following_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="5dp"
        android:layout_marginStart="10dp"
        android:text="@string/followers_hint"/>

</RelativeLayout>

В нашем layout стало чуть больше кода. И добавилась ещё одна проблема: представим, что мы решили, что отступ должен быть равен 6dp. Теперь нам надо внести изменение в 6 местах.

Знакомство с файлом dimens

Для решения этой проблемы в Android есть файл, в котором можно хранить размеры отступов, текстов. Файл называется dimens.xml. Давайте создадим его: нажмём правой кнопкой по папке res/values, выберем New -> Value resource file

DimensPopup.png

Введём имя файла dimens.

Файл создался. Добавим в него строку вида:

<dimen name="text_small_margin">5dp</dimen>

Догадались? Теперь давайте в нашем layout файле будем использовать ссылку на этот ресурс вместо того, чтобы писать 5dp. Теперь нам достаточно поменять значения атрибута только в одном месте.

Итоговая версия:

activity_user_info.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp">

    <ImageView
        android:id="@+id/user_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/user_name_text_view"
        android:layout_below="@id/user_image_view"
        style="@style/TextView.Primary"
        android:layout_marginTop="@dimen/text_small_margin"
        android:text="Имя"/>

    <TextView
        android:id="@+id/user_nick_text_view"
        android:layout_below="@id/user_name_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="@dimen/text_small_margin"
        android:text="Ник"/>

    <TextView
        android:id="@+id/user_description_text_view"
        android:layout_below="@id/user_nick_text_view"
        style="@style/TextView.Primary"
        android:layout_marginTop="@dimen/text_small_margin"
        android:text="Описание"/>

    <TextView
        android:id="@+id/user_location_text_view"
        android:text="Местоположение"
        android:layout_below="@id/user_description_text_view"
        android:layout_marginTop="@dimen/text_small_margin"
        style="@style/TextView.Secondary"/>

    <TextView
        android:id="@+id/following_text_view"
        android:layout_below="@id/user_location_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="@dimen/text_small_margin"
        android:text="@string/following_hint"/>

    <TextView
        android:id="@+id/followers_text_view"
        android:layout_below="@id/user_location_text_view"
        android:layout_toEndOf="@+id/following_text_view"
        style="@style/TextView.Secondary"
        android:layout_marginTop="@dimen/text_small_margin"
        android:layout_marginStart="10dp"
        android:text="@string/followers_hint"/>

</RelativeLayout>

styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="TextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textAppearance">@style/Text</item>
    </style>

    <style name="TextView.Primary">
        <item name="android:textAppearance">@style/Text.Primary</item>
    </style>

    <style name="TextView.Secondary">
        <item name="android:textAppearance">@style/Text.Secondary</item>
    </style>

    <style name="Text">
        <item name="android:textSize">16sp</item>
    </style>

    <style name="Text.Primary">
        <item name="android:textColor">@color/black</item>
    </style>

    <style name="Text.Secondary">
        <item name="android:textColor">@color/gray_dove_light</item>
    </style>
</resources>

dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="text_small_margin">5dp</dimen>
</resources>

Ключевые моменты:

  • Стили нужны, чтобы локализировать изменения в xml коде.
  • Когда чувствуете, что копируете xml разметку, остановитесь и вынесите переиспользуемые атрибуты в стиль.
  • Лучше всего, когда вы не переопределяете в наследуемом стиле атрибуты parent. Если вы так делаете, то стоит задумать о создании базового стиля, который будет хранить только общие атрибуты.
  • Стили для текста рекомендуется хранить в TextAppearance стилях. Это позволяет комбинировать стили элемента и стили текста.
  • Если вы используете одно и то же значение размера, отступа в нескольких местах, то лучше вынести это значение в файл dimens и просто ссылаться на него. Это позволит вносить изменение в одном месте.

Полезные материалы:

Полный листинг изменений кода:

Code diff

УВИДЕТЬ ВСЕ Добавить заметку
ВЫ
Добавить ваш комментарий