Темы, стили и другие


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

Содержание


Введение
Атрибуты для RectView
Cтиль для RectView
Стиль по умолчанию
Атрибут темы для стиля RectView
Тема на уровне Активити
Тема на уровне вью
Порядок применения значений атрибутов
Темы в ресурсах
Итоги

Введение


Стили и Темы в андроиде — это механизмы, позволяющее отделить детали оформления (например, цвет, размер шрифта и т.д) от структуры 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)
    }
}


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



Cтиль для 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: Если статья будет интересна, напишу более продвинутую статью или статью с большим количеством реальных примеров.
  • +22
  • 5.2k
  • 7
Support the author
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    +2
    Отличная статья. Автор объяснил очень доходчиво.
    Спасибо.
      0
      Спасибо. Я старался.
      Из забавного, сразу после публикации захотелось все переписать.
      0
      Спасибо за статью! Помогло навести порядок в голове) Есть просьба, можешь приложить ссылки на источники. И жду продолжения!
        0
        Спасибо, прямо то чего не хватало. Подписался в ожидании продолжения.
          0
          Изначально статья была сильно больше, я ее сильно сократил. Сейчас думаю как перекомпилировать материал и зайти с противоположной стороны.
          0
          Автор, продолжайте, пожалуйста. Написано очень легко для понимания.
            0
            В процессе. Выкраиваю время по возможности.

          Only users with full accounts can post comments. Log in, please.