Базовая гармонизация цветов в представлениях Android

1. Прежде чем начать

В этом практическом занятии вы узнаете, как согласовать ваши пользовательские цвета с цветами, генерируемыми динамической темой.

Предварительные требования

Разработчики должны быть

  • Знание основных концепций оформления тем в Android.
  • Уверенное владение виджетами Android и их свойствами.

Что вы узнаете

  • Как использовать цветовую гармонизацию в приложении с помощью различных методов.
  • Как работает гармонизация и как она меняет цвет.

Что вам понадобится

  • Для того чтобы следовать инструкциям, вам понадобится компьютер с установленной операционной системой Android.

2. Обзор приложения

Voyaĝi — это транспортное приложение, которое уже использует динамическую тему оформления. Для многих транспортных систем цвет является важным индикатором поездов, автобусов или трамваев, и его нельзя заменить любыми доступными динамическими основными, второстепенными или третичными цветами. Мы сосредоточим нашу работу на RecyclerView для цветных транспортных карт.

62ff4b2fb6c9e14a.png

3. Формирование темы

Мы рекомендуем использовать наш инструмент Material Theme Builder в качестве первого шага для создания темы Material3. На вкладке «Настройка» теперь можно добавить больше цветов в вашу тему. Справа будут показаны цветовые роли и тональные палитры для этих цветов.

В расширенном разделе «Цвета» вы можете удалять или переименовывать цвета.

20cc2cf72efef213.png

В меню экспорта отобразится ряд возможных вариантов экспорта. На момент написания статьи специальная обработка настроек гармонизации в Material Theme Builder доступна только в Android Views.

6c962ad528c09b4.png

Понимание новых экспортных показателей

Чтобы вы могли использовать эти цвета и связанные с ними цветовые роли в своих темах независимо от того, решите ли вы их гармонизировать, в экспортируемый файл теперь включен файл attrs.xml, содержащий названия цветовых ролей для каждого пользовательского цвета.

<resources>
   <attr name="colorCustom1" format="color" />
   <attr name="colorOnCustom1" format="color" />
   <attr name="colorCustom1Container" format="color" />
   <attr name="colorOnCustom1Container" format="color" />
   <attr name="harmonizeCustom1" format="boolean" />

   <attr name="colorCustom2" format="color" />
   <attr name="colorOnCustom2" format="color" />
   <attr name="colorCustom2Container" format="color" />
   <attr name="colorOnCustom2Container" format="color" />
   <attr name="harmonizeCustom2" format="boolean" />
</resources>

В файле themes.xml мы сгенерировали четыре роли цвета для каждого пользовательского цвета ( color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer> ). Свойства harmonize<name> отражают, выбрал ли разработчик эту опцию в Material Theme Builder. Это не повлияет на цвет в основной теме.

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

В файле colors.xml указываются исходные цвета, используемые для генерации перечисленных выше цветовых ролей, а также логические значения, определяющие, будет ли смещаться цветовая палитра или нет.

<resources>
   <!-- other colors used in theme -->

   <color name="custom1">#1AC9E0</color>
   <color name="custom2">#32D312</color>
</resources>

4. Изучение пользовательских цветовых решений

Приблизив изображение боковой панели Material Theme Builder, мы видим, что добавление пользовательского цвета отображает панель с четырьмя основными цветовыми решениями в светлой и темной палитре.

c6ee942b2b93cd92.png

В Android Views мы экспортируем эти цвета, но на практике они могут быть представлены экземпляром объекта ColorRoles .

Класс ColorRoles имеет четыре свойства: accent , onAccent , accentContainer и onAccentContainer . Эти свойства представляют собой целочисленное представление четырех шестнадцатеричных цветов.

public final class ColorRoles {

  private final int accent;
  private final int onAccent;
  private final int accentContainer;
  private final int onAccentContainer;

  // truncated code

}

Вы можете получить четыре ключевые роли цвета для произвольного цвета во время выполнения, используя метод getColorRoles в классе MaterialColors getColorRoles Этот метод позволяет создать набор из четырех ролей цвета во время выполнения, задав определенный начальный цвет.

public static ColorRoles getColorRoles(
    @NonNull Context context,
    @ColorInt int color
) { /* implementation */ }

Аналогично, выходные значения представляют собой фактические значения цвета, а НЕ указатели на них.**

5. Что такое цветовая гармонизация?

Новая цветовая система Material по своей сути алгоритмическая, генерирующая основные, второстепенные, третичные и нейтральные цвета из заданного исходного цвета. Один из вопросов, который мы часто задавали внутренним и внешним партнерам, касался того, как реализовать динамическое изменение цвета, сохраняя при этом контроль над некоторыми цветами.

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

Цвет в вашем материальном мире описывается оттенками, насыщенностью и тоном. Оттенок цвета связан с восприятием его человеком как принадлежащего к одному цветовому диапазону или к другому. Тон описывает, насколько светлым или темным он кажется, а насыщенность — интенсивность цвета. На восприятие оттенка могут влиять культурные и лингвистические факторы, например, часто упоминаемое отсутствие слова для обозначения синего цвета в древних культурах, где он вместо этого рассматривался как цвет, относящийся к той же цветовой гамме, что и зеленый.

57c46d9974c52e4a.png Определенный оттенок может считаться теплым или холодным в зависимости от его положения в цветовом спектре. Переход к красному, оранжевому или желтому оттенку обычно считается более теплым, а переход к синему, зеленому или фиолетовому — более холодным. Даже внутри теплых и холодных цветов будут присутствовать теплые и холодные тона. Ниже показан «теплый» желтый цвет с более выраженным оранжевым оттенком, тогда как «холодный» желтый цвет в большей степени находится под влиянием зеленого. 597c6428ff6b9669.png

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

Оттенок оранжевого и зеленого изменился, но они по-прежнему воспринимаются как оранжевые и зеленые.

766516c321348a7c.png

Если вы хотите узнать больше о некоторых проектных решениях, исследованиях и соображениях, мои коллеги Аян Дэниелс и Эндрю Лу написали статью в блоге, в которой более подробно рассматриваются эти вопросы, чем в этом разделе.

6. Ручная гармонизация цвета.

Для гармонизации одного тона в MaterialColors есть две функции: harmonize и harmonizeWithPrimary .

harmonizeWithPrimary использует Context как средство доступа к текущей теме и, соответственно, к основному цвету из неё.

@ColorInt
public static int harmonizeWithPrimary(@NonNull Context context, @ColorInt int colorToHarmonize) {
    return harmonize(
        colorToHarmonize,
        getColor(context, R.attr.colorPrimary, MaterialColors.class.getCanonicalName()));
  }


@ColorInt
  public static int harmonize(@ColorInt int colorToHarmonize, @ColorInt int colorToHarmonizeWith) {
    return Blend.harmonize(colorToHarmonize, colorToHarmonizeWith);
  }

Чтобы восстановить набор из четырех тонов, нам нужно сделать еще кое-что.

Given we already have the source color, we need to:

  1. определить, следует ли его гармонизировать.
  2. определить, находимся ли мы в темном режиме, и,
  3. Возвращает либо согласованный, либо несогласованный объект ColorRoles .

Определение целесообразности гармонизации

В экспортированной теме из Material Theme Builder мы добавили логические атрибуты, используя номенклатуру harmonize<Color> . Ниже приведена удобная функция для доступа к этому значению.

Если значение найдено, возвращается его значение; в противном случае определяется, что гармонизация цвета не требуется.

// Looks for associated harmonization attribute based on the color id
// custom1 ===> harmonizeCustom1
fun shouldHarmonize(context: Context, colorId: Int): Boolean {
   val root = context.resources.getResourceEntryName(colorId)
   val harmonizedId = "harmonize" + root.replaceFirstChar { it.uppercaseChar() }
   
   val identifier = context.resources.getIdentifier(
           harmonizedId, "bool", context.packageName)
   
   return if (identifier != 0) context.resources.getBoolean(identifier) else false
}

Создание объекта Harmonized ColorRoles

Функция retrieveHarmonizedColorRoles — это еще одна вспомогательная функция, объединяющая все вышеупомянутые шаги: получение значения цвета для именованного ресурса, попытка определить логический атрибут для определения гармонизации и возврат объекта ColorRoles производного от исходного или смешанного цвета (с учетом светлой или темной схемы).

fun retrieveHarmonizedColorRoles(
   view: View,
   customId: Int,
   isLight: Boolean
): ColorRoles {
   val context = view.context
   val custom = context.getColor(customId);
  
   val shouldHarmonize = shouldHarmonize(context, customId)
   if (shouldHarmonize) {
       val blended = MaterialColors.harmonizeWithPrimary(context, custom)
       return MaterialColors.getColorRoles(blended, isLight)
   } else return MaterialColors.getColorRoles(custom, isLight)
}

7. Заполнение проездных карт

Как уже упоминалось, для заполнения и раскрашивания коллекции проездных карт мы будем использовать RecyclerView и адаптер.

e4555089b065b5a7.png

Хранение данных о передаче

Для хранения текстовых данных и информации о цвете проездных карт мы используем класс данных, в котором хранятся имя, пункт назначения и идентификатор ресурса цвета.

data class TransitInfo(val name: String, val destination: String, val colorId: Int)

/*  truncated code */

val transitItems = listOf(
   TransitInfo("53", "Irvine", R.color.custom1),
   TransitInfo("153", "Brea", R.color.custom1),
   TransitInfo("Orange County Line", "Oceanside", R.color.custom2),
   TransitInfo("Pacific Surfliner", "San Diego", R.color.custom2)
)

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

Согласование можно выполнить во время выполнения с помощью следующей функции onBindViewHolder .

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   val transitInfo = list.get(position)
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = retrieveHarmonizedColorRoles(
           holder.itemView, color,
           !isNightMode(holder.itemView.context)
       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

8. Автоматическая гармонизация цветов.

В качестве альтернативы ручной настройке цветовой гармонизации вы можете поручить это сделать. Класс HarmonizedColorOptions — это класс-конструктор, который позволяет задавать большую часть того, что мы до сих пор делали вручную.

После получения текущего контекста, обеспечивающего доступ к текущей динамической схеме, необходимо указать базовые цвета, которые вы хотите гармонизировать, и создать новый контекст на основе объекта HarmonizedColorOptions и контекста с включенной функцией DynamicColors.

Если вы не хотите гармонизировать цвет, просто не включайте его в параметр harmonizedOptions.

val newContext = DynamicColors.wrapContextIfAvailable(requireContext())


val harmonizedOptions = HarmonizedColorsOptions.Builder()
 .setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
 .build();

harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

Поскольку согласованный базовый цвет уже обработан, вы можете обновить ваш метод onBindViewHolder, чтобы он просто вызывал MaterialColors.getColorRoles и указывал, должны ли возвращаемые роли быть светлыми или темными.

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   /*...*/
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = MaterialColors.getColorRoles(context.getColor(color), !isNightMode(context))

       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

9. Автоматическая гармонизация атрибутов темы.

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

Ранее в этом практическом занятии мы говорили об экспорте атрибутов темы.

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

Аналогично первому автоматическому методу, мы можем передать значения в HarmonizedColorOptions и использовать HarmonizedColors для получения Context с согласованными цветами. Однако между этими двумя методами есть одно ключевое различие. Нам также необходимо предоставить наложение темы, содержащее поля, которые необходимо согласовать.

val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

// Harmonizing individual attributes
val harmonizedColorAttributes = HarmonizedColorAttributes.create(
 intArrayOf(
   R.attr.colorCustom1,
   R.attr.colorOnCustom1,
   R.attr.colorCustom1Container,
   R.attr.colorOnCustom1Container,
   R.attr.colorCustom2,
   R.attr.colorOnCustom2,
   R.attr.colorCustom2Container,
   R.attr.colorOnCustom2Container
 ), R.style.AppTheme_Overlay
)
val harmonizedOptions =
 HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

val harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

Ваш адаптер будет использовать согласованный контекст. Значения в наложении темы должны относиться к несогласованному светлому или темному варианту.

<style name="AppTheme.Overlay" parent="AppTheme">
   <item name="colorCustom1">@color/harmonized_colorCustom1</item>
   <item name="colorOnCustom1">@color/harmonized_colorOnCustom1</item>
   <item name="colorCustom1Container">@color/harmonized_colorCustom1Container</item>
   <item name="colorOnCustom1Container">@color/harmonized_colorOnCustom1Container</item>

   <item name="colorCustom2">@color/harmonized_colorCustom2</item>
   <item name="colorOnCustom2">@color/harmonized_colorOnCustom2</item>
   <item name="colorCustom2Container">@color/harmonized_colorCustom2Container</item>
   <item name="colorOnCustom2Container">@color/harmonized_colorOnCustom2Container</item>
</style>

Внутри XML-файла разметки мы можем использовать эти согласованные атрибуты как обычно.

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   style="?attr/materialCardViewFilledStyle"
   android:id="@+id/card"
   android:layout_width="80dp"
   android:layout_height="100dp"
   android:layout_marginStart="8dp"
   app:cardBackgroundColor="?attr/colorCustom1Container"
   >

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_margin="8dp">

       <TextView
           android:id="@+id/transitName"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:textSize="28sp"
           android:textStyle="bold"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintTop_toTopOf="parent" />

       <TextView
           android:id="@+id/transitDestination"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginBottom="4dp"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintBottom_toBottomOf="parent" />
   </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

10. Исходный код

package com.example.voyagi.harmonization.ui.dashboard

import android.content.Context
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.voyagi.harmonization.R
import com.example.voyagi.harmonization.databinding.FragmentDashboardBinding
import com.example.voyagi.harmonization.ui.home.TransitCardAdapter
import com.example.voyagi.harmonization.ui.home.TransitInfo
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.ColorRoles
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.HarmonizedColorAttributes
import com.google.android.material.color.HarmonizedColors
import com.google.android.material.color.HarmonizedColorsOptions
import com.google.android.material.color.MaterialColors


class DashboardFragment : Fragment() {

 enum class TransitMode { BUS, TRAIN }
 data class TransitInfo2(val name: String, val destination: String, val mode: TransitMode)

 private lateinit var dashboardViewModel: DashboardViewModel
 private var _binding: FragmentDashboardBinding? = null

 // This property is only valid between onCreateView and
 // onDestroyView.
 private val binding get() = _binding!!

 override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
 ): View? {
   dashboardViewModel =
     ViewModelProvider(this).get(DashboardViewModel::class.java)

   _binding = FragmentDashboardBinding.inflate(inflater, container, false)
   val root: View = binding.root


   val recyclerView = binding.recyclerView

   val transitItems = listOf(
     TransitInfo2("53", "Irvine", TransitMode.BUS),
     TransitInfo2("153", "Brea", TransitMode.BUS),
     TransitInfo2("Orange County Line", "Oceanside", TransitMode.TRAIN),
     TransitInfo2("Pacific Surfliner", "San Diego", TransitMode.TRAIN)
   )
  
   val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

   // Harmonizing individual attributes
   val harmonizedColorAttributes = HarmonizedColorAttributes.create(
     intArrayOf(
       R.attr.colorCustom1,
       R.attr.colorOnCustom1,
       R.attr.colorCustom1Container,
       R.attr.colorOnCustom1Container,
       R.attr.colorCustom2,
       R.attr.colorOnCustom2,
       R.attr.colorCustom2Container,
       R.attr.colorOnCustom2Container
     ), R.style.AppTheme_Overlay
   )
   val harmonizedOptions =
     HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

   val harmonizedContext =
     HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)


   val adapter = TransitCardAdapterAttr(transitItems, harmonizedContext)
   recyclerView.adapter = adapter
   recyclerView.layoutManager =
     LinearLayoutManager(harmonizedContext, RecyclerView.HORIZONTAL, false)

   return root
 }

 override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
 }
}

class TransitCardAdapterAttr(val list: List<DashboardFragment.TransitInfo2>, context: Context) :
 RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 val colorRolesMap = mutableMapOf<Int, ColorRoles>()
 private var harmonizedContext: Context? = context

 override fun onCreateViewHolder(
   parent: ViewGroup,
   viewType: Int
 ): RecyclerView.ViewHolder {
   return if (viewType == DashboardFragment.TransitMode.BUS.ordinal) {
     BusViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_bus, parent, false))
   } else TrainViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_train, parent, false))
 }

 override fun getItemCount(): Int {
   return list.size
 }

 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   val item = list[position]
   if (item.mode.ordinal == DashboardFragment.TransitMode.BUS.ordinal) {
     (holder as BusViewHolder).bind(item)
     (holder as TransitBindable).adjustNameLength()
   } else {
       (holder as TrainViewHolder).bind(item)
       (holder as TransitBindable).adjustNameLength()
   }
 }

 override fun getItemViewType(position: Int): Int {
   return list[position].mode.ordinal
 }

 interface TransitBindable {
   val card: MaterialCardView
   var transitName: TextView
   var transitDestination: TextView

   fun bind(item: DashboardFragment.TransitInfo2) {
     transitName.text = item.name
     transitDestination.text = item.destination
   }
   fun Float.toDp(context: Context) =
     TypedValue.applyDimension(
       TypedValue.COMPLEX_UNIT_DIP,
       this,
       context.resources.displayMetrics
     )
   fun adjustNameLength(){
     if (transitName.length() > 4) {
       val layoutParams = card.layoutParams
       layoutParams.width = 100f.toDp(card.context).toInt()
       card.layoutParams = layoutParams
       transitName.setTypeface(Typeface.DEFAULT_BOLD);

       transitName.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f)
     }
   }
 }

 inner class BusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
 inner class TrainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
}

11. Примеры пользовательских интерфейсов

Стандартная тема оформления и пользовательские цвета без гармонизации.

a5a02a72aef30529.png

Гармонизированные пользовательские цвета

4ac88011173d6753.pngd5084780d2c6b886.png

dd0c8b90eccd8bef.pngc51f8a677b22cd54.png

12. Резюме

В этом практическом занятии вы узнали:

  • Основы нашего алгоритма цветовой гармонизации
  • Как создать цветовые роли на основе заданного увиденного цвета.
  • Как выборочно гармонизировать цвет в пользовательском интерфейсе.
  • Как согласовать набор атрибутов в теме оформления.

Если у вас возникнут вопросы, не стесняйтесь задавать их нам в любое время, используя @MaterialDesign в Твиттере .

Следите за обновлениями и новыми обучающими материалами по дизайну на youtube.com/MaterialDesign