1. Antes de começar
Neste codelab, você vai aprender a harmonizar suas cores personalizadas com as geradas por um tema dinâmico.
Pré-requisitos
Os desenvolvedores precisam ser
- Conhecimento dos conceitos básicos de temas no Android
- Conhecimento de Views de widgets do Android e das propriedades delas
O que você vai aprender
- Como usar a harmonização de cores no seu aplicativo usando vários métodos
- Como a harmonização funciona e como ela muda a cor
O que é necessário
- Um computador com o Android instalado, se você quiser acompanhar.
2. Visão geral do app
O Voyaĝi é um app de transporte público que já usa um tema dinâmico. Para muitos sistemas de transporte público, a cor é um indicador importante de trens, ônibus ou bondes, e não pode ser substituída por cores primárias, secundárias ou terciárias dinâmicas disponíveis. Vamos concentrar nosso trabalho no RecyclerView dos cartões de transporte coloridos.

3. Gerar um tema
Recomendamos usar nossa ferramenta Material Theme Builder como primeira etapa para criar um tema do Material3. Na guia personalizada, agora é possível adicionar mais cores ao tema. À direita, você verá as funções de cor e as paletas tonais dessas cores.
Na seção de cores estendidas, é possível remover ou renomear cores.

O menu de exportação vai mostrar várias opções possíveis. No momento da redação deste artigo, o tratamento especial das configurações de harmonização do Material Theme Builder está disponível apenas no Android Views.

Entender os novos valores de exportação
Para permitir que você use essas cores e as funções de cores associadas nos seus temas, com ou sem harmonização, o download exportado agora inclui um arquivo attrs.xml com os nomes das funções de cores para cada cor personalizada.
<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>
No arquivo themes.xml, geramos as quatro funções de cores para cada cor personalizada (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>). As propriedades harmonize<name> refletem se o desenvolvedor selecionou a opção no Material Theme Builder. Isso não vai mudar a cor no 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>
No arquivo colors.xml, as cores principais usadas para gerar as funções de cores listadas acima são especificadas junto com valores booleanos para indicar se a paleta de cores será alterada ou não.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Analisar a cor personalizada
Ao ampliar o painel lateral do Material Theme Builder, podemos ver que adicionar uma cor personalizada mostra um painel com as quatro funções principais de cores em uma paleta clara e escura.

No Android Views, exportamos essas cores para você, mas, nos bastidores, elas podem ser representadas por uma instância do objeto ColorRoles.
A classe ColorRoles tem quatro propriedades: accent, onAccent, accentContainer e onAccentContainer. Essas propriedades são a representação de números inteiros das quatro cores hexadecimais.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
É possível extrair as quatro funções de cores principais de uma cor arbitrária no tempo de execução usando getColorRoles na classe MaterialColors chamada getColorRoles, que permite criar esse conjunto de quatro funções de cores no tempo de execução com uma cor de seed específica.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Da mesma forma, os valores de saída são os valores de cor reais, NÃO ponteiros para eles.**
5. O que é harmonização de cores?
O novo sistema de cores do Material é algorítmico por design, gerando cores primárias, secundárias, terciárias e neutras com base em uma cor inicial. Um ponto de preocupação que recebemos muito ao conversar com parceiros internos e externos foi como adotar a cor dinâmica e manter o controle sobre algumas cores.
Essas cores geralmente têm um significado ou contexto específico no aplicativo que seria perdido se fossem substituídas por uma cor aleatória. Caso contrário, se deixadas no estado em que se encontram, essas cores podem parecer visualmente desagradáveis ou fora de lugar.
A cor no Material You é descrita por matiz, croma e tom. O matiz de uma cor se relaciona à percepção dela como membro de um intervalo de cores em vez de outro. O tom descreve a aparência clara ou escura, e o croma é a intensidade da cor. A percepção da tonalidade pode ser afetada por fatores culturais e linguísticos, como a falta de uma palavra para azul em culturas antigas, que era visto na mesma família do verde.
Um matiz específico pode ser considerado quente ou frio, dependendo de onde ele se encontra no espectro de matizes. Mudar para um tom vermelho, laranja ou amarelo geralmente é considerado como aquecimento, e para um tom azul, verde ou roxo é considerado como resfriamento. Mesmo dentro das cores quentes ou frias, você terá tons quentes e frios. Abaixo, o amarelo "mais quente" tem um tom mais alaranjado, enquanto o amarelo "mais frio" é mais influenciado pelo verde. 
O algoritmo de harmonização de cores examina a matiz da cor não deslocada e a cor com que ela deve ser harmonizada para localizar uma matiz que seja harmoniosa, mas não altere as qualidades de cor subjacentes. No primeiro gráfico, há tons menos harmoniosos de verde, amarelo e laranja representados em um espectro. No próximo gráfico, o verde e o laranja foram harmonizados com o tom amarelo. O novo verde é mais quente, e o novo laranja é mais frio.
O matiz mudou no laranja e no verde, mas eles ainda podem ser percebidos como laranja e verde.

Se quiser saber mais sobre algumas das decisões, explorações e considerações de design, meus colegas Ayan Daniels e Andrew Lu escreveram uma postagem no blog com mais detalhes do que esta seção.
6. Harmonizar uma cor manualmente
Para harmonizar um único tom, há duas funções em MaterialColors, harmonize e harmonizeWithPrimary.
O harmonizeWithPrimary usa o Context como uma forma de acessar o tema atual e, consequentemente, a cor primária dele.
@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 o conjunto de quatro tons, precisamos fazer um pouco mais.
Como já temos a cor de origem, precisamos:
- determinar se ele precisa ser harmonizado;
- determinar se estamos no modo escuro e
- retornar um objeto
ColorRolesharmonizado ou não harmonizado.
Como determinar se é necessário harmonizar
No tema exportado do Material Theme Builder, incluímos atributos booleanos usando a nomenclatura harmonize<Color>. Confira abaixo uma função de conveniência para acessar esse valor.
Se encontrado, ele retorna o valor. Caso contrário, determina que a cor não deve ser harmonizada.
// 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
}
Como criar um objeto ColorRoles harmonizado
retrieveHarmonizedColorRoles é outra função de conveniência que une todas as etapas mencionadas: recuperação do valor de cor de um recurso nomeado, tentativas de resolver um atributo booleano para determinar a harmonização e retorno de um objeto ColorRoles derivado da cor original ou combinada (considerando o esquema claro ou escuro).
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. Preenchendo cartões de transporte público
Como mencionado antes, vamos usar um RecyclerView e um adaptador para preencher e colorir a coleção de cartões de transporte público.

Como armazenar dados de trânsito
Para armazenar os dados de texto e as informações de cor dos cartões de transporte público, usamos uma classe de dados que armazena o nome, o destino e o ID do recurso de cor.
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)
)
Vamos usar essa cor para gerar os tons necessários em tempo real.
É possível harmonizar no tempo de execução com a seguinte função 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. Harmonizar cores automaticamente
Como alternativa ao processamento manual, você pode deixar que a harmonização seja feita para você. HarmonizedColorOptions é uma classe builder que permite especificar muito do que fizemos manualmente até agora.
Depois de recuperar o contexto atual para ter acesso ao esquema dinâmico atual, especifique as cores básicas que você quer harmonizar e crie um novo contexto com base nesse objeto HarmonizedColorOptions e no contexto ativado do DynamicColors.
Se você não quiser harmonizar uma cor, basta não incluí-la em "harmonizedOptions".
val newContext = DynamicColors.wrapContextIfAvailable(requireContext())
val harmonizedOptions = HarmonizedColorsOptions.Builder()
.setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
.build();
harmonizedContext =
HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)
Com a cor base harmonizada já processada, você pode atualizar o onBindViewHolder para simplesmente chamar MaterialColors.getColorRoles e especificar se os papéis retornados devem ser claros ou escuros.
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. Harmonização automática de atributos de tema
Os métodos mostrados até agora dependem da recuperação das funções de cor de uma cor individual. Isso é ótimo para mostrar que um tom real está sendo gerado, mas não é realista para a maioria dos aplicativos atuais. É provável que você não derive uma cor diretamente, mas use um atributo de tema existente.
No início deste codelab, falamos sobre a exportação 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>
Semelhante ao primeiro método automático, podemos fornecer valores para HarmonizedColorOptions e usar HarmonizedColors para recuperar um Context com as cores harmonizadas. Há uma diferença principal entre os dois métodos. Também precisamos fornecer uma sobreposição de tema com os campos a serem harmonizados.
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)
Seu adaptador usaria o contexto harmonizado. Os valores na sobreposição de tema precisam se referir à variante clara ou escura não harmonizada.
<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 do arquivo de layout XML, podemos usar esses atributos harmonizados 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. Código-fonte
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. Exemplos de interfaces
Temas padrão e cores personalizadas sem harmonização

Cores personalizadas harmonizadas


12. Resumo
Neste codelab, você aprendeu a:
- Conceitos básicos do nosso algoritmo de harmonização de cores
- Como gerar funções de cores com base em uma cor vista.
- Como harmonizar seletivamente uma cor em uma interface do usuário.
- Como harmonizar um conjunto de atributos em um tema.
Se tiver dúvidas, fale com a gente a qualquer momento usando o @MaterialDesign no Twitter.
Confira outros tutoriais e conteúdo de design em youtube.com/MaterialDesign.