Привет читателям!
В какой-то момент любое крупное приложение разрастается так, что сложно везде поддерживать однотипный дизайн и динамично реагировать на любые изменения и тенденции в дизайне и UX-требованиях.
Поэтому решили внедрить в наше приложение дизайн-систему и добавить поддержку нескольких тем оформления.
Изучив различные способы, выработали свой подход к решению такой задачи. Хотелось сделать так, чтобы дизайн-систему и поддержку стилей можно было повторно использовать в других своих проектах. В соответствии с этой идеей разрабатывались компоненты и темы.
Компоненты дизайн-системы
Дизайн-система и её компоненты предназначены для унификации дизайна и стилевого единства во всем приложении.
Компонентами дизайн-системы в нашем случае будем называть custom view с возможностью адаптации к нескольким стилям приложения. Компоненты могут применяться в любом месте приложения (кнопки, элементы списка, заголовки и т.д.).
Проектирование, отладка и доработка компонентов дизайн-системы
Заказчиками компонентов дизайн-системы являются дизайнеры. С ними на первом этапе согласовываем надобность элемента (оценка переиспользуемости) и его функциональность.
После согласования должно быть понятно, какие опции нужно вынести в атрибуты custom view (цвет текста, текст, иконочку, цвет тинта иконочки и т.д.), а какие скрыть от изменений извне (это позволяет уберечь элемент от неправильного использования разработчиками).
Далее дизайнеры отрисовывают компонент в своих средах и отдают на разработку.
При реализации компонента нужно добавить поддержку тем (светлая или темная тема и т.д.) О том, как компонент поддерживает несколько тем, я расскажу ниже.
Лучшие методики
- Создать модуль с компонентами дизайн-системы. Из положительных моментов: отдельный модуль может быть использован в других приложениях, а модульность позволяет быстрее ориентироваться.
- Создать тестовое приложение с компонентами дизайн-системы. Это ускоряет разработку и отладку.
Способы внедрения темы в приложение
Мне известно два способа поддержки стилей в Android:
- Программный (программная перекраска).
- Стандартные механизмы стилей в Android.
Программный способ
Мы перекрашиваем всю иерархию view в runtime. Рекурсивно проходимся по ней и по определенным правилам перехода из одной темы в другую перекрашиваем компоненты. Те из них, которые не должны перекрашиваться, маркируются с помощью
android:tag
или android:contentDescription
. Эти компоненты не учитываются при разборе иерархии экрана.Перекрашивать можно как перед отображением экрана (например, в
onStart()
у Activity
), так и при работе с ним. Недостатки
- Требует дополнительных ресурсов, снижает производительность. Стилизация применяется после инициализации всех компонентов.
- Нужно быть внимательным к правилам перехода из одной темы в другую. Требуется учесть огромное множество правил перекраски, можно что-то забыть. Получается длинная простыня из
switch — case
(Java) илиwhen
(Kotlin). И в довесок требуется учесть элементы, которые не нужно красить при помощи вышеупомянутых тегов. - Нельзя частично перекрасить в соответствии с темами. В любом правиле есть исключения, и не всегда всё в приложении делается по дизайн-системе. Непонятно, как действовать если требуется частичная перекраска некоторых элементов.
Применение стиля сводится к описанию изменений в конкретных элементах:
if (view is TextView) {
view.setTextColor(
if (darkMode) R.color.blue else R.color.black
)
} else if (view is TabLayout) {
view.doAnything()
}
Достоинства
Не требует пересоздания Activity (это важно! Нет морганий при смене темы).
Я внедрил этот подход в одном известном всем продукте (см. скриншоты). Работает довольно быстро при простой однотипной вёрстке(в данном случае она была простая).
|
Стандартный механизм стилей в Android
Стиль — локальная стилизация экрана или view, затрагивающая только отдельный экран или view. Часто такую стилизацию называют «ThemeOverlay», или «легковесная» тема, которая позволяет переопределить атрибуты основной темы).
Тема — глобальная стилизация экранов приложения, затрагивающая подмену стилей, цветов и т.д. у всего, что мы видим на экранах приложения.
Темой можно считать множество стилизаций, которые можно переключать.
Примеры
В теме могут содержаться как стили конкретных view элементов, так и конкретные цвета.
<style name="DesignSystemTheme" parent="Theme.AppCompat.Light">
<!-- colors -->
<item name="cm_primary_background">#123456</item>
<item name="cm_secondary_background">#654321</item>
<!-- View's (Component's) styles -->
<item name="cm_header1_style">@style/Header1.Light</item>
<item name="cm_header2_style">@style/Header2.Light</item>
</style>
Здесь объявлен стиль для конкретной view:
<style name="Header1" parent="BaseTextWidget">
<item name="android:textSize">28sp</item>
<item name="lineHeight">34sp</item>
<item name="fontFamily">@font/roboto_bold</item>
</style>
<style name="Header1.Light">
<item name="android:textColor">#123456</item>
</style>
<style name="Header1.Dark">
<item name="android:textColor">#fedcba</item>
</style>
Стили поддерживают явное и неявное наследование:
- Явное:
Header1
унаследован отBaseTextWidget
. - Неявное:
Header1.Light
унаследован отHeader1
.
Если к текстовому элементу мы применим стиль
Header1
, то подтянется только Header1
. А атрибуты Header1.Light
или Header1.Dark
не применятся.Если к текстовому элементу мы применим стиль
Header1.Light
/Dark
, то подтянутся стили Header1.Light
/Dark
и Header1
(достоинство неявного наследования)Множественного наследования темы не поддерживают. Вероятно, из-за конфликтов одноименных атрибутов.
Стили каждого компонента дизайн-системы мы решили размещать в файлах
attrs_component_name.xml
(см. attrs_header1
, attrs_button
и т.д.)Стилизация компонентов дизайн системы. Архитектура компонентов. Поддержка нескольких тем
Стандартный конструктор view
Стандартный конструктор view предоставляет обширные средства для настройки элемента. Внешний вид элементов можно изменить через .xml-атрибуты или через определение стиля по умолчанию в стандартном конcтрукторе view.
Рассмотрим стандартный конструктор view на примере
H1Component
(задаёт крупный текст в шапке экранов):class H1Component @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.cm_header1_style
) : AppCompatTextView(context, attrs, defStyleAttr)
Здесь
attrs
— атрибуты из определения .xml (в том числе кастомные атрибуты view). Они парсятся и применяются стандартным образом (см. ниже на примере FabComponent
).class FabButtonComponent @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageButton(context, attrs, defStyleAttr) {
init {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.FabButtonComponent
)
val icon = a.getDrawable(R.styleable.FabButtonComponent_cm_icon)
a.recycle()
// apply attrs here
}
}
defStyleAttr
— стиль view по умолчанию.context
— контекст view, при помощи которого она создана.ВАЖНО: чтобы view успешно переключала тему, необходимо чтобы она была создана при помощи контекста, унаследованного от
android.view.ContextThemeWrapper
(то есть контекст activity
подходит, а applicationContext
— не подходит (применится тема, которая подтянется из стиля, указанного в Manifest экрана).ВАЖНО: при такой реализации главный приоритет у атрибутов, объявленных в .xml. У стилей, описанных в теме, приоритет ниже.
Интеграция стиля в компоненты дизайн системы и его связь с темой
Для поддержки темы компонентами дизайн-системы мы определяем в компонентах
defStyleAttr
и переключаем его в соответствии с темой, в которой он определен.Реализация темы в приложении
Создаем две темы:
<style name="ThemeA">
<item name="primary_background">@color/red</item>
<item name="best_textview_style">@style/MyBestText.A</item>
</style>
<style name="ThemeB">
<item name="primary_background">@color/brown</item>
<item name="best_textview_style">@style/MyBestText.B</item>
</style>
Компоненты дизайн системы системы будут тянуть этот стиль в таком ключе:
class MyBestText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.best_textview_style
) : TextView(context, attrs, defStyleAttr)
Тут определены стили каждой темы для этого элемента:
<style name="MyBestText" parent="android:Widget.TextView">
<item name="android:textSize">28sp</item>
<item name="lineHeight">34sp</item>
<item name="fontFamily">@font/roboto_bold</item>
</style>
<style name="MyBestText.A">
<item name="android:textColor">@color/white</item>
</style>
<style name="MyBestText.B">
<item name="android:textColor">@color/black</item>
</style>
Применяем тему через стандартный механизм Android.
При создании Activity указываем нужную тему. Тогда
MyBestText
подтянет нужный стиль и окрасит свой текст в белый или черный в зависимости от темы (см. выше описание темы MyBestText
).private void setAppTheme(@NonNull Boolean isDarkModeEnabled) {
if (isDarkModeEnabled) {
setTheme(R.style.DesignSystemDark);
} else {
setTheme(R.style.DesignSystemLight);
}
Цвета из темы мы будем разрешать прямо из .xml и подтягивать из темы.
<SomethingView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primary_background">
ВАЖНО: начиная с Android 5.0 допускается отовсюду динамически разрешать
android:background=»?attr/primary_background»
(селекторы, shape, vector drawables и т.д.) В Android 4.4 есть ограничение на селекторы, при попытке динамически разрешить итоговый цвет из селекторов система упадёт. При всех достоинствах такой реализации компоненты дизайн-системы не могут в preview Android Studio полноценно работать со стилизованными темами (к элементам не будут применяться стили).
Пока тема официально не использована нашими экранами, а только подключается программно (то есть стили наших
activity
не подгружают явным образом тему из Manifest
), мы не можем комфортно работать с элементами, поддерживающими темы в preview (их даже не будет в списке).Тестирование компонентов дизайн-системы
Для тестирования и анализа степени покрытия приложения дизайнеры предложили разработать отладочную панель с настройками стилей компонентов, цветов и т.д.
Темы в Android являются неизменяемыми, но их всегда можно перезаписать полностью или частично через
Activity.setTheme
(@StyleRes final int resid
). Так можно в нужный момент получить любую комбинацию стилей и собрать свою собственную тему. Но все стили должны быть объявлены в .xml заранее.Программно изменять атрибут темы без отсылок к объявленным стилям, к сожалению, нельзя. По крайней мере, я не нашёл способа.
Если знаете, как подсунуть свой цвет в атрибут темы (не объявленный в ресурсах как
style
), то напишите мне. Тогда мы сможем прямо из коробки манипулировать цветами с бэка на уровне стилизации всего приложения!Делаем рабочее preview компонентов дизайн-системы в Android Studio
Темы экранов приложения должны наследоваться от темы дизайн-системы.
Preview компонентов в .xml
При некорректно установленной теме экрана компоненты дизайн-системы тоже не будут отображаться корректно (не применятся стили и цвета):
При установке темы, унаследованной от темы дизайн-системы, мы получим вот что:
Видно, как разрешились все атрибуты темы и правильно подтянулись стили компонента.
Проверка поведения компонентов в другой теме в Preview без пересборки приложения
Чтобы проверить отображение в другой теме достаточно переключить тему в Preview light/dark.
Если конкретные реализации темы завязаны на ресурсы values/values-night, то можно переключать из preview в dark mode. И всё будет работать из коробки без выставления
setTheme
в Activity
.Переключение тем в приложении
Переключение тем в приложении может быть завязано на системное переключение dark-mode. В таком случае темы должны быть определены в директориях values и values-night.
Если планируется три и более тем, то потребуется вручную разрешать, какую из тем поставить через
activity.setTheme()
.Результаты стилизации смотрим ниже:
|
|
А как же третья тема под AB-тестом?
Как ранее говорилось, в таком случае придется вручную выставлять
setTheme
для применения нужной темы.Итоги
- У нас есть надежный механизм динамической смены тем и подстройки стилей (как в отладочной панели).
- Мы можем создавать новые компоненты дизайн-системы, поддерживающие стилизацию, и внедрять их повсеместно.
Теперь мы можем как угодно стилизовать всё наше приложение и настроить дизайн-систему. Всё упирается лишь в нашу фантазию.
Ссылка на тестовый проект в Git с пошаговым руководством по интеграции тем в свой проект: https://github.com/Dragues/SampleThemeApplication/
agent10
Не очень понял в чём преимущество создания своих кастомных компонент над уже стандартными. В этом примере вы меняется дефолтный стиль.
Но разве мы не можем в темах и сейчас поменять стиль стандартных компонент без необходимости наследоваться? Всякие buttonStyle, materialButtonStyle, toolbarStyle и прочие аттрибуты..
anonymous Автор
Доброе утро! Ответил вам в отдельном треде ниже((