1. Prima di iniziare
In questo codelab imparerai ad armonizzare i tuoi colori personalizzati con quelli generati da un tema dinamico.
Prerequisiti
Gli sviluppatori devono
- Familiarità con i concetti di base dei temi in Android
- Comoda esperienza di lavoro con le visualizzazioni dei widget Android e le relative proprietà
Obiettivi didattici
- Come utilizzare l'armonizzazione dei colori nella tua applicazione utilizzando più metodi
- Come funziona l'armonizzazione e come cambia il colore
Che cosa ti serve
- Un computer con Android installato, se vuoi seguire la procedura.
2. Panoramica app
Voyaĝi è un'applicazione di trasporto pubblico che utilizza già un tema dinamico. Per molti sistemi di trasporto pubblico, il colore è un indicatore importante di treni, autobus o tram e non può essere sostituito da colori primari, secondari o terziari dinamici disponibili. Ci concentreremo sul RecyclerView delle tessere del trasporto pubblico colorate.

3. Generazione di un tema
Ti consigliamo di utilizzare il nostro strumento Material Theme Builder come primo passo per creare un tema Material3. Nella scheda Personalizzata, ora puoi aggiungere altri colori al tema. A destra, vengono visualizzati i ruoli dei colori e le tavolozze tonali per questi colori.
Nella sezione dei colori estesi, puoi rimuovere o rinominare i colori.

Il menu di esportazione mostrerà una serie di opzioni di esportazione possibili. Al momento della stesura, la gestione speciale delle impostazioni di armonizzazione di Material Theme Builder è disponibile solo in Android Views

Informazioni sui nuovi valori di esportazione
Per consentirti di utilizzare questi colori e i relativi ruoli di colore nei tuoi temi, indipendentemente dal fatto che tu scelga o meno di armonizzarli, il download esportato ora include un file attrs.xml contenente i nomi dei ruoli di colore per ogni colore personalizzato.
<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>
In themes.xml, abbiamo generato i quattro ruoli di colore per ogni colore personalizzato (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>). Le proprietà harmonize<name> riflettono se lo sviluppatore ha selezionato l'opzione in Material Theme Builder. Non sposterà il colore nel tema principale.
<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>
Nel file colors.xml, i colori iniziali utilizzati per generare i ruoli colore elencati sopra vengono specificati insieme ai valori booleani che indicano se la tavolozza dei colori verrà spostata o meno.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Esaminare il colore personalizzato
Se ingrandiamo il riquadro laterale di Material Theme Builder, possiamo vedere che l'aggiunta di un colore personalizzato mostra un riquadro con i quattro ruoli di colore chiave in una tavolozza chiara e scura.

In Android Views, esportiamo questi colori per te, ma dietro le quinte possono essere rappresentati da un'istanza dell'oggetto ColorRoles.
La classe ColorRoles ha quattro proprietà: accent, onAccent, accentContainer e onAccentContainer. Queste proprietà sono la rappresentazione intera dei quattro colori esadecimali.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
Puoi recuperare i quattro ruoli di colore chiave da un colore arbitrario in fase di runtime utilizzando getColorRoles nella classe MaterialColors chiamata getColorRoles, che ti consente di creare questo insieme di quattro ruoli di colore in fase di runtime dato un colore seme specifico.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Allo stesso modo, i valori di output sono i valori di colore effettivi, NON i relativi puntatori.**
5. Che cos'è l'armonizzazione dei colori?
Il nuovo sistema di colori di Material è algoritmico per progettazione e genera colori primari, secondari, terziari e neutri da un determinato colore seme. Un punto di preoccupazione che abbiamo riscontrato spesso quando abbiamo parlato con partner interni ed esterni è come adottare il colore dinamico mantenendo il controllo su alcuni colori.
Questi colori spesso hanno un significato o un contesto specifico nell'applicazione che andrebbe perso se venissero sostituiti da un colore casuale. In alternativa, se lasciati così come sono, questi colori potrebbero apparire visivamente stridenti o fuori luogo.
Il colore in Material You è descritto da tonalità, croma e tono. La tonalità di un colore si riferisce alla percezione che si ha di esso come membro di una gamma di colori rispetto a un'altra. Il tono descrive quanto appare chiaro o scuro, mentre la croma è l'intensità del colore. La percezione della tonalità può essere influenzata da fattori culturali e linguistici, come la mancanza di una parola per il blu nelle culture antiche, che veniva invece visto nella stessa famiglia del verde.
Una tonalità particolare può essere considerata calda o fredda a seconda della sua posizione nello spettro delle tonalità. Il passaggio a una tonalità rossa, arancione o gialla è generalmente considerato un aumento della temperatura, mentre il passaggio a una tonalità blu, verde o viola è considerato una diminuzione della temperatura. Anche all'interno dei colori caldi o freddi, avrai toni caldi e freddi. Di seguito, il giallo "più caldo" è più aranciato, mentre il giallo "più freddo" è più influenzato dal verde. 
L'algoritmo di armonizzazione dei colori esamina la tonalità del colore non spostato e il colore con cui deve essere armonizzato per individuare una tonalità armoniosa che non alteri le qualità cromatiche sottostanti. Nella prima immagine, sono rappresentate tonalità di verde, giallo e arancione meno armoniose su uno spettro. Nella grafica successiva, il verde e l'arancione sono stati armonizzati con la tonalità gialla. Il nuovo verde è più caldo e il nuovo arancione è più freddo.
La tonalità è cambiata sull'arancione e sul verde, ma questi colori sono ancora percepibili come arancione e verde.

Se vuoi saperne di più su alcune delle decisioni di progettazione, esplorazioni e considerazioni, i miei colleghi Ayan Daniels e Andrew Lu hanno scritto un post del blog che approfondisce l'argomento rispetto a questa sezione.
6. Armonizzare manualmente un colore
Per armonizzare un singolo tono, esistono due funzioni in MaterialColors, harmonize e harmonizeWithPrimary.
harmonizeWithPrimary utilizza Context come mezzo per accedere al tema corrente e, di conseguenza, al colore 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);
}
Per recuperare il set di quattro toni, dobbiamo fare qualcos'altro.
Dato che abbiamo già il colore di origine, dobbiamo:
- determinare se deve essere armonizzato,
- determinare se siamo in modalità Buio e
- restituisce un oggetto
ColorRolesarmonizzato o non armonizzato.
Determinare se armonizzare
Nel tema esportato da Material Theme Builder, abbiamo incluso attributi booleani utilizzando la nomenclatura harmonize<Color>. Di seguito è riportata una funzione di convenienza per accedere a questo valore.
Se viene trovato, ne restituisce il valore; altrimenti determina che non deve armonizzare il colore.
// 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
}
Creazione di un oggetto ColorRoles armonizzato
retrieveHarmonizedColorRoles è un'altra funzione di convenienza che unisce tutti i passaggi sopra menzionati: recupera il valore del colore per una risorsa denominata, tenta di risolvere un attributo booleano per determinare l'armonizzazione e restituisce un oggetto ColorRoles derivato dal colore originale o combinato (dato lo schema chiaro o scuro).
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. Aggiunta di carte del trasporto pubblico
Come accennato in precedenza, utilizzeremo un RecyclerView e un adattatore per popolare e colorare la raccolta di schede del trasporto pubblico.

Archiviazione dei dati di transito
Per memorizzare i dati di testo e le informazioni sul colore delle tessere del trasporto pubblico, utilizziamo una classe di dati che memorizza il nome, la destinazione e l'ID risorsa colore.
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)
)
Utilizzeremo questo colore per generare le tonalità di cui abbiamo bisogno in tempo reale.
Puoi armonizzare in fase di runtime con la seguente funzione 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. Armonizzare automaticamente i colori
In alternativa alla gestione manuale dell'armonizzazione, puoi delegarla. HarmonizedColorOptions è una classe di builder che ti consente di specificare gran parte di ciò che abbiamo fatto finora manualmente.
Dopo aver recuperato il contesto corrente per accedere allo schema dinamico attuale, devi specificare i colori di base che vuoi armonizzare e creare un nuovo contesto basato sull'oggetto HarmonizedColorOptions e sul contesto in cui è abilitato DynamicColors.
Se non vuoi armonizzare un colore, non includerlo in harmonizedOptions.
val newContext = DynamicColors.wrapContextIfAvailable(requireContext())
val harmonizedOptions = HarmonizedColorsOptions.Builder()
.setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
.build();
harmonizedContext =
HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)
Con il colore di base armonizzato già gestito, puoi aggiornare onBindViewHolder per chiamare semplicemente MaterialColors.getColorRoles e specificare se i ruoli restituiti devono essere chiari o scuri.
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. Armonizzazione automatica degli attributi del tema
I metodi mostrati finora si basano sul recupero dei ruoli dei colori da un singolo colore. È un ottimo modo per dimostrare che viene generato un tono reale, ma non è realistico per la maggior parte delle applicazioni esistenti. Probabilmente non deriverai un colore direttamente, ma utilizzerai un attributo del tema esistente.
In precedenza in questo codelab, abbiamo parlato dell'esportazione degli attributi del tema.
<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>
Analogamente al primo metodo automatico, possiamo fornire valori a HarmonizedColorOptions e utilizzare HarmonizedColors per recuperare un contesto con i colori armonizzati. Esiste una differenza fondamentale tra i due metodi. Inoltre, dobbiamo fornire una sovrapposizione del tema contenente i campi da armonizzare.
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)
L'adattatore utilizzerebbe il contesto armonizzato. I valori nella sovrapposizione del tema devono fare riferimento alla variante chiara o scura non armonizzata.
<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>
All'interno del file di layout XML, possiamo utilizzare questi attributi armonizzati normalmente.
<?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. Codice sorgente
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. Esempi di UI
Temi predefiniti e colori personalizzati senza armonizzazione

Colori personalizzati armonizzati


12. Riepilogo
In questo codelab hai imparato:
- Nozioni di base sul nostro algoritmo di armonizzazione dei colori
- Come generare ruoli di colore da un colore visualizzato.
- Come armonizzare selettivamente un colore in un'interfaccia utente.
- Come armonizzare un insieme di attributi in un tema.
Se hai domande, non esitare a contattarci in qualsiasi momento utilizzando @MaterialDesign su Twitter.
Continua a seguirci per altri contenuti e tutorial di design su youtube.com/MaterialDesign.