1. Antes de comenzar
En este codelab, aprenderás a armonizar tus colores personalizados con los que genera un tema dinámico.
Requisitos previos
Los desarrolladores deben tener los siguientes conocimientos:
- Conocimientos básicos sobre los conceptos de temas en Android
- Comodidad para trabajar con Views de widgets de Android y sus propiedades
Qué aprenderás
- Cómo usar la armonización de colores en tu aplicación con varios métodos
- Cómo funciona la armonización y cómo cambia el color
Requisitos
- Una computadora con Android instalado si quieres seguir el codelab.
2. Descripción general de la app
Voyaĝi es una aplicación de transporte público que ya usa un tema dinámico. Para muchos sistemas de transporte público, el color es un indicador importante de trenes, autobuses o tranvías, y estos no se pueden reemplazar por los colores primarios, secundarios o terciarios dinámicos que estén disponibles. Enfocaremos nuestro trabajo en el RecyclerView de las tarjetas de transporte público de colores.

3. Genera un tema
Te recomendamos que uses nuestra herramienta Material Theme Builder como primer paso para crear un tema de Material 3. En la pestaña personalizada, ahora puedes agregar más colores a tu tema. A la derecha, se mostrarán las funciones de color y las paletas tonales para esos colores.
En la sección de color extendido, puedes quitar o cambiar el nombre de los colores.

El menú de exportación mostrará varias opciones de exportación posibles. Al momento de escribir este artículo, el manejo especial de la configuración de armonización de Material Theme Builder solo está disponible en Android Views.

Comprende los nuevos valores de exportación
Para permitirte usar estos colores y sus funciones de color asociadas en tus temas, ya sea que elijas armonizar o no, la descarga exportada ahora incluye un archivo attrs.xml que contiene los nombres de las funciones de color para cada color personalizado.
<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>
En themes.xml, generamos las cuatro funciones de color para cada color personalizado (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>). Las propiedades harmonize<name> reflejan si el desarrollador seleccionó la opción en Material Theme Builder. No cambiará el color en el tema principal.
<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>
En el archivo colors.xml, se especifican los colores semilla que se usan para generar las funciones de color mencionadas anteriormente, junto con los valores booleanos para determinar si se cambiará o no la paleta de colores.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Examina el color personalizado
Si hacemos zoom en el panel lateral de Material Theme Builder, podemos ver que agregar un color personalizado muestra un panel con las cuatro funciones de color clave en una paleta clara y oscura.

En Android Views, exportamos estos colores por ti, pero, en segundo plano, se pueden representar con una instancia del objeto ColorRoles.
La clase ColorRoles tiene cuatro propiedades: accent, onAccent, accentContainer, y onAccentContainer. Estas propiedades son la representación entera de los cuatro colores hexadecimales.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
Puedes recuperar las cuatro funciones de color clave de un color arbitrario en el tiempo de ejecución con getColorRoles en la clase MaterialColors llamada getColorRoles, que te permite crear ese conjunto de cuatro funciones de color en el tiempo de ejecución dado un color semilla específico.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Del mismo modo, los valores de salida son los valores de color reales, NO punteros a ellos.**
5. ¿Qué es la armonización de colores?
El nuevo sistema de colores de Material es algorítmico por diseño, ya que genera colores primarios, secundarios, terciarios y neutros a partir de un color semilla determinado. Un punto de preocupación que recibimos mucho cuando hablamos con socios internos y externos fue cómo adoptar el color dinámico y, al mismo tiempo, mantener el control sobre algunos colores.
Estos colores suelen tener un significado o contexto específico en la aplicación que se perdería si se reemplazaran por un color aleatorio. De manera alternativa, si se dejan como están, estos colores podrían verse visualmente discordantes o fuera de lugar.
El color en Material You se describe por el matiz, la croma y el tono. El matiz de un color se relaciona con la percepción que tiene una persona de él como miembro de un rango de color en comparación con otro. El tono describe qué tan claro u oscuro aparece, y la croma es la intensidad del color. La percepción del matiz puede verse afectada por factores culturales y lingüísticos, como la falta de una palabra para el azul en las culturas antiguas, que se menciona con frecuencia, y que, en cambio, se ve en la misma familia que el verde.
Un matiz en particular se puede considerar cálido o frío según su ubicación en el espectro de matices. Por lo general, se considera que cambiar a un matiz rojo, naranja o amarillo lo hace más cálido, y se dice que cambiar a un azul, verde o morado lo hace más frío. Incluso dentro de los colores cálidos o fríos, tendrás tonos cálidos y fríos. A continuación, el amarillo "más cálido" tiene un tono más naranja, mientras que el amarillo "más frío" está más influenciado por el verde. 
El algoritmo de armonización de colores examina el matiz del color sin cambiar y el color con el que se debe armonizar para ubicar un matiz que sea armonioso, pero que no altere sus cualidades de color subyacentes. En el primer gráfico, hay matices menos armoniosos de verde, amarillo y naranja trazados en un espectro. En el siguiente gráfico, el verde y el naranja se armonizaron con el matiz amarillo. El nuevo verde es más cálido y el nuevo naranja es más frío.
El matiz cambió en el naranja y el verde, pero aún se pueden percibir como naranja y verde.

Si quieres obtener más información sobre algunas de las decisiones de diseño, exploraciones y consideraciones, mis colegas Ayan Daniels y Andrew Lu escribieron una entrada de blog que profundiza un poco más que esta sección.
6. Armoniza un color de forma manual
Para armonizar un solo tono, hay dos funciones en MaterialColors, harmonize y harmonizeWithPrimary.
harmonizeWithPrimary usa el Context como un medio para acceder al tema actual y, luego, al color primario.
@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);
}
Para recuperar el conjunto de cuatro tonos, debemos hacer un poco más.
Dado que ya tenemos el color fuente, debemos hacer lo siguiente:
- Determinar si se debe armonizar
- Determinar si estamos en modo oscuro
- Mostrar un objeto
ColorRolesarmonizado o no armonizado
Determina si se debe armonizar
En el tema exportado de Material Theme Builder, incluimos atributos booleanos con la nomenclatura harmonize<Color>. A continuación, se muestra una función de conveniencia para acceder a ese valor.
Si se encuentra, muestra su valor; de lo contrario, determina que no debe armonizar el color.
// Looks for associated harmonization attribute based on the color id
// custom1 ===> harmonizeCustom1
fun shouldHarmonize(context: Context, colorId: Int): Boolean {
val root = context.resources.getResourceEntryName(colorId)
val harmonizedId = "harmonize" + root.replaceFirstChar { it.uppercaseChar() }
val identifier = context.resources.getIdentifier(
harmonizedId, "bool", context.packageName)
return if (identifier != 0) context.resources.getBoolean(identifier) else false
}
Crea un objeto ColorRoles armonizado
retrieveHarmonizedColorRoles es otra función de conveniencia que une todos los pasos mencionados anteriormente: recuperar el valor de color para un recurso con nombre, intentar resolver un atributo booleano para determinar la armonización y mostrar un objeto ColorRoles derivado del color original o combinado (dado el esquema claro u oscuro).
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. Completa las tarjetas de transporte público
Como se mencionó anteriormente, usaremos un RecyclerView y un adaptador para completar y colorear la colección de tarjetas de transporte público.

Almacena datos de transporte público
Para almacenar los datos de texto y la información de color de las tarjetas de transporte público, usamos una clase de datos que almacena el nombre, el destino y el ID del recurso de color.
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)
)
Usaremos este color para generar los tonos que necesitamos en tiempo real.
Podrías armonizar en el tiempo de ejecución con la siguiente función 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. Armoniza colores automáticamente
Como alternativa al manejo manual de la armonización, puedes hacer que se maneje por ti. HarmonizedColorOptions es una clase de compilador que te permite especificar gran parte de lo que hicimos hasta ahora de forma manual.
Después de recuperar el contexto actual para que tengas acceso al esquema dinámico actual, debes especificar los colores base que deseas armonizar y crear un contexto nuevo basado en ese objeto HarmonizedColorOptions y el contexto habilitado de DynamicColors.
Si no quieres armonizar un color, simplemente no lo incluyas en 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 el color base armonizado ya controlado, puedes actualizar tu onBindViewHolder para llamar simplemente a MaterialColors.getColorRoles y especificar si las funciones que se muestran deben ser claras u oscuras.
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. Armoniza automáticamente los atributos del tema
Los métodos que se muestran hasta ahora dependen de la recuperación de las funciones de color de un color individual. Eso es excelente para mostrar que se está generando un tono real, pero no es realista para la mayoría de las aplicaciones existentes. Es probable que no derives un color directamente, sino que uses un atributo de tema existente.
Anteriormente en este codelab, hablamos sobre la exportación de atributos de 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>
Al igual que el primer método automático, podemos proporcionar valores a HarmonizedColorOptions y usar HarmonizedColors para recuperar un Context con los colores armonizados. Hay una diferencia clave entre los dos métodos. Además, debemos proporcionar una superposición de tema que contenga los campos que se armonizarán.
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)
Tu adaptador usaría el contexto armonizado. Los valores de la superposición de tema deben hacer referencia a la variante clara u oscura no armonizada.
<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>
Dentro del archivo de diseño XML, podemos usar esos atributos armonizados como de costumbre.
<?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. Código fuente
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. Ejemplos de IU
Temas predeterminados y colores personalizados sin armonización

Colores personalizados armonizados


12. Resumen
En este codelab, aprendiste lo siguiente:
- Los conceptos básicos de nuestro algoritmo de armonización de colores
- Cómo generar funciones de color a partir de un color visto determinado
- Cómo armonizar de forma selectiva un color en una interfaz de usuario
- Cómo armonizar un conjunto de atributos en un tema
Si tienes alguna pregunta, no dudes en consultarnos en cualquier momento en @MaterialDesign en Twitter.
Mira más instructivos y contenido de diseño en youtube.com/MaterialDesign.