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

62ff4b2fb6c9e14a.png

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.

20cc2cf72efef213.png

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.

6c962ad528c09b4.png

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.

c6ee942b2b93cd92.png

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.

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

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.

766516c321348a7c.png

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:

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

e4555089b065b5a7.png

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

a5a02a72aef30529.png

Cores personalizadas harmonizadas

4ac88011173d6753.png d5084780d2c6b886.png

dd0c8b90eccd8bef.png c51f8a677b22cd54.png

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.