1. 시작하기 전에
이 Codelab에서는 동적 테마에서 생성된 색상과 맞춤 색상을 조화시키는 방법을 알아봅니다.
기본 요건
개발자는 다음을 수행할 수 있어야 합니다.
- Android의 기본 테마 설정 개념에 익숙함
- Android 위젯 뷰 및 속성으로 작업하는 데 익숙함
학습할 내용
- 여러 가지 방법을 사용하여 애플리케이션에서 색상 조화를 사용하는 방법
- 조화가 작동하는 방식과 색상을 이동하는 방법
필요한 항목
- 따라 하려면 Android가 설치된 컴퓨터가 필요합니다.
2. 앱 개요
Voyaĝi는 이미 동적 테마를 사용하는 대중교통 애플리케이션입니다. 많은 대중교통 시스템에서 색상은 기차, 버스 또는 트램의 중요한 지표이며 사용 가능한 동적 기본 색상, 보조 색상 또는 3차 색상으로 대체할 수 없습니다. 색상이 지정된 대중교통 카드의 RecyclerView에 중점을 두겠습니다.

3. 테마 생성
Material3 테마를 만들려면 Material Theme Builder 도구를 먼저 사용하는 것이 좋습니다. 이제 맞춤 탭에서 테마에 색상을 더 추가할 수 있습니다. 오른쪽에 이러한 색상의 색상 역할과 색조 팔레트가 표시됩니다.
확장된 색상 섹션에서 색상을 삭제하거나 이름을 바꿀 수 있습니다.

내보내기 메뉴에 가능한 내보내기 옵션이 여러 개 표시됩니다. 작성 시 Material Theme Builder의 조화 설정 특별 처리는 Android 뷰에서만 사용할 수 있습니다.

새 내보내기 값 이해
조화를 선택하든 선택하지 않든 테마에서 이러한 색상과 연결된 색상 역할을 사용할 수 있도록 내보낸 다운로드에는 이제 각 맞춤 색상의 색상 역할 이름이 포함된 attrs.xml 파일이 포함됩니다.
<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>
themes.xml에서 각 맞춤 색상 (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>)의 4가지 색상 역할을 생성했습니다. harmonize<name> 속성은 개발자가 Material Theme Builder에서 옵션을 선택했는지 여부를 반영합니다. 핵심 테마의 색상은 이동하지 않습니다.
<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>
colors.xml 파일에서 위에 나열된 색상 역할을 생성하는 데 사용되는 시드 색상은 색상의 팔레트가 이동되는지 여부에 대한 불리언 값과 함께 지정됩니다.
<resources>
<!-- other colors used in theme -->
<color name="custom1">#1AC9E0</color>
<color name="custom2">#32D312</color>
</resources>
4. 맞춤 색상 검사
Material Theme Builder의 측면 패널을 확대하면 맞춤 색상을 추가하면 밝은 팔레트와 어두운 팔레트에 4가지 핵심 색상 역할이 있는 패널이 표시됩니다.

Android 뷰에서는 이러한 색상을 내보내지만 백그라운드에서 ColorRoles 객체의 인스턴스로 표현될 수 있습니다.
ColorRoles 클래스에는 4가지 속성(accent, onAccent, accentContainer, onAccentContainer)이 있습니다. 이러한 속성은 4가지 16진수 색상의 정수 표현입니다.
public final class ColorRoles {
private final int accent;
private final int onAccent;
private final int accentContainer;
private final int onAccentContainer;
// truncated code
}
특정 시드 색상이 지정된 경우 런타임에 4가지 색상 역할을 만들 수 있는 getColorRoles라는 MaterialColors 클래스의 getColorRoles를 사용하여 런타임에 임의의 색상에서 4가지 핵심 색상 역할을 가져올 수 있습니다.
public static ColorRoles getColorRoles(
@NonNull Context context,
@ColorInt int color
) { /* implementation */ }
마찬가지로 출력 값은 실제 색상 값이며 포인터가 아닙니다.**
5. 색상 조화란 무엇인가요?
Material의 새로운 색상 시스템은 알고리즘으로 설계되어 있으며 지정된 시드 색상에서 기본 색상, 보조 색상, 3차 색상, 중립 색상을 생성합니다. 내부 및 외부 파트너와 대화할 때 많이 받은 우려 사항 중 하나는 일부 색상을 제어하면서 동적 색상을 수용하는 방법이었습니다.
이러한 색상은 애플리케이션에서 특정 의미 또는 컨텍스트를 전달하는 경우가 많으며 임의의 색상으로 대체되면 손실됩니다. 또는 그대로 두면 이러한 색상이 시각적으로 거슬리거나 어색해 보일 수 있습니다.
Material You의 색상은 색조, 채도, 색조로 설명됩니다. 색상의 색조는 한 색상 범위의 구성원으로서 다른 색상 범위와 비교하여 색상을 인식하는 것과 관련이 있습니다. 색조는 밝거나 어둡게 보이는 방식을 설명하고 채도는 색상의 강도입니다. 색조 인식은 문화적, 언어적 요인에 영향을 받을 수 있습니다. 예를 들어 고대 문화에는 파란색을 나타내는 단어가 없으며 대신 녹색과 같은 계열로 간주되는 경우가 많습니다.
특정 색조는 색조 스펙트럼에서 위치에 따라 따뜻하거나 차갑게 간주될 수 있습니다. 빨간색, 주황색 또는 노란색 색조로 이동하면 일반적으로 더 따뜻해지고 파란색, 녹색 또는 보라색으로 이동하면 더 차가워진다고 합니다. 따뜻한 색상이나 차가운 색상 내에서도 따뜻한 색조와 차가운 색조가 있습니다. 아래에서 '따뜻한' 노란색은 주황색 색조가 더 강한 반면 '차가운' 노란색은 녹색의 영향을 더 많이 받습니다. 
색상 조화 알고리즘은 이동되지 않은 색상의 색조와 조화되어야 하는 색상을 검사하여 조화되지만 기본 색상 품질을 변경하지 않는 색조를 찾습니다. 첫 번째 그래픽에는 스펙트럼에 조화되지 않은 녹색, 노란색, 주황색 색조가 표시되어 있습니다. 다음 그래픽에서는 녹색과 주황색이 노란색 색조와 조화되었습니다. 새로운 녹색은 더 따뜻하고 새로운 주황색은 더 차갑습니다.
주황색과 녹색의 색조가 이동되었지만 여전히 주황색과 녹색으로 인식될 수 있습니다.

디자인 결정, 탐색, 고려사항에 관해 자세히 알아보려면 동료인 Ayan Daniels와 Andrew Lu가 이 섹션보다 약간 더 자세히 설명하는 블로그 게시물을 작성했습니다.
6. 수동으로 색상 조화
단일 색조를 조화시키려면 MaterialColors, harmonize 및 harmonizeWithPrimary에 두 가지 함수가 있습니다.
harmonizeWithPrimary는 Context를 사용하여 현재 테마에 액세스하고 그 후 기본 색상에 액세스합니다.
@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);
}
4가지 색조 집합을 가져오려면 약간 더 많은 작업을 해야 합니다.
소스 색상이 이미 있으므로 다음을 실행해야 합니다.
- 조화시켜야 하는지 확인
- 어두운 모드인지 확인
- 조화된
ColorRoles객체 또는 조화되지 않은ColorRoles객체 반환
조화 여부 결정
Material Theme Builder에서 내보낸 테마에는 harmonize<Color> 명명 규칙을 사용하는 불리언 속성이 포함되어 있습니다. 다음은 이 값에 액세스하는 편의 함수입니다.
찾은 경우 값을 반환하고, 그렇지 않으면 색상을 조화시키지 않아야 한다고 결정합니다.
// 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
}
조화된 ColorRoles 객체 만들기
retrieveHarmonizedColorRoles는 명명된 리소스의 색상 값 가져오기, 조화를 결정하기 위해 불리언 속성 확인 시도, 원래 색상 또는 혼합된 색상 (밝은 또는 어두운 구성표 제공)에서 파생된 ColorRoles 객체 반환 등 앞서 언급한 모든 단계를 결합하는 또 다른 편의 함수입니다.
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. 대중교통 카드 채우기
앞서 언급했듯이 RecyclerView와 어댑터를 사용하여 대중교통 카드 모음을 채우고 색상을 지정합니다.

대중교통 데이터 저장
대중교통 카드의 텍스트 데이터와 색상 정보를 저장하기 위해 이름, 대상, 색상 리소스 ID를 저장하는 데이터 클래스를 사용합니다.
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)
)
이 색상을 사용하여 필요한 색조를 실시간으로 생성합니다.
다음 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. 색상 자동 조화
조화를 수동으로 처리하는 대신 조화를 처리하도록 할 수 있습니다. HarmonizedColorOptions는 지금까지 수동으로 수행한 작업을 대부분 지정할 수 있는 빌더 클래스입니다.
현재 동적 구성표에 액세스할 수 있도록 현재 컨텍스트를 가져온 후 조화시키려는 기본 색상을 지정하고 HarmonizedColorOptions 객체와 DynamicColors 사용 설정 컨텍스트를 기반으로 새 컨텍스트를 만들어야 합니다.
색상을 조화시키지 않으려면 harmonizedOptions에 포함하지 마세요.
val newContext = DynamicColors.wrapContextIfAvailable(requireContext())
val harmonizedOptions = HarmonizedColorsOptions.Builder()
.setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
.build();
harmonizedContext =
HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)
조화된 기본 색상이 이미 처리되었으므로 MaterialColors.getColorRoles를 호출하고 반환된 역할이 밝은지 어두운지 지정하도록 onBindViewHolder를 업데이트할 수 있습니다.
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. 테마 속성 자동 조화
지금까지 표시된 메서드는 개별 색상에서 색상 역할을 가져오는 데 의존합니다. 실제 색조가 생성되고 있음을 보여주는 데는 좋지만 대부분의 기존 애플리케이션에는 현실적이지 않습니다. 색상을 직접 파생하지 않고 기존 테마 속성을 사용할 가능성이 높습니다.
이 Codelab 앞부분에서 테마 속성 내보내기에 관해 설명했습니다.
<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>
첫 번째 자동 메서드와 마찬가지로 HarmonizedColorOptions에 값을 제공하고 HarmonizedColors를 사용하여 조화된 색상이 있는 컨텍스트를 가져올 수 있습니다. 두 메서드 간에는 한 가지 중요한 차이점이 있습니다. 조화시킬 필드가 포함된 테마 오버레이도 제공해야 합니다.
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)
어댑터는 조화된 컨텍스트를 사용합니다. 테마 오버레이의 값은 조화되지 않은 밝은 또는 어두운 변형을 참조해야 합니다.
<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>
XML 레이아웃 파일 내에서 이러한 조화된 속성을 정상적으로 사용할 수 있습니다.
<?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. 소스 코드
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. UI 예시
조화가 없는 기본 테마 설정 및 맞춤 색상

조화된 맞춤 색상


12. 요약
이 Codelab에서 배운 내용은 다음과 같습니다.
- 색상 조화 알고리즘의 기본사항
- 지정된 색상에서 색상 역할을 생성하는 방법
- 사용자 인터페이스에서 색상을 선택적으로 조화시키는 방법
- 테마에서 속성 집합을 조화시키는 방법
궁금한 사항은 언제든지 Twitter의 @MaterialDesign으로 문의해 주세요.
youtube.com/MaterialDesign에서 더 많은 디자인 콘텐츠 및 튜토리얼을 기대해 주세요.