Темы и стили в Android

Практически все разработчики знают, что в андроиде есть темы, но применение их обычно ограничивается копированием кусков xml из Stack Overflow или других ресурсов. В интернете есть информация по темам, но это обычно просто рецепт, как добиться определенного результата. В этой статье я постарался дать вводный обзор механизма стилизации андроида.

Введение

Стили и Темы в андроиде — это механизмы, позволяющее отделить детали оформления (например, цвет, размер шрифта и т.д) от структуры UI. Разобраться, как это работает, нам поможет простой пример с кастомной вью.

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

class RectView @JvmOverloads constructor(
    context: Context,
    attrSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrSet, defStyleAttr) {

    private val paint = Paint().apply {
        color = Color.BLUE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
    }
}

Атрибуты для RectView

Теперь хотелось бы иметь возможность менять цвет прямоугольника из верстки. Для этого нам надо добавить новый атрибут, добавим в файл attrs.xml:

<resources>
    <attr name="rectColor" format="color"/>
</resources>

Теперь мы можем в коде обращаться к id этого атрибута через R.attr.rectColor и в верстке экрана мы можем использовать атрибут app:rectColor.

<org.berendeev.themes.RectView
    android:id="@+id/text2"
    android:layout_width="100dp"
    app:rectColor="@color/colorPrimary"
    android:layout_height="100dp"/>

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

Давайте научим RectView понимать атрибут rectColor, добавим группу атрибутов:

<resources>
    <attr name="rectColor" format="color"/>
    <declare-styleable name="RectView">
    <attr name="rectColor"/>
    </declare-styleable>
</resources>

В классе R сгенерировались 2 новых поля:

  • R.styleable.RectView — это массив id атрибутов, в данный момент это массив из одного элемента R.attr.rectColor
  • R.styleable.RectView_rectColor — это индекс id атрибута в массиве R.styleable.RectView, т.е. id атрибута мы можем получить и так:
R.styleable.RectView[R.styleable.RectView_rectColor]

И добавим в код поддержку атрибута rectColor:

init {
    val typedArray = context.theme.obtainStyledAttributes(
        attrSet,
        R.styleable.RectView,
        0,
        0
    )
    try {
        paint.color = typedArray.getColor(
            R.styleable.RectView_rectColor,
            Color.BLUE
        )
    } finally {
        typedArray.recycle()
    }
}

Полный код:

class RectView @JvmOverloads constructor(
    context: Context,
    attrSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrSet, defStyleAttr) {

    private val paint = Paint().apply {
    color = Color.BLUE
    }

    init {
        val typedArray = context.theme.obtainStyledAttributes(
            attrSet,
            R.styleable.RectView,
            0,
            0
        )
        try {
            paint.color = typedArray.getColor(
                R.styleable.RectView_rectColor,
                Color.BLUE
            )
        } finally {
            typedArray.recycle()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
    }
}

Теперь мы можем менять цвет прямоугольника из верстки:

Стиль для RectView

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

Решение – вынести все в стиль.

<style name="DefaultRectViewStyle">
    <item name="rectColor">@color/colorAccent</item>
</style>

Теперь мы можем создать сколько угодно вью с одинаковым оформлением, указав style.

<org.berendeev.themes.RectView
    android:id="@+id/text2"
    android:layout_width="100dp"
    style="@style/DefaultRectViewStyle"
    android:layout_height="100dp"/>

Стиль по умолчанию

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

context.theme.obtainStyledAttributes(
    attrSet,
    R.styleable.RectView,
    0,
    R.style.DefaultRectViewStyle
)

Этого достаточно. Теперь все RectView, которые мы добавляем в верстку, будут иметь стиль по умолчанию DefaultRectViewStyle.

Атрибут темы для стиля RectView

Значение по умолчанию — это хорошо, но хотелось бы управляет оформлением гибче. Удобно задавать стиль конкретного вью на все приложение целиком или для отдельного Activity.

Для этого нам понадобится новый атрибут. Его значение мы будем задавать в теме, значением будет стиль, определяющий внешний вид RectView.

<attr name="rectViewStyle" format="reference"/>

И научим наш rectView понимать этот атрибут темы, передав в метод obtainStyledAttributes третий параметр R.attr.rectViewStyle.

context.theme.obtainStyledAttributes(
    attrSet,
    R.styleable.RectView,
    R.attr.rectViewStyle,
    R.style.DefaultRectViewStyle
)

Теперь, если в теме будет задан item с именем rectViewStyle и значением типа стиль, то этот стиль применится ко всем RectView с этой темой.

Тема на уровне Активити

Зададим значение атрибуту rectViewStyle в нашей теме.

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="rectViewStyle">@style/LocalRectViewStyle</item>
    </style>
</resources>

Указываем тему в манифесте приложения.

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android";
    package="org.berendeev.themes">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Всем активити будет установлена тема AppTheme по умолчанию. Мы задали оформление RectView для всего приложения.

Тему также можно задать для конкретной Activity в манифесте:

<activity android:name=".MainActivity" android:theme="@style/MyTheme">

Тут важно, что тема, которую мы будем задавать на уровне приложения или активити, должна быть унаследована от стандартной темы. Например, Theme.AppCompat. Иначе мы получим краш в рантайме.

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

Тема на уровне вью

На уровне вью мы не устанавливаем тему заново, а перезаписываем нужные значения атрибутов, поэтому наследоваться от стандартной темы не нужно. Родитель может быть пустым или, например, мы можем отнаследоваться от одного из оверлеев ThemeOverlay.AppCompat.

Здесь мы задали тему MyOverlay группе LinearLayout и всем его потомкам по иерархии.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android";
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:theme="@style/MyOverlay">

    <org.berendeev.themes.RectView
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</LinearLayout>

В теме мы можем обойтись без родителя, т.к. мы просто модифицируем тему активити.

<style name="MyOverlay">
    <item name="rectViewStyle">@style/LocalRectViewStyle</item>
</style>

Порядок применения значений атрибутов

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

Темы в ресурсах

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

<shape xmlns:android="http://schemas.android.com/apk/res/android";
       android:shape="rectangle">
    <solid android:color="?attr/colorPrimary"/>
</shape>

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

Такой трюк можно использовать во всех ресурсах, например, в selector или в векторном рисунке. Это делает оформление более структурированным и гибким. Мы можем быстро поменять тему для всей активити.

Итоги

  • Тема и стиль на уровне ресурсов — одно и то же, но используются по-разному.
  • Тема может содержать в себе другие темы, просто значения или стили для вьюх.
  • Стиль указывается в верстке на уровне вью. LayoutInflater считает значения стиля и передаст их как AttributeSet в конструктор View.
  • Темы — это механизм, позволяющий определить оформление глобально для всей активити.
  • Значения темы можно менять для элемента (и потомков) в иерархии вью.
  • Значения темы можно использовать не только во View, но и в ресурсах.

PS: Если статья будет интересна, напишу более продвинутую статью или статью с большим количеством реальных примеров.

Источник: Темы, стили и другие