1. Sebelum memulai
Dalam codelab ini, Anda akan mempelajari cara menyelaraskan warna kustom dengan warna yang dihasilkan oleh tema dinamis.
Prasyarat
Pengembang seharusnya
- Pemahaman tentang konsep tema dasar di Android
- Nyaman bekerja dengan Tampilan widget Android dan propertinya
Yang akan Anda pelajari
- Cara menggunakan harmonisasi warna di aplikasi Anda menggunakan beberapa metode
- Cara kerja harmonisasi dan perubahan warnanya
Yang Anda butuhkan
- Komputer yang dilengkapi Android jika Anda ingin mengikutinya.
2. Ringkasan Aplikasi
Voya lapisani adalah aplikasi transportasi umum yang sudah menggunakan tema dinamis. Untuk banyak sistem transportasi umum, warna merupakan indikator penting untuk kereta, bus, atau trem, dan warna ini tidak dapat diganti dengan 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 Material Theme Builder kami sebagai perhentian pertama Anda untuk membuat tema Material3. Pada tab khusus, sekarang Anda dapat menambahkan lebih banyak warna ke tema Anda. Di sebelah kanan, Anda akan melihat peran warna dan palet tonal untuk warna-warna tersebut.
Di bagian warna yang diperluas, Anda dapat menghapus atau mengganti nama warna.
Menu ekspor akan menampilkan sejumlah kemungkinan opsi ekspor. Pada saat penulisan, penanganan khusus setelan harmonisasi Material Theme Builder hanya tersedia di Tampilan Android
Memahami nilai ekspor baru
Agar Anda dapat menggunakan warna ini dan peran warna yang terkait dalam tema Anda, terlepas dari apakah Anda memilih untuk harmonisasi 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>
menunjukkan apakah developer telah memilih opsi di Builder Tema Material. Warnanya tidak akan berubah dalam 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 seed yang digunakan untuk menghasilkan peran warna yang tercantum di atas ditentukan bersama dengan nilai boolean untuk mengetahui apakah palet warna akan digeser 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 menambahkan warna kustom akan memunculkan panel dengan empat peran warna utama dalam palet terang dan gelap.
Di Android View, kita mengekspor warna-warna ini untuk Anda, tetapi di balik layar, warna-warna ini dapat diwakili 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 seed tertentu.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
Demikian juga nilai output adalah nilai warna sebenarnya, BUKAN pointer ke nilai tersebut.**
5. Apa itu Harmonisasi Warna?
Sistem warna baru Material dirancang berdasarkan algoritma, yang menghasilkan warna primer, sekunder, tersier, dan netral dari warna benih tertentu. Salah satu poin kekhawatiran yang banyak kami terima ketika berbicara dengan partner internal dan eksternal adalah bagaimana menggunakan warna dinamis sambil tetap mengontrol beberapa warna.
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-warna ini mungkin terlihat berantakan atau tidak pada tempatnya.
Warna pada Material You dijelaskan melalui rona, kroma, dan nuansa. Rona warna berkaitan dengan persepsi seseorang tentang warna tersebut sebagai bagian dari satu rentang warna versus yang lain. Tone menggambarkan bagaimana terang atau gelap muncul dan kroma adalah intensitas warna. Persepsi terhadap hue dapat dipengaruhi oleh faktor budaya dan linguistik, seperti kurangnya kata untuk biru dalam budaya kuno, tetapi tidak terlihat di keluarga yang sama dengan warna hijau.
Rona tertentu dapat dianggap hangat atau sejuk bergantung pada posisinya di spektrum hue. Pergeseran ke rona merah, oranye, atau kuning umumnya dianggap membuat warnanya lebih hangat dan ke arah biru, hijau, atau ungu dikatakan membuat warnanya lebih dingin. Meski dalam warna hangat atau dingin, Anda akan memiliki nuansa hangat dan dingin. Di bawah ini, "lebih hangat" kuning lebih berwarna oranye sedangkan "lebih dingin" kuning lebih dipengaruhi oleh warna hijau.
Algoritma harmonisasi warna memeriksa hue dari warna yang tidak bergeser dan warna yang harus diselaraskan untuk menemukan hue yang harmonis tetapi tidak mengubah kualitas warna yang mendasarinya. Pada grafik pertama, terdapat rona hijau, kuning, dan oranye yang kurang harmonis yang diplot pada spektrum. Pada grafik berikutnya, hijau dan oranye telah diselaraskan dengan rona kuning. Warna hijau baru lebih hangat dan oranye baru lebih keren.
Rona telah bergeser menjadi oranye dan hijau, tetapi masih dapat dianggap sebagai oranye dan hijau.
Jika Anda ingin mempelajari lebih lanjut tentang beberapa keputusan desain, eksplorasi, dan pertimbangan, rekan saya Ayan Daniels dan Andrew Lu telah menulis postingan blog yang sedikit lebih mendalam daripada bagian ini.
6. Menyelaraskan warna secara manual
Untuk menyelaraskan nada tunggal, ada dua fungsi di MaterialColors
, harmonize
, dan harmonizeWithPrimary
.
harmonizeWithPrimary
menggunakan Context
sebagai cara untuk mengakses tema saat ini dan kemudian warna utama darinya.
@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 set empat nada, kita perlu melakukan lebih banyak lagi.
Jika sudah memiliki warna sumber, kita harus:
- menentukan apakah harus diselaraskan,
- menentukan apakah kita berada dalam mode gelap, dan,
- menampilkan objek
ColorRoles
yang diselaraskan atau tidak diselaraskan.
Menentukan apakah akan menyelaraskan
Dalam tema yang diekspor dari Material Theme Builder, kami menyertakan atribut boolean menggunakan nomenklatur harmonize<Color>
. Berikut adalah fungsi kenyamanan untuk mengakses nilai tersebut.
Jika ditemukan, kode akan menampilkan nilainya; yang lainnya, ia menentukan bahwa warna tidak seharusnya selaras.
// 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
Harmonized
retrieveHarmonizedColorRoles
adalah fungsi praktis lain yang menggabungkan semua langkah yang disebutkan di atas: mengambil nilai warna untuk resource yang dinamai, mencoba me-resolve atribut boolean untuk menentukan harmonisasi, dan menampilkan objek ColorRoles
yang berasal dari warna asli atau campuran (berdasarkan 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 multi-trip
Seperti yang disebutkan sebelumnya, kita akan menggunakan RecyclerView dan adaptor untuk mengisi dan mewarnai kumpulan kartu transportasi umum.
Menyimpan data Transportasi Umum
Untuk menyimpan data teks dan informasi warna untuk 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 nada yang kita perlukan secara real-time.
Anda dapat melakukan harmonisasi 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 melakukannya sendiri. HarmonizedColorOptions adalah class builder yang memungkinkan Anda menentukan banyak hal yang telah kita lakukan sejauh ini secara manual.
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 dan konteks yang mengaktifkan DynamicColors.
Jika Anda tidak ingin menyelaraskan warna, cukup jangan sertakan 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)
Karena warna dasar yang diselaraskan sudah ditangani, Anda dapat memperbarui onBindViewHolder untuk memanggil MaterialColors.getColorRoles
serta 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 bergantung pada pengambilan peran warna dari masing-masing warna. Cara ini sangat bagus untuk menunjukkan bahwa nada yang sebenarnya sedang dibuat, tetapi tidak realistis untuk sebagian besar aplikasi yang sudah ada. Anda mungkin tidak akan mendapatkan warna secara langsung, tetapi menggunakan atribut tema yang sudah 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 diselaraskan. Ada satu perbedaan utama antara kedua metode tersebut. Kita juga 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 selaras. Nilai dalam overlay tema harus merujuk pada varian terang atau gelap yang tidak harmonis.
<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 Khusus yang Terselaraskan
12. Ringkasan
Dalam codelab ini, Anda telah mempelajari:
- Dasar-dasar algoritma harmonisasi warna kami
- Cara membuat peran warna dari warna tertentu yang terlihat.
- Cara menyelaraskan warna secara selektif dalam antarmuka pengguna.
- Cara menyelaraskan kumpulan 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