1. Antes de começar
Neste codelab, você vai aprender a harmonizar as cores personalizadas com as geradas por um tema dinâmico.
Pré-requisitos
Os desenvolvedores precisam:
- Ter familiaridade com os conceitos básicos de temas no Android
- Saber trabalhar com visualizações de widgets do Android e as propriedades delas
O que você vai aprender
- Como usar a harmonização de cores no 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 aplicativo 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 ela 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 de cartões de transporte público coloridos.

3. Gerar um tema
Recomendamos usar nossa ferramenta Material Theme Builder como primeiro passo para criar um tema do Material 3. Na guia "Personalizado", agora é possível adicionar mais cores ao tema. À direita, você verá as funções de cor e as paletas de tons 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, o tratamento especial das configurações de harmonização do Material Theme Builder só está disponível nas visualizações do Android.

Entender os novos valores de exportação
Para permitir que você use essas cores e as funções de cor associadas nos temas, independentemente de escolher harmonizar ou não, o download exportado agora inclui um arquivo attrs.xml contendo os nomes das funções de cor 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>
Em themes.xml, geramos as quatro funções de cor 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. Ela 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 iniciais usadas para gerar as funções de cor 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 a adição de uma cor personalizada mostra um painel com as quatro funções de cor principais em uma paleta clara e escura.

Nas visualizações do Android, 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 inteira 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 recuperar as quatro funções de cor 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 cor no tempo de execução, considerando uma cor inicial 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 de uma determinada cor inicial. Um ponto de preocupação que recebemos muito ao conversar com parceiros internos e externos foi como adotar a cor dinâmica, mantendo 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. Como alternativa, se deixadas no estado em que se encontram, essas cores podem parecer visualmente desagradáveis ou fora do 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 como ela aparece clara ou escura, e o croma é a intensidade da cor. A percepção do matiz pode ser afetada por fatores culturais e linguísticos, como a falta de uma palavra para azul em culturas antigas, que é frequentemente mencionada, com ela sendo vista na mesma família do verde.
Um matiz específico pode ser considerado quente ou frio, dependendo de onde ele está no espectro de matiz. Mudar para um matiz vermelho, laranja ou amarelo geralmente é considerado mais quente, e para um azul, verde ou roxo é considerado mais frio. Mesmo nas cores quentes ou frias, você terá tons quentes e frios. Abaixo, o amarelo "mais quente" é mais alaranjado, enquanto o amarelo "mais frio" é mais influenciado pelo verde. 
O algoritmo de harmonização de cores examina o matiz da cor não alterada e a cor com que ela precisa ser harmonizada para localizar um matiz harmonioso, mas que não altera as qualidades de cor subjacentes. No primeiro gráfico, há matizes verde, amarelo e laranja menos harmoniosos plotados em um espectro. No próximo gráfico, o verde e o laranja foram harmonizados com o matiz 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 você quiser saber mais sobre algumas das decisões de design, análises detalhadas e considerações, meus colegas Ayan Daniels e Andrew Lu escreveram uma postagem no blog (link em inglês) que aborda o assunto 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.
harmonizeWithPrimary usa o Context como um meio de acessar o tema atual e, posteriormente, 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 ela precisa ser harmonizada,
- determinar se estamos no modo escuro e
- retornar um objeto
ColorRolesharmonizado ou não.
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 não é necessário harmonizar a cor.
// 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
}
Criar um objeto ColorRoles harmonizado
retrieveHarmonizedColorRoles é outra função de conveniência que une todas as etapas mencionadas: recuperar o valor da cor para um recurso nomeado, tentar resolver um atributo booleano para determinar a harmonização e retornar um objeto ColorRoles derivado da cor original ou mesclada (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. Preencher cartões de transporte público
Como mencionado anteriormente, vamos usar um RecyclerView e um adaptador para preencher e colorir a coleção de cartões de transporte público.

Armazenar dados de transporte público
Para armazenar os dados de texto e as informações de cor dos cartões de transporte público, estamos usando 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 tratamento manual da harmonização, você pode fazer com que ela seja tratada para você. HarmonizedColorOptions é uma classe de builder que permite especificar muito do que fizemos até agora manualmente.
Depois de recuperar o contexto atual para ter acesso ao esquema dinâmico atual, é necessário especificar as cores de base que você quer harmonizar e criar 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 de base harmonizada já tratada, você pode atualizar o onBindViewHolder para simplesmente chamar MaterialColors.getColorRoles e especificar se as funções retornadas precisam ser claras ou escuras.
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. Harmonizar atributos de tema automaticamente
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 contexto com as cores harmonizadas. Há uma diferença fundamental entre os dois métodos. Além disso, precisamos fornecer uma sobreposição de tema contendo 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)
O 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:
- Os conceitos básicos do nosso algoritmo de harmonização de cores
- Como gerar funções de cor de 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.