1. Sebelum memulai
Dalam codelab ini, Anda akan mempelajari cara menyelaraskan warna kustom dengan warna yang dihasilkan oleh tema dinamis.
Prasyarat
Developer harus
- Memahami konsep tema dasar di Android
- Nyaman bekerja dengan widget Android View dan propertinya
Yang akan Anda pelajari
- Cara menggunakan harmonisasi warna di aplikasi Anda menggunakan beberapa metode
- Cara kerja harmonisasi dan cara mengubah warna
Yang Anda butuhkan
- Komputer yang dilengkapi Android jika Anda ingin mengikuti langkah-langkahnya.
2. Ringkasan Aplikasi
Voyaĝi adalah aplikasi transportasi umum yang sudah menggunakan tema dinamis. Untuk banyak sistem transportasi umum, warna adalah indikator penting untuk kereta, bus, atau trem dan warna ini tidak dapat digantikan oleh warna primer, sekunder, atau tersier dinamis apa pun yang tersedia. Kita akan memfokuskan pekerjaan pada RecyclerView kartu transportasi umum berwarna.

3. Membuat Tema
Sebaiknya gunakan alat kami, Material Theme Builder, sebagai langkah pertama untuk membuat tema Material3. Di tab kustom, Anda kini dapat menambahkan lebih banyak warna ke tema. Di sebelah kanan, Anda akan melihat peran warna dan palet tonal untuk warna tersebut.
Di bagian warna yang diperluas, Anda dapat menghapus atau mengganti nama warna.

Menu ekspor akan menampilkan sejumlah opsi ekspor yang memungkinkan. Pada saat penulisan, penanganan khusus setelan harmonisasi Material Theme Builder hanya tersedia di Android Views

Memahami nilai ekspor baru
Untuk memungkinkan Anda menggunakan warna ini dan peran warna terkait dalam tema, baik Anda memilih untuk menyelaraskan atau tidak, download yang diekspor kini menyertakan file attrs.xml yang berisi nama peran warna untuk setiap warna kustom.
<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>
Di themes.xml, kita telah membuat empat peran warna untuk setiap warna kustom (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>). Properti harmonize<name> mencerminkan apakah developer telah memilih opsi di Material Theme Builder. Hal ini tidak akan mengubah warna di tema inti.
<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>
Dalam file colors.xml, warna dasar yang digunakan untuk membuat peran warna yang tercantum di atas ditentukan bersama dengan nilai boolean untuk menentukan apakah palet warna akan diubah atau tidak.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. Memeriksa Warna Kustom
Dengan memperbesar panel samping Material Theme Builder, kita dapat melihat bahwa penambahan warna kustom akan menampilkan panel dengan empat peran warna utama dalam palet terang dan gelap.

Di Android Views, kami mengekspor warna ini untuk Anda, tetapi di balik layar, warna ini dapat direpresentasikan oleh instance objek ColorRoles.
Class ColorRoles memiliki empat properti, accent, onAccent, accentContainer, dan onAccentContainer. Properti ini adalah representasi bilangan bulat dari empat warna heksadesimal.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
Anda dapat mengambil empat peran warna utama dari warna arbitrer saat runtime menggunakan getColorRoles di class MaterialColors yang disebut getColorRoles yang memungkinkan Anda membuat kumpulan empat peran warna tersebut saat runtime dengan warna dasar tertentu.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Demikian pula, nilai output adalah nilai warna sebenarnya, BUKAN pointer ke nilai tersebut.**
5. Apa yang dimaksud dengan Penyelarasan Warna?
Sistem warna baru Material dirancang secara algoritma, yang menghasilkan warna primer, sekunder, tersier, dan netral dari warna dasar tertentu. Salah satu poin kekhawatiran yang sering kami terima saat berbicara dengan partner internal dan eksternal adalah cara menerapkan warna dinamis sambil tetap mengontrol beberapa warna.
Warna ini sering kali memiliki makna atau konteks tertentu dalam aplikasi yang akan hilang jika diganti dengan warna acak. Atau, jika dibiarkan apa adanya, warna ini mungkin terlihat tidak serasi atau tidak sesuai.
Warna di Material You dijelaskan berdasarkan rona, kromatisitas, dan tone. Rona warna berkaitan dengan persepsi seseorang terhadap warna tersebut sebagai anggota dari satu rentang warna versus rentang warna lainnya. Tone menggambarkan seberapa terang atau gelapnya warna dan chroma adalah intensitas warna. Persepsi hue dapat dipengaruhi oleh faktor budaya dan bahasa, seperti tidak adanya kata untuk warna biru dalam budaya kuno yang sering disebutkan, yang menganggapnya sebagai warna yang sama dengan hijau.
Warna tertentu dapat dianggap hangat atau dingin, bergantung pada posisinya dalam spektrum warna. Pergeseran ke arah warna merah, oranye, atau kuning umumnya dianggap membuatnya lebih hangat, sedangkan pergeseran ke arah warna biru, hijau, atau ungu dikatakan membuatnya lebih dingin. Bahkan dalam warna hangat atau dingin, Anda akan memiliki nuansa hangat dan dingin. Di bawah, warna kuning "lebih hangat" memiliki nuansa oranye, sedangkan warna kuning "lebih dingin" lebih dipengaruhi oleh warna hijau. 
Algoritma harmonisasi warna memeriksa rona warna yang tidak diubah dan warna yang harus diharmonisasikan dengannya untuk menemukan rona yang harmonis tetapi tidak mengubah kualitas warna dasarnya. Pada grafik pertama, ada warna hijau, kuning, dan oranye yang kurang harmonis yang diplot pada spektrum. Pada grafik berikutnya, warna hijau dan oranye telah diselaraskan dengan warna kuning. Warna hijau baru lebih hangat dan warna oranye baru lebih dingin.
Warna oranye dan hijau telah berubah, tetapi masih dapat dianggap sebagai oranye dan hijau.

Jika Anda ingin mempelajari lebih lanjut beberapa keputusan desain, eksplorasi, dan pertimbangan, rekan saya, Ayan Daniels dan Andrew Lu, telah menulis postingan blog yang membahasnya secara lebih mendalam daripada bagian ini.
6. Menyelaraskan warna secara manual
Untuk menyelaraskan satu nada, ada dua fungsi di MaterialColors, harmonize, dan harmonizeWithPrimary.
harmonizeWithPrimary menggunakan Context sebagai cara untuk mengakses tema saat ini dan selanjutnya warna primer dari tema tersebut.
@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);
}
Untuk mengambil kumpulan empat nada, kita perlu melakukan sedikit lebih banyak.
Karena sudah memiliki warna sumber, kita perlu:
- menentukan apakah data tersebut harus diselaraskan,
- menentukan apakah kita berada dalam mode gelap, dan,
- menampilkan objek
ColorRolesyang diselaraskan atau tidak diselaraskan.
Menentukan apakah akan menyelaraskan atau tidak
Dalam tema yang diekspor dari Material Theme Builder, kami menyertakan atribut boolean menggunakan nomenklatur harmonize<Color>. Di bawah ini adalah fungsi praktis untuk mengakses nilai tersebut.
Jika ditemukan, nilai akan ditampilkan; jika tidak, warna tidak akan diselaraskan.
// 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
}
Membuat objek ColorRoles yang Diharmoniskan
retrieveHarmonizedColorRoles adalah fungsi praktis lain yang menggabungkan semua langkah yang disebutkan di atas: mengambil nilai warna untuk resource bernama, mencoba menyelesaikan atribut boolean untuk menentukan harmonisasi, dan menampilkan objek ColorRoles yang berasal dari warna asli atau campuran (diberikan skema terang atau gelap).
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. Mengisi kartu transportasi umum
Seperti yang disebutkan sebelumnya, kita akan menggunakan RecyclerView dan adaptor untuk mengisi dan mewarnai kumpulan kartu transportasi umum.

Menyimpan data Transit
Untuk menyimpan data teks dan informasi warna kartu transportasi umum, kita menggunakan class data yang menyimpan nama, tujuan, dan ID resource warna.
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)
)
Kita akan menggunakan warna ini untuk menghasilkan nuansa yang kita butuhkan secara real-time.
Anda dapat menyelaraskan saat runtime dengan fungsi onBindViewHolder berikut.
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. Menyelaraskan warna secara otomatis
Sebagai alternatif untuk menangani harmonisasi secara manual, Anda dapat meminta kami menanganinya untuk Anda. HarmonizedColorOptions adalah class builder yang memungkinkan Anda menentukan sebagian besar hal yang telah kita lakukan secara manual sejauh ini.
Setelah mengambil konteks saat ini sehingga Anda memiliki akses ke skema dinamis saat ini, Anda perlu menentukan warna dasar yang ingin diselaraskan dan membuat konteks baru berdasarkan objek HarmonizedColorOptions tersebut dan konteks yang mengaktifkan DynamicColors.
Jika Anda tidak ingin menyelaraskan warna, cukup jangan sertakan warna tersebut dalam harmonizedOptions.
val newContext = DynamicColors.wrapContextIfAvailable(requireContext())
val harmonizedOptions = HarmonizedColorsOptions.Builder()
.setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
.build();
harmonizedContext =
HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)
Dengan warna dasar yang diselaraskan sudah ditangani, Anda dapat memperbarui onBindViewHolder untuk cukup memanggil MaterialColors.getColorRoles dan menentukan apakah peran yang ditampilkan harus terang atau gelap.
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. Menyelaraskan atribut tema secara otomatis
Metode yang ditampilkan hingga saat ini mengandalkan pengambilan peran warna dari setiap warna. Hal ini bagus untuk menunjukkan bahwa nada suara yang dihasilkan adalah nada suara asli, tetapi tidak realistis untuk sebagian besar aplikasi yang ada. Anda mungkin tidak mendapatkan warna secara langsung, tetapi menggunakan atribut tema yang ada.
Sebelumnya dalam codelab ini, kita telah membahas cara mengekspor atribut 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>
Mirip dengan metode otomatis pertama, kita dapat memberikan nilai ke HarmonizedColorOptions dan menggunakan HarmonizedColors untuk mengambil Context dengan warna yang diharmoniskan. Ada satu perbedaan utama antara kedua metode tersebut. Selain itu, kita perlu menyediakan overlay tema yang berisi kolom yang akan diselaraskan.
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)
Adaptor Anda akan menggunakan konteks yang diharmonisasi. Nilai dalam overlay tema harus merujuk pada varian terang atau gelap yang tidak diselaraskan.
<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>
Di dalam file tata letak XML, kita dapat menggunakan atribut yang diselaraskan tersebut seperti biasa.
<?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. Kode Sumber
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. Contoh UI
Tema Default dan Warna Kustom tanpa Harmonisasi

Warna Kustom yang Diharmoniskan


12. Ringkasan
Dalam codelab ini, Anda telah mempelajari:
- Dasar-dasar algoritma harmonisasi warna kami
- Cara membuat peran warna dari warna yang terlihat.
- Cara menyelaraskan warna secara selektif di antarmuka pengguna.
- Cara menyelaraskan sekumpulan atribut dalam tema.
Jika ada pertanyaan, silakan hubungi kami kapan saja menggunakan @MaterialDesign di Twitter.
Nantikan konten desain dan tutorial lainnya di youtube.com/MaterialDesign