1. Prima di iniziare
In questo codelab, imparerai ad armonizzare i tuoi colori personalizzati con quelli generati da un tema dinamico.
Prerequisiti
Gli sviluppatori dovrebbero
- Familiarità con i concetti di base della tematizzazione in Android
- Facile utilizzo delle viste dei widget Android e delle relative proprietà
Obiettivi didattici
- Come utilizzare l'armonizzazione del colore nell'applicazione utilizzando più metodi
- Come funziona l'armonizzazione e come cambia colore
Che cosa ti serve
- Un computer su cui è installato Android 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 con qualsiasi colore primario, secondario o terziario dinamico disponibile. Ci concentreremo sulla RecyclerView delle carte del trasporto pubblico colorate.
3. Generazione di un tema
Per creare un tema Material3, ti consigliamo di usare il nostro strumento Strumento per la creazione di temi Material come prima cosa. Nella scheda Personalizzata, ora puoi aggiungere altri colori al tema. Sulla destra, verranno visualizzati i ruoli e le tavolozze dei toni relativi a questi colori.
Nella sezione dei colori estesi, puoi rimuovere o rinominare i colori.
Il menu Esporta mostra una serie di possibili opzioni di esportazione. Al momento della stesura di questo articolo, la gestione speciale delle impostazioni di armonizzazione da parte di Material Theme Builder è disponibile solo nelle viste Android
Informazioni sui nuovi valori di esportazione
Per consentirti di utilizzare questi colori e i relativi ruoli per i colori nei 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 dei colori 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 topics.xml, abbiamo generato i quattro ruoli relativi ai colori per ogni colore personalizzato (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>
). Le proprietà harmonize<name>
indicano se lo sviluppatore ha selezionato l'opzione in Material Theme Builder. Il colore non cambierà 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 originali utilizzati per generare i ruoli dei colori elencati in precedenza sono specificati insieme ai valori booleani per indicare se la tavolozza del colore verrà modificata o meno.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Analisi del colore personalizzato
Se aumentiamo lo zoom nel riquadro laterale del generatore di temi Material, notiamo che l'aggiunta di un colore personalizzato fa risaltare un riquadro con i quattro ruoli colore principali in una tavolozza delle tonalità chiare e scure.
Nelle visualizzazioni Android, questi colori vengono esportati per te, ma in background 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 di numeri interi 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 principali per i colori da un colore arbitrario in fase di runtime utilizzando getColorRoles
nella classe MaterialColors denominata getColorRoles
, che ti consente di creare quel set di quattro ruoli per i colori in fase di runtime con un colore di origine specifico.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Analogamente, i valori di output sono i valori effettivi del colore, NON i puntatori.**
5. Che cos'è l'armonizzazione dei colori?
Il nuovo sistema di colori del materiale è progettato in modo algoritmico e genera colori primari, secondari, terziari e neutri da un determinato colore di origine. Una delle preoccupazioni che abbiamo ricevuto spesso quando abbiamo parlato con i partner interni ed esterni è il modo in cui adottare i colori dinamici mantenendo il controllo su alcuni colori.
Questi colori spesso hanno un significato o un contesto specifico nell'applicazione che andrebbero persi se venissero sostituiti da un colore casuale. In alternativa, se lasciati invariati, questi colori potrebbero sembrare visivamente fastidiosi o fuori luogo.
Il colore in Material You è descritto da tonalità, crominanza e tono. La tonalità di un colore è correlata alla percezione che uno si fa di un colore come membro di una gamma di colori piuttosto che di un'altra. Il tono descrive quanto appare chiaro o scuro e la crominanza è l'intensità del colore. La percezione della tonalità può essere influenzata da fattori culturali e linguistici, ad esempio la mancanza di una parola per il blu nelle culture antiche, perché ora viene considerata nella stessa famiglia del verde.
Una determinata tonalità può essere considerata calda o fredda a seconda del punto in cui si trova nello spettro di tonalità. Il passaggio a una tonalità di rosso, arancione o giallo è generalmente considerato più caldo, mentre verso un blu, verde o viola si dice che lo renda più fresco. Anche se i colori sono caldi o freddi, i toni saranno caldi e freddi. Sotto, la parola "scaldavivande" il giallo è più arancione, mentre il "più freddo" il giallo è più influenzato dal verde.
L'algoritmo di armonizzazione del colore esamina la tonalità del colore non modificato e il colore con cui deve essere armonizzata per individuare una tonalità armoniosa che non alteri le qualità cromatiche sottostanti. Nel primo grafico, le tonalità di verde, giallo e arancione sono meno armoniose tracciate in uno spettro. Nel grafico successivo, 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à è mutata sull'arancione e sul verde, ma è ancora possibile percepirli come arancione e verde.
Se vuoi saperne di più su alcune decisioni, esplorazioni e considerazioni relative alla progettazione, i miei colleghi Ayan Daniels e Andrew Lu hanno scritto un post del blog un po' più approfondito di questa sezione.
6. Armonizzazione manuale di 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, successivamente, al suo 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 l'insieme di quattro toni, dobbiamo fare un altro po' di più.
Dato che disponiamo già del colore di origine, dobbiamo:
- determinare se devono essere armonizzati,
- determinare se siamo in modalità Buio
- restituiscono un oggetto
ColorRoles
armonizzato o non armonizzato.
Stabilire se armonizzare
Nel tema esportato dal generatore di temi Material, abbiamo incluso degli attributi booleani utilizzando la nomenclatura harmonize<Color>
. Di seguito è riportata una funzione di convenienza per accedere a questo valore.
Se lo trova, restituisce il suo valore; altrimenti determina che non dovrebbe 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 citati sopra: 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 misto (considerato 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. Inserimento delle carte del trasporto pubblico in corso...
Come accennato prima, utilizzeremo RecyclerView e un adattatore per compilare e colorare la raccolta delle carte del trasporto pubblico.
Archiviazione dei dati sul trasporto pubblico
Per archiviare i dati di testo e le informazioni sul colore delle carte del trasporto pubblico, utilizziamo una classe di dati in cui vengono memorizzati il nome, la destinazione e l'ID risorsa del 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 i toni di cui abbiamo bisogno in tempo reale.
Puoi armonizzarti 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. Armonizzazione automatica dei colori
Puoi lasciare che sia un'alternativa alla gestione manuale dell'armonizzazione. HarmonizedColorOptions è una classe del builder che consente di specificare gran parte di ciò che abbiamo fatto finora a mano.
Dopo aver recuperato il contesto corrente in modo da avere accesso allo schema dinamico corrente, devi specificare i colori di base che vuoi armonizzare e creare un nuovo contesto in base all'oggetto HarmonizedColorOptions e al contesto abilitato per DynamicColors.
Se non desideri armonizzare un colore, non includerlo in armonizedOptions.
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 onBindViewAspettaer 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 fino ad ora si basano sul recupero dei ruoli dei colori da un singolo colore. Questo è ottimo per dimostrare che viene generato un tono reale, ma non realistico per la maggior parte delle applicazioni esistenti. Probabilmente non ricaverai direttamente un colore, ma utilizzerai un attributo tema esistente.
In precedenza in questo codelab abbiamo parlato dell'esportazione degli attributi dei temi.
<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 ad HarmonizedColorOptions e utilizzare HarmonizedColors per recuperare un contesto con i colori armonizzati. Esiste una differenza fondamentale tra i due metodi. Dobbiamo inoltre fornire un overlay 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 userebbe il contesto armonizzato. I valori nell'overlay 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 come di consueto.
<?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. UI di esempio
Tema predefinito e colori personalizzati senza armonizzazione
Colori personalizzati armonizzati
12. Riepilogo
In questo codelab, hai appreso:
- Nozioni di base sul nostro algoritmo di armonizzazione dei colori
- Come generare ruoli colore da un determinato colore rilevato.
- Come armonizzare selettivamente un colore in un'interfaccia utente.
- Come armonizzare un insieme di attributi in un tema.
Per eventuali domande, non esitare a contattarci in qualsiasi momento tramite @MaterialDesign su Twitter.
Continua a seguirci per altri tutorial e contenuti di design su youtube.com/MaterialDesign