Harmonização de cores básica nas visualizações do Android

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.

62ff4b2fb6c9e14a.png

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.

20cc2cf72efef213.png

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.

6c962ad528c09b4.png

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.

c6ee942b2b93cd92.png

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.

57c46d9974c52e4a.pngUm 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. 597c6428ff6b9669.png

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.

766516c321348a7c.png

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:

  1. determinar se ele precisa ser harmonizado;
  2. determinar se estamos no modo escuro e
  3. retornar um objeto ColorRoles harmonizado 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.

e4555089b065b5a7.png

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

a5a02a72aef30529.png

Cores personalizadas harmonizadas

4ac88011173d6753.png d5084780d2c6b886.png

dd0c8b90eccd8bef.png c51f8a677b22cd54.png

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.