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

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 devem ser

  • Conhecer os conceitos básicos de aplicação 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 seu aplicativo usando vários métodos
  • Como funciona a harmonização e como ela muda de 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 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 eles não podem ser substituídos por quaisquer cores dinâmicas primárias, secundárias ou terciárias disponíveis. Vamos concentrar nosso trabalho na RecyclerView de cartões de transporte público coloridos.

62ff4b2fb6c9e14a.png

3. Como gerar um tema

Recomendamos usar nossa ferramenta Material Theme Builder como primeira parada para criar um tema do Material3. Na guia personalizada, agora você pode adicionar mais cores ao tema. À direita, serão mostrados os papéis de cores e as paletas tonais dessas cores.

Na seção de cores estendida, você pode remover ou renomear cores.

20cc2cf72efef213.png

O menu de exportação exibirá várias opções possíveis de exportação. No momento, o processamento especial de configurações de harmonização do Material Theme Builder está disponível apenas nas visualizações do Android.

6c962ad528c09b4.png

Noções básicas sobre os novos valores de exportação

Para permitir que você use essas cores e as funções de cores associadas nos seus temas, independentemente de optar por harmonizar ou não, o download exportado agora inclui um arquivo attrs.xml com os nomes dos papéis das cores de 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 topics.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. Isso não 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 de semente usadas para gerar os papéis de cor listados acima são especificadas com valores booleanos para indicar se a paleta de cores será deslocada ou não.

<resources>
   <!-- other colors used in theme -->

   <color name="custom1">#1AC9E0</color>
   <color name="custom2">#32D312</color>
</resources>

4. Análise de cor personalizada

Analisando o painel lateral do Material Theme Builder, podemos ver que a adição de uma cor personalizada mostra um painel com as quatro principais funções de cor em uma paleta clara e escura.

c6ee942b2b93cd92.png

Nas visualizações do Android, exportamos essas cores, 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 do número inteiro 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 principais funções de cor de uma cor arbitrária no ambiente de execução usando getColorRoles na classe MaterialColors chamada getColorRoles, que permite criar esse conjunto de quatro funções de cor no momento da execução, de acordo com uma cor de semente 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 é a harmonização de cores?

O novo sistema de cores do Material Design é algorítmico por design, gerando cores primárias, secundárias, terciárias e neutras a partir de uma determinada cor de semente. Um ponto de preocupação que recebemos muito ao conversar com parceiros internos e externos era como adotar cores dinâmicas, 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. Alternativamente, se deixadas como estão, essas cores podem parecer visualmente desfocadas ou fora do lugar.

Cor no Material Design Você é descrito por matiz, chroma e tom. A tonalidade de uma cor se relaciona com a percepção que alguém tem dela como membro de um intervalo de cores em relação a outro. O tom descreve quão claro ou escuro ele aparece e chroma é a intensidade da cor. A percepção de matiz pode ser afetada por fatores culturais e linguísticos, como a falta de uma palavra para azul em culturas antigas, em vez de ser vista na mesma família que o verde.

57c46d9974c52e4a.pngUma tonalidade específica pode ser considerada quente ou fria, dependendo de onde ela está no espectro de matiz. Normalmente, quando muda para um tom vermelho, laranja ou amarelo, ela fica mais quente e, para um tom azul, verde ou roxo, ela fica mais fria. Mesmo com cores quentes ou frias, você terá tons quentes e frios. Abaixo, está o termo "mais quente", o amarelo é mais alaranjado, enquanto a opção "mais fria" o amarelo é mais influenciado pelo verde. 597c6428ff6b9669.png

O algoritmo de harmonização de cores examina a tonalidade da cor não alterada e a cor com que ela deve ser harmonizada para localizar uma tonalidade harmoniosa, mas que não altere suas qualidades de cor subjacentes. No primeiro gráfico, há tons menos harmoniosos de verde, amarelo e laranja plotados 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 fresco.

A tonalidade mudou em laranja e verde, mas eles ainda podem ser percebidos como laranja e verde.

766516c321348a7c.png

Se você quiser saber mais sobre algumas das decisões, análises detalhadas e considerações de design, meus colegas Ayan Daniels e Andrew Lu escreveram uma postagem do blog um pouco mais detalhada do que esta seção.

6. Como harmonizar uma cor manualmente

Para harmonizar um único tom, há duas funções em MaterialColors, harmonize e harmonizeWithPrimary.

O harmonizeWithPrimary usa o Context para acessar o tema atual e, em seguida, a cor principal 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 da fonte, precisamos:

  1. determinar se deve ser harmonizado,
  2. determinar se estamos usando o modo escuro
  3. retornar um objeto ColorRoles harmonizado ou não harmonizado.

Como determinar se 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, retorna seu valor; caso contrário, ele determina que não deve 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
}

Como criar um objeto ColorRoles harmonizado

retrieveHarmonizedColorRoles é outra função de conveniência que une todas as etapas mencionadas acima: recuperar o valor da cor de um recurso nomeado, tenta resolver um atributo booleano para determinar a harmonização e retorna 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 anteriormente, vamos usar uma RecyclerView e um adaptador para preencher e colorir a coleção de cartões de transporte público.

e4555089b065b5a7.png

Como 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.

Você poderia harmonizar durante a 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

Uma alternativa ao processamento manual da harmonização é fazer com que ela seja feita para você. HarmonizedColorOptions é uma classe de builder que permite especificar manualmente muito do que fizemos até agora.

Depois de recuperar o contexto atual para ter acesso ao esquema dinâmico atual, especifique as cores de base que você quer harmonizar e crie um novo contexto com base nesse objeto HarmonizedColorOptions e no contexto ativado para DynamicColors.

Se você não quiser harmonizar uma cor, simplesmente não a inclua 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á processada, é possível 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. Harmonizar automaticamente os atributos do tema

Os métodos mostrados até agora dependem da recuperação dos papéis 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. Você provavelmente não vai derivar uma cor diretamente, mas usar um atributo de tema existente.

Anteriormente neste codelab, falamos sobre como exportar atributos do 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>

Assim como no primeiro método automático, podemos fornecer valores a HarmonizedColorOptions e usar HarmonizedColors para extrair um contexto com as cores harmonizadas. Há uma diferença importante entre os dois métodos. Além disso, precisamos fornecer uma sobreposição de temas 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 IUs

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:

  • Noções básicas do nosso algoritmo de harmonização de cores
  • Como gerar funções de cor com base em uma determinada cor.
  • 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.