-
Notifications
You must be signed in to change notification settings - Fork 0
안드로이드 테마 적용
해당 문서는 안드로이드 디자인 테마 적용을 위한 작업 과정에 대해 기록한 내용이다.
머티리얼 디자인은 Google 디자이너와 개발자가 구축하고 지원하는 디자인 시스템이다.
Material에는 Android, Flutter 및 웹을 위한 심층적인 UX 지침과 UI 구성요소 구현이 포함되어 있다.
쉽게 말해 안드로이드 앱들에게 권장하는 디자인 가이드라인이라는이다.
머티리얼 디자인은 디자인 권장사항, 일반 규칙, 시각적 요소가 포함되어 있어 어느 기기에서나 친근한 분위기를 조성할 수 있는 생생한 앱을 구축할 수 있습니다.
사용자가 이 같은 시각적 언어에 익숙해지면 이러한 디자인을 기대하게 됩니다.
이 디자인을 따르면 곧바로 사용자의 눈을 사로잡는 앱을 만드는 동시에 사용성을 향상하고 사용자 참여도와 유지율을 개선할 수 있습니다.
구글에서는 다음과 같은 이유로 앱에 Material Design을 사용하길 권장하고 있다.
요약하자면 다음과 같다.
- 이미 많은 앱이 사용하는 표준이므로 사용자에게 친근감을 주고 사용성을 향상시킨다.
- 디자이너와 개발자 간 소통에 모호함이 사라진다.
- 지속적으로 업데이트되고 있어 자동으로 트랜드를 반영할 수 있다.
프로젝트 디자인 단계에서부터 Material Design을 반영하기로 했기 때문에, 피그마의 여러 플러그인을 사용하였다.
Materal Design은 쉽게 컬러 팔레트를 구성할 수 있도록 미리 사용 가능한 색상들을 제시한다.
주조색과 보조색을 설정하여 Materal Design에서 제시하는 색상 값을 얻어 피그마에 적용할 수 있었다.
또한 Material Design에서는 Type Scale을 통해 사용할 폰트 스타일을 제시하는데,
디자인 과정에서 이에 맞게 모든 폰트 크기를 지정하였다.
마지막으로 해당 파일을 피그마 프로젝트에 추가해
Material Design에서 사용되는 컴포넌트들을 피그마에서 쉽게 적용할 수 있도록 하였다.
이와 같은 과정을 통해 디자이너 없이도 빠른 앱 디자인이 가능했고, 추후 UI 구현에 있어 소통 오류가 적었다.
상기한 플러그인 중 피그마에서 사용한 Material Theme Builder를 사용하면 다음과 같이 color값을 extract할 수 있다.
압축 파일로 color.xml과 theme.xml을 생성해주며, 자동으로 다크 모드 색상까지 추가하여 준다.
이를 안드로이드 프로젝트 폴더에 잘 옮겨 놓으면 일일히 컬러 값을 지정하지 않아도 된다.
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true"
tools:context=".ui.detail.DetailActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface">
Material 디자인은 시스템 상태에 따라 다양한 색상과 폰트 스타일 등을 제공한다.
이에 맞추기 위해서는 xml 파일에 색상 코드가 그대로 들어가면 안된다.
?attr 지정자를 사용하면 이 문제를 해결할 수 있다.
이 지정자를 사용하면 해당 속성 값은 현재 테마에 지정된 값을 사용하게 된다.
이를 통해 간결하게 현재 테마에 맞는 색상을 대응시킬 수 있다.
class ConfirmDialogFragment : DialogFragment() {
...
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
initDialogInfo()
return MaterialAlertDialogBuilder(
requireActivity(),
R.style.ThemeOverlay_App_MaterialAlertDialog
).apply {
...
xml이 아닌 코틀린 파일 내에서 직접 View나 Layout을 빌드할 경우, 앞에서 사용한 ?attr 지정자를 사용할 수 없게 된다.
이 경우, 빌더에서 style을 지정할 수 있어 여기에 적절한 style을 넣으면 시스템 상태에 맞는 디자인을 적용할 수 있다.
<style name="ThemeOverlay.App.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
<item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button</item>
<item name="buttonBarNegativeButtonStyle">@style/Widget.App.Button</item>
</style>
...
<style name="MaterialAlertDialog.App" parent="MaterialAlertDialog.Material3">
<item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
<item name="backgroundTint">?attr/colorSurfaceContainerHigh</item>
<item name="shapeAppearanceOverlay">@null</item>
</style>
<style name="MaterialAlertDialog.App.Title.Text" parent="MaterialAlertDialog.Material3.Title.Text">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="MaterialAlertDialog.App." parent="MaterialAlertDialog.Material3.Title.Text">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
해당 프로젝트의 경우 Material Design Component의 MaterialAlertDialog를 코드상에서 빌드한다.
Material 3 의 Component 페이지를 찾아보면 어떤 식으로 style을 지정해야 하는지 나와 있다.
xml 파일에서는 ?attr 지정자를 사용할 수 있으니 이제 런타임 빌드된 컴포넌트에도 테마 색상을 적용할 수 있다.
어두운 테마는 다음과 같은 여러 가지 장점이 있습니다.
- 전력 사용량을 상당히 절약할 수 있습니다(기기 화면 기술에 따라 다름).
- 시력이 낮은 사용자와 밝은 빛에 민감한 사용자를 위한 가시성을 개선합니다.
- 누구나 어두운 환경에서 쉽게 기기를 사용할 수 있습니다.
Android 10부터는 다크 테마가 지원된다.
따라서 사용자마다 상황에 맞게 다크 모드를 켜거나 끌 수 있어야 한다.
이러한 과제를 해결하기 위해서는 사용자가 원할 때마다 라이트 모드와 다크 모드를 오갈 수 있어야 한다.
추가로, Android 12부터는 다이나믹 컬러가 지원되어 이 역시 지원 가능하도록 해야 한다고 판단했다.
Dynamic Color는 사용자 배경화면 색이 앱에 쓰이는 색에 자연스럽게 입혀져 개성에 맞게 앱을 이용할 수 있는 기능이다.
우리는 마이페이지에서 '테마 변경' 메뉴를 선택 시 다음과 같은 다이얼로그를 통해 테마 설정을 변경할 수 있는 기능을 구현하려 했다.
private fun applyDynamicMode() = when (binding.rgDynamicColor.checkedRadioButtonId) {
R.id.rb_yes -> {
DynamicColors.applyToActivitiesIfAvailable(requireActivity().application)
PriceGuardApp.MODE_DYNAMIC
}
else -> {
DynamicColors.applyToActivitiesIfAvailable(
requireActivity().application,
DynamicColorsOptions.Builder()
.setThemeOverlay(R.style.Theme_PriceGuard).build()
)
PriceGuardApp.MODE_DYNAMIC_NO
}
}
private fun applyDarkMode() = when (binding.rgDarkMode.checkedRadioButtonId) {
R.id.rb_system -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
PriceGuardApp.MODE_SYSTEM
}
R.id.rb_light -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
PriceGuardApp.MODE_LIGHT
}
R.id.rb_dark -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
PriceGuardApp.MODE_DARK
}
else -> {
PriceGuardApp.MODE_SYSTEM
}
}
private fun checkDynamicThemeSupport() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Disable Dynamic Theme Radio Group
(0 until binding.rgDynamicColor.childCount).forEach { idx ->
binding.rgDynamicColor.getChildAt(idx).isEnabled = false
}
}
}
주요 로직은 다음과 같다.
- SDK 버전이 31 미만이라면 다이나믹 모드 설정 라디오버튼을 비활성화 시킨다.
- 다이나믹 모드가 설정되면
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
를 실행한다. - 다이나믹 모드가 해제되면 앱 테마를 대신 적용한다.
- 다크 모드가 활성화되었다면
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
를, 비활성화되었다면AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
를 실행한다. - 다크 모드가 시스템 값을 따라가도록 설정한다면
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
를 실행한다.
private fun saveTheme(dynamicMode: Int, darkMode: Int) {
lifecycleScope.launch(Dispatchers.IO) {
configDataSource.saveDynamicMode(dynamicMode)
configDataSource.saveDarkMode(darkMode)
}
}
...
override suspend fun saveDynamicMode(mode: Int) {
dataStore.edit { preferences ->
preferences[dynamicMode] = mode
}
}
override suspend fun saveDarkMode(mode: Int) {
dataStore.edit { preferences ->
preferences[darkMode] = mode
}
}
이대로만 하면 어플리케이션을 켤 때마다 사용자가 설정값을 바꿔야 하기에,
테마를 변경할 때마다 변경된 데이터를 저장하여 갖고 있다 최초 실행 시 적용하기로 하였다.
테마 변경 다이얼로그에서 모든 변경 사항에 대해 DataStore에 설정값을 저장한다.
저장하는 값은 Application 밑에 companion object로 설정한 상수로 두었다.
앱 내 모든 곳에서 사용되며 원자값을 저장하는 것이 쉽기 때문이다.
private fun initAppTheme() {
CoroutineScope(Dispatchers.IO).launch {
val dynamicColorMode = configDataSource.getDynamicMode()
val darkMode = configDataSource.getDarkMode()
when (dynamicColorMode) {
MODE_DYNAMIC -> {
DynamicColors.applyToActivitiesIfAvailable(this@PriceGuardApp)
}
}
when (darkMode) {
MODE_LIGHT -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
MODE_DARK -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
}
}
최초 앱 실행 시 Application onCreate 시 위 함수가 호출된다.
읽어온 데이터가 무엇이냐에 따라 테마를 적용하도록 하였다.
서브 모듈로 설정하여 개발한 그래프 오픈소스 라이브러리에서 사용자 테마를 받아 색상을 정의한 과정에 대해 기록하였다.
해당 링크 참고.
- https://developer.android.com/distribute/best-practices/develop/use-material-design?hl=ko
- https://medium.com/@sunminlee89/material-design%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-a755341ea37b
- https://m3.material.io/styles/color/system/how-the-system-works
- https://www.figma.com/community/plugin/1034969338659738588
- https://www.figma.com/community/file/1035203688168086460
- https://developer.android.com/guide/topics/ui/look-and-feel/darktheme?hl=ko