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

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

내보내기 메뉴에 가능한 내보내기 옵션이 여러 개 표시됩니다. 작성 시점에는 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 테마 빌더에서 옵션을 선택했는지 여부를 반영합니다. 핵심 테마의 색상은 변경되지 않습니다.
<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 빌더의 측면 패널을 확대하면 맞춤 색상을 추가하면 밝은 팔레트와 어두운 팔레트에 4가지 주요 색상 역할이 있는 패널이 표시됩니다.

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

디자인 결정, 탐색, 고려사항에 대해 자세히 알아보려면 동료인 아얀 다니엘스와 앤드류 루가 작성한 이 섹션보다 좀 더 심층적인 블로그 게시물을 참고하세요.
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 빌더에서 내보낸 테마에는 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)
조화된 기본 색상이 이미 처리되었으므로 onBindViewHolder를 업데이트하여 MaterialColors.getColorRoles를 호출하고 반환된 역할이 밝은지 어두운지 지정할 수 있습니다.
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에서 더 많은 디자인 콘텐츠 및 튜토리얼을 기대해 주세요.