Skip to content

안드로이드 테마 적용

EunhoKang edited this page Dec 13, 2023 · 7 revisions

해당 문서는 안드로이드 디자인 테마 적용을 위한 작업 과정에 대해 기록한 내용이다.

Material Design

머티리얼 디자인은 Google 디자이너와 개발자가 구축하고 지원하는 디자인 시스템이다.

Material에는 Android, Flutter 및 웹을 위한 심층적인 UX 지침과 UI 구성요소 구현이 포함되어 있다.

쉽게 말해 안드로이드 앱들에게 권장하는 디자인 가이드라인이라는이다.

머티리얼 디자인은 디자인 권장사항, 일반 규칙, 시각적 요소가 포함되어 있어 어느 기기에서나 친근한 분위기를 조성할 수 있는 생생한 앱을 구축할 수 있습니다. 
사용자가 이 같은 시각적 언어에 익숙해지면 이러한 디자인을 기대하게 됩니다. 
이 디자인을 따르면 곧바로 사용자의 눈을 사로잡는 앱을 만드는 동시에 사용성을 향상하고 사용자 참여도와 유지율을 개선할 수 있습니다.

구글에서는 다음과 같은 이유로 앱에 Material Design을 사용하길 권장하고 있다.

요약하자면 다음과 같다.

  1. 이미 많은 앱이 사용하는 표준이므로 사용자에게 친근감을 주고 사용성을 향상시킨다.
  2. 디자이너와 개발자 간 소통에 모호함이 사라진다.
  3. 지속적으로 업데이트되고 있어 자동으로 트랜드를 반영할 수 있다.

피그마

image

프로젝트 디자인 단계에서부터 Material Design을 반영하기로 했기 때문에, 피그마의 여러 플러그인을 사용하였다.

Materal Design은 쉽게 컬러 팔레트를 구성할 수 있도록 미리 사용 가능한 색상들을 제시한다.

Material Theme Builder를 통해

주조색과 보조색을 설정하여 Materal Design에서 제시하는 색상 값을 얻어 피그마에 적용할 수 있었다.

또한 Material Design에서는 Type Scale을 통해 사용할 폰트 스타일을 제시하는데,

디자인 과정에서 이에 맞게 모든 폰트 크기를 지정하였다.

image

마지막으로 해당 파일을 피그마 프로젝트에 추가해

Material Design에서 사용되는 컴포넌트들을 피그마에서 쉽게 적용할 수 있도록 하였다.

이와 같은 과정을 통해 디자이너 없이도 빠른 앱 디자인이 가능했고, 추후 UI 구현에 있어 소통 오류가 적었다.

Android

컬러 팔레트 적용

image

상기한 플러그인 중 피그마에서 사용한 Material Theme Builder를 사용하면 다음과 같이 color값을 extract할 수 있다.

압축 파일로 color.xml과 theme.xml을 생성해주며, 자동으로 다크 모드 색상까지 추가하여 준다.

이를 안드로이드 프로젝트 폴더에 잘 옮겨 놓으면 일일히 컬러 값을 지정하지 않아도 된다.

?attr 지정자

<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는 사용자 배경화면 색이 앱에 쓰이는 색에 자연스럽게 입혀져 개성에 맞게 앱을 이용할 수 있는 기능이다.

image

우리는 마이페이지에서 '테마 변경' 메뉴를 선택 시 다음과 같은 다이얼로그를 통해 테마 설정을 변경할 수 있는 기능을 구현하려 했다.

    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
            }
        }
    }

주요 로직은 다음과 같다.

  1. SDK 버전이 31 미만이라면 다이나믹 모드 설정 라디오버튼을 비활성화 시킨다.
  2. 다이나믹 모드가 설정되면 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)를 실행한다.
  3. 다이나믹 모드가 해제되면 앱 테마를 대신 적용한다.
  4. 다크 모드가 활성화되었다면 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)를, 비활성화되었다면 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)를 실행한다.
  5. 다크 모드가 시스템 값을 따라가도록 설정한다면 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 시 위 함수가 호출된다.

읽어온 데이터가 무엇이냐에 따라 테마를 적용하도록 하였다.

그래프 라이브러리에서 테마 적용

서브 모듈로 설정하여 개발한 그래프 오픈소스 라이브러리에서 사용자 테마를 받아 색상을 정의한 과정에 대해 기록하였다.

해당 링크 참고.

출처

Clone this wiki locally