1. Avant de commencer
Dans cet atelier de programmation, vous apprendrez à harmoniser vos couleurs personnalisées avec celles générées par un thème dynamique.
Prérequis
Les développeurs doivent être
- Vous maîtrisez les concepts de base de la thématisation dans Android.
- Vous maîtrisez les vues de widget Android et leurs propriétés.
Points abordés
- Utiliser plusieurs méthodes d'harmonisation des couleurs dans votre application
- Fonctionnement de l'harmonisation et changement de couleur
Prérequis
- Un ordinateur sur lequel Android est installé si vous souhaitez suivre la procédure.
2. Présentation de l'application
Voyaĝi est une application de transports en commun qui utilise déjà un thème dynamique. Pour de nombreux réseaux de transports en commun, la couleur est un indicateur important des trains, des bus ou des tramways. Ces couleurs ne peuvent pas être remplacées par les couleurs dynamiques primaires, secondaires ou tertiaires disponibles. Nous allons concentrer notre travail sur la RecyclerView composée de cartes de transport colorées.
3. Générer un thème
Nous vous recommandons d'utiliser notre outil Material Theme Builder comme premier arrêt pour créer un thème Material3. Dans l'onglet "Personnalisé", vous pouvez désormais ajouter d'autres couleurs à votre thème. Sur la droite, les rôles et les palettes tonales de ces couleurs s'affichent.
Dans la section "Couleur étendue", vous pouvez supprimer ou renommer des couleurs.
Le menu Exporter affiche un certain nombre d'options d'exportation possibles. Au moment de la rédaction de ce document, la gestion spéciale des paramètres d'harmonisation de Material Theme Builder n'est disponible que dans les vues Android.
Comprendre les nouvelles valeurs d'exportation
Pour vous permettre d'utiliser ces couleurs et les rôles de couleur qui leur sont associés dans vos thèmes, que vous choisissiez d'harmoniser ou non ces couleurs, le téléchargement exporté inclut désormais un fichier attrs.xml contenant les noms des rôles de couleur pour chaque couleur personnalisée.
<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>
Dans thèmes.xml, nous avons généré les quatre rôles de couleur pour chaque couleur personnalisée (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>
). Les propriétés harmonize<name>
indiquent si le développeur a sélectionné l'option dans Material Theme Builder. La couleur du thème principal ne sera pas modifiée.
<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>
Dans le fichier colors.xml
, les couleurs source utilisées pour générer les rôles de couleur listés ci-dessus sont spécifiées, ainsi que des valeurs booléennes indiquant si la palette de couleur doit être décalée ou non.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Examen d'une couleur personnalisée
En zoomant dans le panneau latéral de Material Theme Builder, nous pouvons voir que l'ajout d'une couleur personnalisée fait apparaître un panneau avec les quatre rôles de couleur clés dans une palette claire et sombre.
Dans les affichages Android, nous exportons ces couleurs pour vous, mais en arrière-plan, elles peuvent être représentées par une instance de l'objet ColorRoles
.
La classe ColorRoles comporte quatre propriétés : accent
, onAccent
, accentContainer
et onAccentContainer
. Ces propriétés sont la représentation entière des quatre couleurs hexadécimales.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
Vous pouvez récupérer les quatre rôles de couleur clés à partir d'une couleur arbitraire au moment de l'exécution en utilisant getColorRoles
dans la classe MaterialColors appelée getColorRoles
, qui vous permet de créer cet ensemble de quatre rôles de couleur lors de l'exécution en fonction d'une couleur de graine spécifique.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
De même, les valeurs de sortie sont les valeurs de couleur réelles, et NON les pointeurs vers celles-ci.**
5. Qu'est-ce que l'harmonisation des couleurs ?
Le nouveau système de couleurs de Material est basé sur des algorithmes et génère des couleurs primaires, secondaires, tertiaires et neutres à partir d'une couleur de graine donnée. Lors de nos échanges avec nos partenaires internes et externes, l'un des principaux problèmes était de savoir comment adopter les couleurs dynamiques tout en gardant le contrôle sur certaines couleurs.
Ces couleurs ont souvent une signification ou un contexte spécifique dans l'application, qui serait perdue si elles étaient remplacées par une couleur aléatoire. Si elles sont laissées telles quelles, ces couleurs peuvent également sembler heurtantes ou déplacées.
La couleur dans Material You est décrite par la teinte, la chrominance et le ton. La teinte d'une couleur se rapporte à la perception qu'a une personne de la voir comme membre d'une plage de couleurs par rapport à une autre. Le ton décrit à quel point il est clair ou sombre, et la chroma est l'intensité de la couleur. La perception de la teinte peut être affectée par des facteurs culturels et linguistiques, comme le fait que l'absence d'un mot pour le bleu dans les cultures anciennes soit souvent mentionnée, ce qui signifie qu'il est plutôt considéré comme vert dans la même famille que le vert.
Une teinte particulière peut être considérée comme chaude ou froide selon son emplacement dans le spectre de teinte. Passer à une teinte rouge, orange ou jaune est généralement considéré comme l'augmentation de la chaleur et vers une teinte bleue, verte ou violette est considéré comme plus froid. Même dans les couleurs chaudes ou froides, vous aurez des tons chauds et froids. En dessous, le "plus chaud" le jaune a une teinte plus orange, tandis que le modèle "plus froid" le jaune est plus influencé par le vert.
L'algorithme d'harmonisation des couleurs examine la teinte de la couleur non modifiée et la couleur avec laquelle elle doit être harmonisée pour localiser une teinte harmonieuse, mais qui ne modifie pas ses qualités de couleur sous-jacentes. Dans le premier graphique, il y a des teintes vertes, jaunes et orange moins harmonieuses tracées sur un spectre. Dans le graphique suivant, le vert et l'orange ont été harmonisés avec la teinte jaune. Le nouveau vert est plus chaud et le nouvel orange est plus frais.
La teinte a changé sur l'orange et le vert, mais ils peuvent toujours être perçus comme orange et vert.
Si vous souhaitez en savoir plus sur certaines décisions, explorations et considérations de conception, mes collègues Ayan Daniels et Andrew Lu ont rédigé un article de blog un peu plus détaillé que cette section.
6. Harmoniser manuellement une couleur
Pour harmoniser un seul ton, il existe deux fonctions dans MaterialColors
, harmonize
et harmonizeWithPrimary
.
harmonizeWithPrimary
utilise Context
pour accéder au thème actuel, puis à sa couleur principale.
@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);
}
Pour récupérer l'ensemble des quatre tons, nous devons aller un peu plus loin.
Comme nous disposons déjà de la couleur source, nous devons procéder comme suit:
- déterminer si elle doit être harmonisée,
- de déterminer si nous sommes en mode sombre,
- renvoyer un objet
ColorRoles
harmonisé ou non harmonieux.
Déterminer s'il faut harmoniser
Dans le thème exporté depuis Material Theme Builder, nous avons inclus des attributs booléens en utilisant la nomenclature harmonize<Color>
. Vous trouverez ci-dessous une fonction pratique permettant d'accéder à cette valeur.
Si elle est trouvée, elle renvoie sa valeur. sinon il détermine qu'il ne devrait pas harmoniser la couleur.
// 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
}
Créer un objet ColorRoles
harmonisé
retrieveHarmonizedColorRoles
est une autre fonction pratique qui réunit toutes les étapes ci-dessus: récupérer la valeur de couleur d'une ressource nommée, tenter de résoudre un attribut booléen pour déterminer l'harmonisation, et renvoie un objet ColorRoles
dérivé de la couleur d'origine ou mélangée (compte clair ou sombre donné).
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. Ajout des cartes de transport...
Comme indiqué précédemment, nous allons utiliser un RecyclerView et un adaptateur pour remplir et colorer la collection de cartes de transport.
Stocker des données sur les transports en commun
Pour stocker les données textuelles et les informations de couleur des cartes de transport, nous utilisons une classe de données qui stocke le nom, la destination et l'ID de ressource de couleur.
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)
)
Nous utiliserons cette couleur pour générer les tons dont nous avons besoin en temps réel.
Vous pouvez harmoniser les valeurs au moment de l'exécution avec la fonction onBindViewHolder
suivante.
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. Harmonisation automatique des couleurs
Vous pouvez utiliser la gestion manuelle pour gérer l'harmonisation. HarmonizedColorOptions est une classe de compilateur qui vous permet de spécifier une grande partie de ce que nous avons fait jusqu'à présent manuellement.
Après avoir récupéré le contexte actuel afin d'avoir accès au schéma dynamique actuel, vous devez spécifier les couleurs de base que vous souhaitez harmoniser et créer un nouveau contexte basé sur cet objet HarmonizedColorOptions et le contexte activé DynamicColors.
Si vous ne souhaitez pas harmoniser une couleur, il vous suffit de ne pas l'inclure dans harmonizedOptions.
val newContext = DynamicColors.wrapContextIfAvailable(requireContext())
val harmonizedOptions = HarmonizedColorsOptions.Builder()
.setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
.build();
harmonizedContext =
HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)
La couleur de base harmonisée déjà gérée vous permet de mettre à jour votre onBindViewHolder pour appeler simplement MaterialColors.getColorRoles
et spécifier si les rôles renvoyés doivent être clairs ou sombres.
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. Harmoniser automatiquement les attributs de thème
Les méthodes présentées jusqu'ici reposent sur la récupération des rôles de couleur à partir d'une couleur individuelle. C'est idéal pour montrer qu'un ton réel est généré, mais qu'il n'est pas réaliste pour la plupart des applications existantes. Il est probable que vous ne dériviez pas une couleur directement, mais que vous utilisiez plutôt un attribut de thème existant.
Précédemment dans cet atelier de programmation, nous avons parlé de l'exportation d'attributs de thème.
<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>
Comme pour la première méthode automatique, nous pouvons fournir des valeurs à HarmonizedColorOptions et utiliser HarmonizedColors pour récupérer un contexte avec les couleurs harmonisées. Il existe une différence clé entre les deux méthodes. Nous devons également ajouter un thème en superposition contenant les champs à harmoniser.
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)
Votre adaptateur utilisera le contexte harmonisé. Les valeurs du thème en superposition doivent faire référence à la variante claire ou sombre non harmonisée.
<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>
Dans le fichier de mise en page XML, nous pouvons utiliser ces attributs harmonisés normalement.
<?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. Code source
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. Exemples d'interface utilisateur
Thématisation par défaut et couleurs personnalisées sans harmonisation
Couleurs personnalisées harmonisées
12. Résumé
Dans cet atelier de programmation, vous avez appris à:
- Principes de base de notre algorithme d'harmonisation des couleurs
- Générer des rôles de couleur à partir d'une couleur vue donnée
- Harmoniser de manière sélective une couleur dans une interface utilisateur
- Harmoniser un ensemble d'attributs dans un thème.
Si vous avez des questions, n'hésitez pas à nous contacter à tout moment à l'adresse @MaterialDesign sur Twitter.
Suivez-nous pour d'autres contenus et tutoriels de conception sur youtube.com/MaterialDesign.