1. Avant de commencer
Dans cet atelier de programmation, vous allez apprendre à harmoniser vos couleurs personnalisées avec celles générées par un thème dynamique.
Prérequis
Les développeurs doivent être
- Vous connaissez les concepts de base des thèmes dans Android.
- Vous êtes à l'aise avec les vues de widget Android et leurs propriétés.
Points abordés
- Utiliser l'harmonisation des couleurs dans votre application à l'aide de plusieurs méthodes
- Fonctionnement de l'harmonisation et modification des couleurs
Prérequis
- Un ordinateur sur lequel Android est installé, si vous souhaitez suivre le tutoriel.
2. Présentation de l'application
Voyaĝi est une application de transport en commun qui utilise déjà un thème dynamique. Pour de nombreux systèmes de transport en commun, la couleur est un indicateur important pour les trains, les bus ou les tramways. Elle ne peut pas être remplacée par les couleurs primaires, secondaires ou tertiaires dynamiques disponibles. Nous allons concentrer nos efforts sur le RecyclerView des cartes de transport colorées.

3. Générer un thème
Nous vous recommandons d'utiliser notre outil Material Theme Builder 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, vous verrez les rôles de couleur et les palettes tonales pour ces couleurs.
Dans la section "Couleurs étendues", vous pouvez supprimer ou renommer des couleurs.

Le menu d'exportation affiche plusieurs options d'exportation possibles. Au moment de la rédaction de cet article, 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 leurs rôles de couleur associés dans vos thèmes, que vous choisissiez ou non d'harmoniser, 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 themes.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 de départ 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 couleurs sera modifiée ou non.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Examiner une couleur personnalisée
En zoomant sur le panneau latéral de Material Theme Builder, nous pouvons voir qu'en ajoutant une couleur personnalisée, un panneau s'affiche avec les quatre rôles de couleur clés dans une palette claire et sombre.

Dans Android Views, nous exportons ces couleurs pour vous, mais en coulisses, 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 à l'aide de getColorRoles dans la classe MaterialColors appelée getColorRoles, qui vous permet de créer cet ensemble de quatre rôles de couleur au moment de l'exécution à partir d'une couleur de base 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 des pointeurs vers celles-ci.**
5. Qu'est-ce que l'harmonisation des couleurs ?
Le nouveau système de couleurs de Material est algorithmique par conception. Il génère des couleurs primaires, secondaires, tertiaires et neutres à partir d'une couleur de base donnée. Un point d'inquiétude qui nous a été souvent soulevé lors de nos discussions avec des partenaires internes et externes concernait la façon d'adopter les couleurs dynamiques tout en gardant le contrôle sur certaines couleurs.
Ces couleurs ont souvent une signification ou un contexte spécifiques dans l'application, qui seraient perdus si elles étaient remplacées par une couleur aléatoire. Sinon, ces couleurs peuvent sembler visuellement choquantes ou déplacées.
Dans Material You, la couleur est décrite par la teinte, la chroma et le ton. La teinte d'une couleur est liée à la façon dont elle est perçue comme appartenant à une gamme de couleurs plutôt qu'à une autre. Le ton décrit la luminosité ou l'obscurité de la couleur, et la chroma correspond à son intensité. La perception de la teinte peut être affectée par des facteurs culturels et linguistiques, comme le manque de mot pour désigner le bleu dans les cultures anciennes, qui était alors considéré comme faisant partie de la même famille que le vert.
Une teinte particulière peut être considérée comme chaude ou froide selon sa position sur le spectre des teintes. En général, une teinte rouge, orange ou jaune est considérée comme chaude, tandis qu'une teinte bleue, verte ou violette est considérée comme froide. Même au sein des couleurs chaudes ou froides, vous trouverez des tons chauds et froids. Ci-dessous, le jaune "chaud" est plus orangé, tandis que le jaune "froid" est plus influencé par le vert. 
L'algorithme d'harmonisation des couleurs examine la teinte de la couleur non modifiée et celle avec laquelle elle doit être harmonisée pour trouver une teinte harmonieuse qui ne modifie pas ses qualités de couleur sous-jacentes. Dans le premier graphique, des teintes vertes, jaunes et orange moins harmonieuses sont représentées sur un spectre. Dans l'image suivante, les couleurs verte et orange ont été harmonisées avec la teinte jaune. Le nouveau vert est plus chaud et le nouvel orange est plus froid.
La teinte de l'orange et du vert a changé, mais elles peuvent toujours être perçues comme de l'orange et du vert.

Si vous souhaitez en savoir plus sur certaines décisions de conception, explorations et considérations, mes collègues Ayan Daniels et Andrew Lu ont rédigé un article de blog plus détaillé que cette section.
6. Harmoniser une couleur manuellement
Pour harmoniser une seule tonalité, 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 faire un peu plus.
Étant donné que nous avons déjà la couleur source, nous devons :
- déterminer s'il doit être harmonisé ;
- déterminer si nous sommes en mode sombre ;
- renvoie un objet
ColorRolesharmonisé ou non harmonisé.
Déterminer si vous devez 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 pour accéder à cette valeur.
Si elle est trouvée, sa valeur est renvoyée. Sinon, il est déterminé que la couleur ne doit pas être harmonisée.
// 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 regroupe toutes les étapes mentionnées ci-dessus : elle récupère la valeur de couleur pour une ressource nommée, tente 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 (en fonction du thème clair ou foncé).
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. Remplir les cartes de transport
Comme indiqué précédemment, nous utiliserons un RecyclerView et un adaptateur pour remplir et colorer la collection de cartes de transport.

Stocker les données de transport en commun
Pour stocker les données textuelles et les informations sur les couleurs 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 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. Harmoniser automatiquement les couleurs
Vous pouvez choisir de gérer l'harmonisation manuellement ou de la confier à un tiers. HarmonizedColorOptions est une classe de création qui vous permet de spécifier la plupart des éléments que nous avons effectués manuellement jusqu'à présent.
Après avoir récupéré le contexte actuel pour accéder au schéma dynamique actuel, vous devez spécifier les couleurs de base que vous souhaitez harmoniser et créer un contexte basé sur cet objet HarmonizedColorOptions et le contexte DynamicColors activé.
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 étant déjà gérée, vous pouvez 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'à présent reposent sur la récupération des rôles de couleur à partir d'une couleur individuelle. C'est idéal pour montrer qu'une tonalité réelle est générée, mais ce n'est pas réaliste pour la plupart des applications existantes. Il est probable que vous ne dériviez pas directement une couleur, mais que vous utilisiez plutôt un attribut de thème existant.
Plus tôt dans cet atelier de programmation, nous avons parlé de l'exportation des 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 essentielle entre les deux méthodes. Nous devons également fournir une superposition de thème 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 utiliserait le contexte harmonisé. Les valeurs de la superposition de thème doivent faire référence à la variante claire ou foncée 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'UI
Thème 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
- Comment générer des rôles de couleur à partir d'une couleur vue donnée.
- Comment harmoniser sélectivement une couleur dans une interface utilisateur.
- Comment 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.