
Привет читателям!
В какой-то момент любое крупное приложение разрастается так, что сложно везде поддерживать однотипный дизайн и динамично реагировать на любые изменения и тенденции в дизайне и 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 as TextView).setTextColor( if (darkMode) R.color.blue else R.color.black ) } else if (view is TabLayout) { (view as TabLayout).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/
ссылка на оригинал статьи https://habr.com/ru/company/citymobil/blog/507896/









Добавить комментарий