Разработка
Настраиваем интерфейс Android-приложения с помощью тем
В этой статье мы рассмотрим различные возможности, которые есть у разработчиков для улучшения пользовательского опыта с помощью тем.
Поддерживать свежесть и привлекательность приложения очень важно, чтобы обеспечить пользователям приятный опыт. Одним из эффективных методов является придание индивидуальности приложению с помощью поддержки различных тем.
В этой статье мы рассмотрим различные возможности, которые есть у разработчиков для улучшения пользовательского опыта с помощью тем. Мы также поделимся опытом нашей компании в создании тем и расскажем о технических трудностях, с которыми мы столкнулись при их реализации для Android.
Давайте приступим!
Темы в приложении
В зависимости от целей вашего приложения и компании у вас есть несколько вариантов, которые следует рассмотреть при внедрении тематического оформления в Android-приложение. Вот несколько из них:
- Переключение между светлым и темным режимом: Это стало практически необходимостью на мобильных платформах, поскольку пользователи предпочитают темный режим.
- Реализация динамических цветов. В Android 12 появилась функция Dynamic Color, позволяющая адаптировать цветовую схему приложения к обоям пользователя.
- Пойти дальше, как это было сделано в Revolut. Мы предлагаем все вышеперечисленные варианты, а также предоставляем нашим клиентам возможность выбирать цветовые темы в приложении. Это позволяет им выбрать любимый цвет из заданной палитры, и внешний вид приложения меняется в соответствии с ним.
Используя эти возможности, вы можете создать приложение, внешний вид которого будет соответствовать бренду и целям вашей компании.
В этой статье мы сделаем простое приложение, использующее все перечисленные возможности настройки:
Переключение между светлым и темным режимом
Поскольку на многих платформах теперь доступен системный темный режим, реализация темной темы в приложении стала практически отраслевым стандартом. Хотя существует множество советов по реализации темного режима, мы не будем вдаваться в технические подробности.
Тем не менее, если вам интересно узнать больше о реализации темной темы в Android, вы можете обратиться к официальному руководству.
В нашем демонстрационном приложении мы поддерживаем темный и светлый режимы, определив два стиля:
Светлый:
<!-- res/values/themes.xml --> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">@color/blue</item> <item name="colorOnPrimary">@color/white</item> <item name="colorSecondary">?attr/colorPrimary</item> <item name="colorOnSecondary">?attr/colorOnPrimary</item> <item name="android:statusBarColor">?attr/colorPrimary</item> </style>
Темный:
<!-- res/values-night/themes.xml --> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">@color/blue_dark</item> <item name="colorOnPrimary">@color/black</item> <item name="colorSecondary">?attr/colorPrimary</item> <item name="colorOnSecondary">?attr/colorOnPrimary</item> <item name="android:statusBarColor">?attr/colorPrimary</item> </style>
Поскольку мы помещаем темы в отдельные папки (values и values-night), система будет подбирать нужную тему автоматически, в зависимости от настроек пользователя.
Оба стиля наследуют Theme.MaterialComponents.DayNight.NoActionBar, который обеспечивает поддержку светлого и темного режимов для основных элементов пользовательского интерфейса, таких как фон и текст. Кроме того, мы определяем собственные colorPrimary и colorSecondary, чтобы придать нашему приложению уникальный вид.
Для переключения между светлым и темным режимами во время выполнения мы используем AppCompatDelegate.setDefaultNightMode:
nightModeRadioButton.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } }
Вот пример того, как выглядит приложение с поддержкой темного и светлого режимов:
Если вам интересно посмотреть на полноэкранный макет и код, вы можете ознакомиться с демонстрационным проектом на GitHub.
Использование динамических цветов
В Android 12 появились расширенные возможности персонализации. Теперь пользователи могут выбрать обои, и система будет автоматически подбирать цвета в соответствии с ними. Кроме того, приложения могут поддерживать цвета обоев с помощью DynamicColors API.
Самый простой способ включить динамические цвета в приложении для Android — использовать тему Theme.Material3.DynamicColors.DayNight. Однако ее использование требует перехода на тему Material3, что не всегда желательно, поскольку она не только меняет цвета, но и изменяет внешний вид компонентов в соответствии с рекомендациями Material3.
Если мы хотим поддерживать динамические цвета без изменения темы приложения, мы можем указать для нашей темы dynamicColorThemeOverlay:
Светлый стиль темы
<!-- res/values/themes.xml --> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Other colors --> <!-- ... --> <!-- Dynamic colors overlay --> <item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.Light</item> </style>
Темный стиль темы
<!-- res/values-night/themes.xml --> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Other colors --> <!-- ... --> <!-- Dynamic colors overlay --> <item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.Dark</item> </style>
Чтобы применить стилизацию, нам нужно вызвать DynamicColors API в onCreate приложения:
class App : Application() { override fun onCreate() { super.onCreate() DynamicColors.applyToActivitiesIfAvailable(this) } }
И вот как выглядит приложение с включенными динамическими цветами:
Изменение цветов во время выполнения программы
При описанном выше подходе приложение всегда будет применять динамические цвета, если они доступны.
Но что, если мы хотим иметь более гибкий способ работы с темами? Некоторые приложения могут поддерживать свой брендинг по умолчанию и не полагаться полностью на динамические цвета, предоставляя пользователям возможность выбрать нужную им тему.
В Revolut мы предоставляем нашим клиентам возможность в любой момент изменить тему приложения:
Однако такой нестандартный подход может оказаться довольно сложным в реализации. После того как клиент выбрал тему, нам необходимо указать приложению, что надо пересоздать все представления, чтобы они применили новые цвета. Фреймворк Android делает это за нас, когда нам нужно сменить светлую тему на темную, используя AppCompatDelegate.setDefaultNightMode. Этот метод перестраивает активити “под капотом”.
Но нет способа принудительно изменить цвета, чтобы поддержать переключение на динамические цвета в рантайме. Хорошая новость заключается в том, что никто не мешает нам проверить, как работает AppCompatDelegate.setDefaultNightMode, и воспроизвести его подход:
//from AppCompatDelegate.applyDayNightToActiveDelegates for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) { final AppCompatDelegate delegate = activeDelegate.get(); if (delegate != null) { delegate.applyDayNight(); //Calls recreate under the hood } }
Да, этот метод просто перебирает все активити и вызывает applyDayNight, заставляя их пересоздаваться! sActivityDelegates — это статический набор, в котором собраны все ссылки на Activity.
Упомянутый API является приватным, поэтому для реализации нашего решения мы будем черпать вдохновение в подходе Android:
object ColorThemesController { private val activities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>()) private var initialized = false private var colorTheme = ColorTheme.DEFAULT fun initialize(application: Application) { if (!initialized) { initialized = true //Registering the callback allows us to listen and react to the lifecycle of every app activity. application.registerActivityLifecycleCallbacks(ActivityCallbacks()) } } fun applyColorTheme(colorTheme: ColorTheme) { this.colorTheme = colorTheme activities.forEach(ActivityCompat::recreate) } private class ActivityCallbacks : ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { when (colorTheme) { ColorTheme.DYNAMIC -> DynamicColors.applyToActivityIfAvailable(activity) ColorTheme.DEFAULT -> { //do nothing, we'll use the activity theme } } activities.add(activity) } override fun onActivityDestroyed(activity: Activity) { activities.remove(activity) } //Other overriden methods } }
ColorThemesController регистрирует ActivityLifecycleCallbacks для сбора всех активити в один набор. Метод applyColorTheme сохраняет выбранную тему в статическом поле и запускает восстановление для всех активити.
Самое интересное происходит в колбеке onActivityCreated. Он применяет выбранную тему к только что созданной активити. DynamicColors.applyToActivityIfAvailable(activity) устанавливает динамические цвета. А для темы по умолчанию мы ничего не будем делать, чтобы сохранить тему активити по умолчанию.
Логика начинает работать после инициализации ColorThemesController в классе App:
class App : Application() { override fun onCreate() { super.onCreate() ColorThemesController.initialize(this) } }
Теперь мы можем вызывать ColorThemesController.applyColorTheme() для внесения изменений во время выполнения программы:
private fun observeColorThemeSelection() { val onCheckedChanged = OnCheckedChangeListener { view, isChecked -> if (!isChecked) return@OnCheckedChangeListener val colorTheme = when (view) { dynamicColorThemeButton -> ColorTheme.DYNAMIC else -> ColorTheme.DEFAULT } ColorThemesController.applyColorTheme(colorTheme) } dynamicColorThemeButton.setOnCheckedChangeListener(onCheckedChanged) defaultColorThemeButton.setOnCheckedChangeListener(onCheckedChanged) }
Давайте проверим, как это работает в действии:
Полный код приложения можно найти в репозитории GitHub.
Пользовательские темы
Динамические цвета доступны только на Android, что приводит к различиям в возможностях персонализации на разных платформах, если ваше приложение распространяется и для iOS.
По этим причинам в Revolut мы не ограничились поддержкой динамических цветов. Мы также предлагаем пользовательские цветовые темы, позволяющие пользователям выбирать основной цвет приложения:
Довольно просто модифицировать наш пример приложения для поддержки пользовательской темы.
Давайте проверим, как мы можем привнести в приложение пользовательскую оранжевую тему.
Первым шагом будет написание темы-оверлея, которую мы применим поверх темы по умолчанию. Подробнее о том, как работают оверлеи тем, вы можете прочитать в этой статье. Если коротко, то наложение темы определяет цвета, которые нужно заменить в данной теме. Оно добавляет новые цвета в тему, если они там отсутствуют. Это похоже на добавление ключей в HashMap.
Вот наложение темы для светлого режима, которое заменяет colorPrimary нашей базовой темы:
<!-- res/values/themes.xml --> <style name="ColorThemeOverlay.Orange"> <item name="colorPrimary">@color/orange</item> </style>
И для темного режима:
<!-- res/values-night/themes.xml --> <style name="ColorThemeOverlay.Orange"> <item name="colorPrimary">@color/orange_dark</item> </style>
Теперь модифицируем наш ColorThemesController для поддержки новой темы:
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { when (colorTheme) { ColorTheme.DYNAMIC -> DynamicColors.applyToActivityIfAvailable(activity) ColorTheme.ORANGE -> { activity.theme.applyStyle(R.style.ColorThemeOverlay_Orange, true) } ColorTheme.DEFAULT -> { //do nothing, we'll use the activity theme } } activities.add(activity) }
Тема Orange применяется к активити с помощью activity.theme.applyStyle(R.style.ColorThemeOverlay_Orange, true) . Этот вызов устанавливает оверлей поверх текущей темы акитиви, как мы и хотели.
Последним штрихом будет вызов ColorThemesController.applyColorTheme при выборе оранжевой темы:
OnCheckedChangeListener { view, isChecked -> if (!isChecked) return@OnCheckedChangeListener val colorTheme = when (view) { dynamicColorThemeButton -> ColorTheme.DYNAMIC orangeColorThemeButton -> ColorTheme.ORANGE else -> ColorTheme.DEFAULT } ColorThemesController.applyColorTheme(colorTheme) }
Теперь наше демонстрационное приложение готово к переключению между стандартной, динамической и пользовательской темой Orange, максимально расширяя возможности персонализации для клиентов.
Заключение
В этой статье мы рассмотрели различные способы персонализации внешнего вида приложения. Поддержка темного режима позволяет удовлетворить предпочтения многих пользователей, а использование динамических цветов обеспечивает синхронизацию приложения с системой. Пользовательские оверлеи обеспечивают кроссплатформенные стили, доступные каждому.
Каждое приложение уникально, и у каждого приложения есть свои уникальные потребности в оформлении. Правильно подобранные инструменты и приемы позволят вам создать впечатление, которое будет соответствовать целям вашего приложения и радовать пользователей своим внешним видом. Применяя рассмотренные в этой статье подходы, вы сможете сделать стиль своего приложения более адаптивным и удобным для пользователей.