Material 3을 사용하는 Compose의 테마 설정

1. 소개

이 Codelab에서는 Material Design 3을 사용하여 Jetpack Compose에서 앱의 테마를 설정하는 방법을 알아봅니다. Material Design 3 색 구성표, 서체, 도형의 주요 구성요소에 관해서도 알아볼 수 있습니다. 맞춤설정이 가능하고 접근성이 높은 방식으로 애플리케이션의 테마를 설정하는 데 도움이 됩니다.

또한 다양한 수준의 강조와 함께 동적 테마 설정에 대한 지원도 살펴봅니다.

학습할 내용

이 Codelab에서는 다음에 관해 알아봅니다.

  • Material 3 테마 설정의 주요 측면
  • Material 3 색 구성표, 앱의 테마를 생성하는 방법
  • 앱에 동적 테마 및 밝은/어두운 테마를 지원하는 방법
  • 서체 및 도형으로 앱 맞춤설정
  • Material 3 구성요소 및 맞춤설정을 통한 앱 스타일 지정

빌드할 항목

이 Codelab에서는 Reply라는 이메일 클라이언트 앱의 테마를 설정합니다. 스타일이 지정되지 않은 애플리케이션으로 시작하여, 기준 테마를 사용하고 학습한 내용을 적용하여 애플리케이션의 테마를 설정하고 어두운 테마를 지원합니다.

d15db3dc75a9d00f.png

기준 테마가 있는 앱의 기본 시작점

색 구성표, 서체, 도형으로 테마를 만든 다음 앱의 이메일 목록과 세부정보 페이지에 적용합니다. 앱에 동적 테마 지원도 추가합니다. Codelab을 마치면 앱의 색상과 동적 테마를 모두 지원하게 됩니다.

Material 3 라이트

밝은 색상 테마 설정과 밝은 동적 테마 설정을 사용하는 테마 설정 Codelab의 끝점

Material 3 다크

어두운 색상 테마 설정과 어두운 동적 테마 설정을 사용하는 테마 설정 Codelab의 끝점

필요한 항목

2. 설정

이 단계에서는 이 Codelab에서 스타일을 지정할 Reply 앱의 전체 코드를 다운로드합니다.

코드 가져오기

이 Codelab의 코드는 codelab-android-compose GitHub 저장소에서 확인할 수 있습니다. 클론하려면 다음을 실행합니다.

$ git clone https://github.com/android/codelab-android-compose

또는 ZIP 파일 두 개를 다운로드해도 됩니다.

샘플 앱 확인

다운로드한 코드에는 사용 가능한 모든 Compose Codelab용 코드가 포함되어 있습니다. 이 Codelab을 완료하려면 Android 스튜디오 내에서 ThemingCodelab 프로젝트를 엽니다.

기본 브랜치로 시작하고 각자의 속도에 맞게 Codelab을 단계별로 따라하는 것이 좋습니다. 언제든지 프로젝트의 git 브랜치를 변경하여 Android 스튜디오에서 두 버전 중 하나를 실행할 수 있습니다.

시작 코드 살펴보기

main 코드에는 상호작용할 다음과 같은 기본 패키지와 파일이 있는 UI 패키지가 포함되어 있습니다.

  • MainActivity.kt: Reply 앱을 시작하는 진입점 활동입니다.
  • com.example.reply.ui.theme: 이 패키지에는 테마, 서체, 색 구성표가 포함됩니다. 이 패키지에 Material Theming 설정을 추가합니다.
  • com.example.reply.ui.components: 목록 항목, 앱 바와 같은 앱의 맞춤 구성요소가 포함됩니다. 이러한 구성요소에 테마를 적용합니다.
  • ReplyApp.kt: UI 트리가 시작되는 기본 구성 가능한 함수입니다. 이 파일에 최상위 테마 설정을 적용합니다.

이 Codelab에서는 ui 패키지 파일에 중점을 둡니다.

3. Material 3 Theming

Jetpack Compose는 디지털 인터페이스를 만들기 위한 포괄적인 디자인 시스템인 Material Design 구현을 제공합니다. Material Design 구성요소(버튼, 카드, 스위치 등)는 제품 브랜드를 효과적으로 반영하기 위해 Material Design을 체계적으로 맞춤설정하는 Material Theming을 기반으로 빌드됩니다.

Material 3 테마는 앱에 테마 설정을 추가하는 색 구성표, 서체, 도형 등의 하위 시스템으로 구성됩니다. 이러한 값을 맞춤설정하면 앱을 빌드하는 데 사용되는 M3 구성요소에 변경사항이 자동으로 반영됩니다. 각 하위 시스템을 자세히 알아보고 샘플 앱에 구현해 보겠습니다.

Material Design의 하위 시스템: 색상, 서체, 도형

Material 3 하위 시스템: 색상, 서체, 도형

4. 색 구성표

색 구성표의 기반은 각각 Material 3 구성요소에 사용되는 13가지 색조의 색조 팔레트와 관련이 있는 5가지 주요 색상의 세트입니다.

M3 테마를 만들기 위한 5개의 주요 기준 색상

M3 테마를 만들기 위한 5개의 주요 기준 색상

각 강조 색상(기본 색상, 보조 색상, 3차 색상)이 페어링, 강조 정의, 시각적 표현을 위한 다양한 색조의 4가지 호환되는 색상으로 제공됩니다.

기본, 보조, 3차 기준 강조 색상의 4가지 색조 색상

기본, 보조, 3차 기준 강조 색상의 4가지 색조 색상

마찬가지로 중성색 또한 표면과 배경에 사용되는 4가지 호환 색조로 나뉩니다. 표면에 배치되는 텍스트 아이콘을 강조하는 데도 중요합니다.

기준 중성색의 4가지 색조 색상

기준 중성색의 4가지 색조 색상

색 구성표 및 색상 역할에 관해 자세히 알아보세요.

색 구성표 생성

맞춤 ColorScheme을 수동으로 만들 수도 있지만 브랜드의 소스 색상을 사용하여 생성하는 것이 더 쉬울 때가 많습니다. 머티리얼 테마 빌더 도구를 사용하면 이 작업을 실행할 수 있고 선택적으로 Compose 테마 설정 코드를 내보낼 수 있습니다.

원하는 색상을 선택할 수 있지만 이 사용 사례에서는 기본적으로 제공되는 Reply 기본 색상(#825500)을 사용합니다. 왼쪽의 핵심 색상 섹션에서 기본 색상을 클릭하고 색상 선택 도구에 코드를 추가합니다.

294f73fc9d2a570e.png

머티리얼 테마 빌더에서 기본 색상 코드 추가

머티리얼 테마 빌더에서 기본 색상을 추가하면 다음 테마와 내보내기 옵션이 오른쪽 상단에 표시됩니다. 이 Codelab에서는 Jetpack Compose에서 테마를 내보냅니다.

오른쪽 상단에 내보내기 옵션이 있는 머티리얼 테마 빌더

오른쪽 상단에 내보내기 옵션이 있는 머티리얼 테마 빌더

기본 색상 #825500은 앱에 추가할 다음 테마를 생성합니다. Material 3에는 구성요소의 상태, 가시도, 강조를 유연하게 표현하는 다양한 색상 역할이 제공됩니다.

기본 색상에서 내보낸 밝은/어두운 색 구성표

기본 색상에서 내보낸 밝은/어두운 색 구성표

생성된 The Color.kt 파일에는 밝은 테마 색상과 어두운 테마 색상 모두에 정의된 모든 역할이 있는 테마 색상이 있습니다.

Color.kt

package com.example.reply.ui.theme
import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF825500)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
val md_theme_light_secondary = Color(0xFF6F5B40)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
val md_theme_light_tertiary = Color(0xFF51643F)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF1F1B16)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF1F1B16)
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
val md_theme_light_outline = Color(0xFF817567)
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
val md_theme_light_inverseSurface = Color(0xFF34302A)
val md_theme_light_inversePrimary = Color(0xFFFFB951)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF825500)
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFFFFB951)
val md_theme_dark_onPrimary = Color(0xFF452B00)
val md_theme_dark_primaryContainer = Color(0xFF633F00)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
val md_theme_dark_secondary = Color(0xFFDDC2A1)
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
val md_theme_dark_onTertiary = Color(0xFF243515)
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF1F1B16)
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
val md_theme_dark_surface = Color(0xFF1F1B16)
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
val md_theme_dark_outline = Color(0xFF9C8F80)
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
val md_theme_dark_inversePrimary = Color(0xFF825500)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
val md_theme_dark_scrim = Color(0xFF000000)

val seed = Color(0xFF825500)

생성된 The Theme.kt 파일에는 밝은 색 및 어두운 색 구성표와 앱 테마를 위한 설정이 포함되어 있습니다. 또한 기본 테마 설정 구성 가능한 함수 AppTheme()도 포함되어 있습니다.

Theme.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable

private val LightColors = lightColorScheme(
   primary = md_theme_light_primary,
   onPrimary = md_theme_light_onPrimary,
   primaryContainer = md_theme_light_primaryContainer,
   onPrimaryContainer = md_theme_light_onPrimaryContainer,
   secondary = md_theme_light_secondary,
   onSecondary = md_theme_light_onSecondary,
   secondaryContainer = md_theme_light_secondaryContainer,
   onSecondaryContainer = md_theme_light_onSecondaryContainer,
   tertiary = md_theme_light_tertiary,
   onTertiary = md_theme_light_onTertiary,
   tertiaryContainer = md_theme_light_tertiaryContainer,
   onTertiaryContainer = md_theme_light_onTertiaryContainer,
   error = md_theme_light_error,
   errorContainer = md_theme_light_errorContainer,
   onError = md_theme_light_onError,
   onErrorContainer = md_theme_light_onErrorContainer,
   background = md_theme_light_background,
   onBackground = md_theme_light_onBackground,
   surface = md_theme_light_surface,
   onSurface = md_theme_light_onSurface,
   surfaceVariant = md_theme_light_surfaceVariant,
   onSurfaceVariant = md_theme_light_onSurfaceVariant,
   outline = md_theme_light_outline,
   inverseOnSurface = md_theme_light_inverseOnSurface,
   inverseSurface = md_theme_light_inverseSurface,
   inversePrimary = md_theme_light_inversePrimary,
   surfaceTint = md_theme_light_surfaceTint,
   outlineVariant = md_theme_light_outlineVariant,
   scrim = md_theme_light_scrim,
)

private val DarkColors = darkColorScheme(
   primary = md_theme_dark_primary,
   onPrimary = md_theme_dark_onPrimary,
   primaryContainer = md_theme_dark_primaryContainer,
   onPrimaryContainer = md_theme_dark_onPrimaryContainer,
   secondary = md_theme_dark_secondary,
   onSecondary = md_theme_dark_onSecondary,
   secondaryContainer = md_theme_dark_secondaryContainer,
   onSecondaryContainer = md_theme_dark_onSecondaryContainer,
   tertiary = md_theme_dark_tertiary,
   onTertiary = md_theme_dark_onTertiary,
   tertiaryContainer = md_theme_dark_tertiaryContainer,
   onTertiaryContainer = md_theme_dark_onTertiaryContainer,
   error = md_theme_dark_error,
   errorContainer = md_theme_dark_errorContainer,
   onError = md_theme_dark_onError,
   onErrorContainer = md_theme_dark_onErrorContainer,
   background = md_theme_dark_background,
   onBackground = md_theme_dark_onBackground,
   surface = md_theme_dark_surface,
   onSurface = md_theme_dark_onSurface,
   surfaceVariant = md_theme_dark_surfaceVariant,
   onSurfaceVariant = md_theme_dark_onSurfaceVariant,
   outline = md_theme_dark_outline,
   inverseOnSurface = md_theme_dark_inverseOnSurface,
   inverseSurface = md_theme_dark_inverseSurface,
   inversePrimary = md_theme_dark_inversePrimary,
   surfaceTint = md_theme_dark_surfaceTint,
   outlineVariant = md_theme_dark_outlineVariant,
   scrim = md_theme_dark_scrim,
)

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

Jetpack Compose에서 테마 설정을 구현하는 핵심 요소는 MaterialTheme 컴포저블입니다.

AppTheme() 함수에서 MaterialTheme() 컴포저블을 래핑합니다. 다음과 같은 두 매개변수가 사용됩니다.

  • useDarkTheme - 이 매개변수는 isSystemInDarkTheme() 함수와 연결되어 시스템 테마 설정을 관찰하고 밝은 테마 또는 어두운 테마를 적용합니다. 앱을 밝은 테마나 어두운 테마로 수동으로 유지하려면 불리언 값을 useDarkTheme에 전달합니다.
  • content - 테마가 적용되는 콘텐츠입니다.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

지금 앱을 실행하려고 하면 동일하게 보입니다. 새로운 테마 색상이 있는 새로운 색 구성표를 가져왔지만 Compose 앱에 테마를 적용하지 않았으므로 기준 테마 설정이 계속 표시됩니다.

테마가 적용되지 않은 경우 기준 테마 설정이 사용된 앱

테마가 적용되지 않은 경우 기준 테마 설정이 사용된 앱

새 테마를 적용하려면 MainActivity.kt에서 기본 구성 가능한 ReplyApp을 기본 테마 설정 함수 AppTheme()으로 래핑합니다.

MainActivity.kt

setContent {
   val uiState by viewModel.uiState.collectAsStateWithLifecycle()

   AppTheme {
       ReplyApp(/*..*/)
   }
}

또한 미리보기 함수를 업데이트하여 앱 미리보기에 적용된 테마를 확인합니다. ReplyAppPreview() 내에서 ReplyApp 컴포저블을 AppTheme으로 래핑하여 미리보기에 테마 설정을 적용합니다.

미리보기 매개변수에 밝은 시스템 테마와 어두운 시스템 테마를 모두 정의했으므로 두 미리보기가 모두 표시됩니다.

MainActivity.kt

@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_YES,
   name = "DefaultPreviewDark"
)
@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_NO,
   name = "DefaultPreviewLight"
)
@Composable
fun ReplyAppPreview() {
   AppTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(
               emails = LocalEmailsDataProvider.allEmails
           )
       )
   }
}

이제 앱을 실행하면 기준 테마 대신 가져온 테마 색상으로 앱 미리보기가 표시됩니다.

fddf7b9cc99b1fe3.png be7a661b4553167b.png

기준 테마가 있는 앱(왼쪽)

가져온 색상 테마가 있는 앱(오른쪽)

674cec6cc12db6a0.png

가져온 색상 테마가 있는 밝은 앱 미리보기 및 어두운 앱 미리보기

Material 3은 밝은 색 구성표와 어두운 색 구성표를 모두 지원합니다. 가져온 테마로만 앱을 래핑했습니다. Material 3 구성요소가 기본 색상 역할을 사용하고 있습니다.

앱에 추가하기 전에 색상 역할과 사용에 관해 알아보겠습니다.

색상 역할 및 접근성

각 색상 역할은 구성요소의 상태, 가시도, 강조에 따라 다양한 위치에서 사용될 수 있습니다.

1f184a05ea57aa84.png

기본, 보조, 3차 색상의 색상 역할

기본은 기본 색상으로, 눈에 띄는 버튼 및 활성 상태와 같은 기본 구성요소에 사용됩니다.

보조 키 색상은 필터 칩과 같이 UI에서 눈에 덜 띄는 구성요소에 사용됩니다.

3차 키 색상은 대비 강조를 위해 사용되며, 중성색은 앱의 배경과 표면에 사용됩니다.

Material의 색상 시스템은 액세스 가능한 대비율을 충족하는 데 사용할 수 있는 표준 색조 값과 측정값을 제공합니다. 기본 위에 on-primary를, primary-container 위에 on-primary-container를 사용하고 다른 강조 색상과 중성색에도 동일하게 사용하여 사용자가 대비감을 느낄 수 있게 합니다.

자세한 내용은 색상 역할 및 접근성을 참고하세요.

색조 및 그림자 고도

Material 3는 주로 색조 색상 오버레이를 사용하여 고도를 나타냅니다. 이는 컨테이너와 표면을 서로 구별하는 새로운 방법입니다. 색조 고도를 높이면 그림자 외에도 더 눈에 띄는 색조가 사용됩니다.

색조 고도와 그림자 고도 기본 색상 슬롯에서 색상을 가져오는 레벨 2의 색조 고도

어두운 테마의 고도 오버레이도 Material Design 3에서 색조 색상 오버레이로 변경되었습니다. 오버레이 색상은 기본 색상 슬롯에서 가져옵니다.

대부분의 M3 구성요소를 뒷받침하는 지원 컴포저블인 M3 Surface에는 색조 고도 지원과 그림자 고도 지원이 모두 포함됩니다.

Surface(
   modifier = modifier,
   tonalElevation = {..}
   shadowElevation = {..}
) {
   Column(content = content)
}

앱에 색상 추가

앱을 실행하면 앱에서 내보낸 색상이 표시되며 구성요소가 기본 색상을 사용합니다. 색상 역할 및 사용에 관해 알게 되었으며 올바른 색상 역할로 앱의 테마를 설정해 보겠습니다.

be7a661b4553167b.png

색상 테마 및 구성요소가 기본 색상 역할을 사용하는 앱

표면 색상

홈 화면에서는 기본 앱 컴포저블을 Surface()에 래핑하여 앱 콘텐츠를 배치할 기초를 제공합니다. MainActivity.kt를 열고 ReplyApp() 컴포저블을 Surface로 래핑합니다.

또한 5.dp의 색조 고도를 제공하여 표면에 기본 슬롯의 색조 색상을 부여합니다. 이는 목록 항목 및 그 위의 검색창과 대비를 이루는 데 도움이 됩니다. 기본적으로 표면의 색조 및 그림자 고도는 0.dp입니다.

MainActivity.kt

AppTheme {
   Surface(tonalElevation = 5.dp) {
       ReplyApp(
           replyHomeUIState = uiState,
          // other parameters
         )
   }
}

이제 애플리케이션을 실행하고 목록 페이지와 세부정보 페이지가 모두 표시되면 전체 앱에 적용된 색조 표면이 확인됩니다.

be7a661b4553167b.png e70d762495173610.png

표면 및 색조 색상이 없는 앱 배경(왼쪽)

표면 및 색조 색상이 적용된 앱 배경(오른쪽)

앱 바 색상

상단의 맞춤 검색창은 디자인 요청대로 배경이 명확하지 않습니다. 기본적으로 기본 표면으로 대체됩니다. 배경을 제공하여 명확하게 구분할 수 있습니다.

5779fc399d8a8187.png

배경이 없는 맞춤 검색창(왼쪽)

배경이 있는 맞춤 검색창(오른쪽)

이제 앱 바가 포함된 ui/components/ReplyAppBars.kt를 수정합니다. Row 컴포저블의 ModifierMaterialTheme.colorScheme.background를 추가합니다.

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

이제 색조 표면과 배경 색상이 있는 앱 바가 명확하게 구분됩니다.

b1b374b801dadc06.png

색조 표면 위에 배경 색상이 표시된 검색창

플로팅 작업 버튼 색상

70ceac87233fe466.png

테마 설정을 적용하지 않은 큰 플로팅 작업 버튼(왼쪽)

3차 색상으로 테마 설정된 큰 플로팅 작업 버튼(오른쪽)

홈 화면에서 플로팅 작업 버튼(FAB)의 모양을 개선하여 클릭 유도 문구 버튼으로 강조할 수 있습니다. 이를 구현하려면 3차 강조 색상을 적용합니다.

ReplyListContent.kt 파일에서 FAB의 containerColortertiaryContainer 색상으로 업데이트하고 콘텐츠 색상을 onTertiaryContainer로 업데이트하여 접근성과 색상 대비를 유지합니다.

ReplyListContent.kt

ReplyInboxScreen(/*..*/) {
// Email list content
  LargeFloatingActionButton(
    containerColor = MaterialTheme.colorScheme.tertiaryContainer,
    contentColor = MaterialTheme.colorScheme.onTertiaryContainer
  ){
   /*..*/
  }
}

앱을 실행하여 테마가 설정된 플로팅 작업 버튼을 확인합니다. 이 Codelab에서는 LargeFloatingActionButton을 사용합니다.

카드 색상

홈 화면의 이메일 목록은 카드 구성요소를 사용합니다. 기본적으로 채워진 카드는 컨테이너 색상으로 표면 변형 색상을 사용하여 표면 색상과 카드 색상을 명확하게 구분합니다. Compose는 ElevatedCardOutlinedCard의 구현도 제공합니다.

보조 색조를 제공하여 중요한 항목을 더 강조할 수 있습니다. 중요한 이메일에 CardDefaults.cardColors()를 사용하여 카드 컨테이너 색상을 업데이트하여 ui/components/ReplyEmailListItem.kt를 수정합니다.

ReplyEmailListItem.kt

Card(
   modifier =  modifier
       .padding(horizontal = 16.dp, vertical = 4.dp)
       .semantics { selected = isSelected }
       .clickable { navigateToDetail(email.id) },
   colors = CardDefaults.cardColors(
       containerColor = if (email.isImportant)
           MaterialTheme.colorScheme.secondaryContainer
       else MaterialTheme.colorScheme.surfaceVariant
   )
){
  /*..*/
}

5818200be0b01583.png 9367d40023db371d.png

색조 표면에 보조 컨테이너 색상을 사용하여 목록 항목 강조표시

세부정보 목록 항목 색상

이제 홈 화면의 테마를 설정했습니다. 이메일 목록 항목 중 하나를 클릭하여 세부정보 페이지를 살펴봅니다.

7a9ea7cf3e91e9c7.png 79b3874aeca4cd1.png

테마 목록 항목이 없는 기본 세부정보 페이지(왼쪽)

배경 테마 설정이 적용된 세부정보 목록 항목(오른쪽)

목록 항목에 적용된 색상이 없으므로 기본 색조 표면 색상으로 대체됩니다. 목록 항목에 배경 색상을 적용하여 구분되도록 하고 패딩을 추가하여 배경 주위에 공백을 둡니다.

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background)
           .padding(20.dp)
    ) {
      // List item content
    }
}

배경을 제공하면 색조 표면과 목록 항목이 명확하게 구분되는 것을 확인할 수 있습니다.

이제 올바른 색상 역할 및 사용이 포함된 홈 및 세부정보 페이지가 모두 생성됩니다. 앱에서 동적 색상을 활용하여 더욱 맞춤설정되고 일관된 환경을 제공하는 방법을 살펴보겠습니다.

5. 앱에 동적 색상 추가

동적 색상은 Material 3의 핵심 부분으로, 알고리즘이 사용자의 배경화면에서 맞춤 색상을 가져와 앱과 시스템 UI에 적용합니다.

동적 테마를 사용하면 더욱 맞춤설정된 앱을 만들 수 있습니다. 또한 시스템 테마를 통한 일관되고 원활한 환경을 사용자에게 제공합니다.

동적 색상은 Android 12 이상에서 사용할 수 있습니다. 동적 색상을 사용할 수 있다면 dynamicDarkColorScheme() 또는 dynamicLightColorScheme()을 사용하여 동적 색 구성표를 설정할 수 있습니다. 그렇지 않은 경우 기본 밝은/어두운 ColorScheme을 사용하는 것으로 대체됩니다.

Theme.kt 파일의 AppTheme 함수 코드를 아래 코드로 바꿉니다.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
   val context = LocalContext.current
   val colors = when {
       (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
           if (useDarkTheme) dynamicDarkColorScheme(context)
           else dynamicLightColorScheme(context)
       }
       useDarkTheme -> DarkColors
       else -> LightColors
   }
   
      MaterialTheme(
       colorScheme = colors,
       content = content
     )
}

fecc63b4c6034236.png

Android 13 배경화면에서 가져온 동적 테마

이제 앱을 실행하면 기본 Android 13 배경화면을 사용하여 동적 테마 설정이 적용된 것을 볼 수 있습니다.

앱 테마 설정에 사용되는 색 구성표에 따라 상태 표시줄의 스타일을 동적으로 지정할 수도 있습니다.

1095e2b2c1ffdc14.png

상태 표시줄 색상이 적용되지 않은 앱(왼쪽)

상태 표시줄 색상이 적용된 앱(오른쪽)

테마의 기본 색상에 따라 상태 표시줄 색상을 업데이트하려면 AppTheme 컴포저블의 색 구성표 선택 뒤에 상태 표시줄 색상을 추가합니다.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
 
 // color scheme selection code

 // Add primary status bar color from chosen color scheme.
 val view = LocalView.current
 if (!view.isInEditMode) {
    SideEffect {
        val window = (view.context as Activity).window
        window.statusBarColor = colors.primary.toArgb()
        WindowCompat
            .getInsetsController(window, view)
            .isAppearanceLightStatusBars = useDarkTheme
    }
 }
   
  MaterialTheme(
    colorScheme = colors,
     content = content
   )
}

앱을 실행하면 상태 표시줄에 기본 색상 테마가 표시됩니다. 시스템 어두운 테마를 변경하여 밝은 동적 테마와 어두운 동적 테마를 모두 사용해 볼 수도 있습니다.

69093b5bce31fd43.png

Android 13 기본 배경화면에 동적 밝은 테마(왼쪽)와 어두운 테마(오른쪽) 적용

지금까지 앱에 색상을 적용하여 앱의 모양을 향상했습니다. 그러나 앱의 모든 텍스트 크기가 동일하므로 이제 앱에 서체를 추가할 수 있습니다.

6. 서체

Material Design 3는 서체 스케일을 정의합니다. 이름 지정과 그룹화는 표시, 헤드라인, 제목, 본문, 라벨로 간소화되었고 각각 대, 중, 소 크기가 있습니다.

999a161dcd9b0ec4.png

Material 3 서체 스케일

서체 정의

Compose는 기존 TextStylefont-related 클래스와 함께 M3 Typography 클래스를 제공하여 Material 3 서체 스케일을 모델링합니다.

Typography 생성자는 각 스타일의 기본값을 제공하므로 맞춤설정하지 않으려는 매개변수는 생략할 수 있습니다. 자세한 내용은 서체 스타일 및 기본값을 참고하세요.

앱에 5가지 서체 스타일(headlineSmall, titleLarge, bodyLarge, bodyMedium, labelMedium)을 사용합니다. 이 스타일은 홈 화면과 세부정보 화면에 모두 적용됩니다.

제목, 라벨, 본문 스타일의 서체 사용을 보여주는 화면

제목, 라벨, 본문 스타일의 서체 사용을 보여주는 화면

다음으로, ui/theme 패키지로 이동하여 Type.kt를 엽니다. 다음 코드를 추가하여 일부 텍스트 스타일을 기본값 대신 원하는 대로 구현합니다.

Type.kt

val typography = Typography(
   headlineSmall = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 24.sp,
       lineHeight = 32.sp,
       letterSpacing = 0.sp
   ),
   titleLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 18.sp,
       lineHeight = 28.sp,
       letterSpacing = 0.sp
   ),
   bodyLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 16.sp,
       lineHeight = 24.sp,
       letterSpacing = 0.15.sp
   ),
   bodyMedium = TextStyle(
       fontWeight = FontWeight.Medium,
       fontSize = 14.sp,
       lineHeight = 20.sp,
       letterSpacing = 0.25.sp
   ),
   labelMedium = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 12.sp,
       lineHeight = 16.sp,
       letterSpacing = 0.5.sp
   )
)

이제 서체가 정의되었습니다. 테마에 추가하려면 AppTheme 내의 MaterialTheme() 컴포저블에 전달합니다.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       content = content
   )
}

서체를 사용한 작업

색상과 마찬가지로 MaterialTheme.typography를 사용하여 현재 테마의 서체 스타일에 액세스합니다. 이렇게 하면 서체 인스턴스가 Type.kt에서 정의된 모든 서체를 사용할 수 있습니다.

Text(
   text = "Hello M3 theming",
   style = MaterialTheme.typography.titleLarge
)

Text(
   text = "you are learning typography",
   style = MaterialTheme.typography.bodyMedium
)

Material Design 서체 스케일에서 15개의 기본 스타일 모두가 제품에 필요한 것은 아닙니다. 이 Codelab에서는 5개의 크기를 선택하고 나머지는 생략합니다.

Text() 컴포저블에 서체를 적용하지 않았기 때문에 기본적으로 모든 텍스트는 기본적으로 Typography.bodyLarge로 대체됩니다.

홈 목록 서체

다음으로, ui/components/ReplyEmailListItem.ktReplyEmailListItem 함수에 서체를 적용하여 제목과 라벨을 구분합니다.

ReplyEmailListItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.titleLarge,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   overflow = TextOverflow.Ellipsis
)

90645c0765167bb7.png 6c4af2f412c18bfb.png

서체가 적용되지 않은 홈 화면(왼쪽)

서체가 적용된 홈 화면(오른쪽)

세부정보 목록 서체

마찬가지로 ui/components/ReplyEmailThreadItem.kt에서 ReplyEmailThreadItem의 모든 텍스트 컴포저블을 업데이트하여 세부정보 화면에 서체를 추가합니다.

ReplyEmailThreadItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = stringResource(id = R.string.twenty_mins_ago),
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.bodyMedium,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

543ac09e43d8761.png 3412771e95a45f36.png

서체가 적용되지 않은 세부정보 화면(왼쪽)

서체가 적용된 세부정보 화면(오른쪽)

서체 맞춤설정

Compose를 사용하면 텍스트 스타일을 맞춤설정하거나 맞춤 글꼴을 제공하기가 매우 쉽습니다. TextStyle을 수정하여 글꼴 유형, 글꼴 모음, 문자 간격 등을 맞춤설정할 수 있습니다.

theme/Type.kt 파일에서 텍스트 스타일을 변경합니다. 그러면 이를 사용하는 모든 구성요소에 반영됩니다.

titleLarge의 경우 fontWeightSemiBold로, lineHeight32.sp로 업데이트합니다. 목록 항목의 제목에 사용됩니다. 제목을 더 강조하고 명확하게 구분합니다.

Type.kt

...
titleLarge = TextStyle(
   fontWeight = FontWeight.SemiBold,
   fontSize = 18.sp,
   lineHeight = 32.sp,
   letterSpacing = 0.0.sp
),
...

f8d2212819eb0b61.png

제목 텍스트에 맞춤 서체 적용

7. 도형

Material 표면은 다양한 도형으로 표시할 수 있습니다. 도형은 주의를 집중시키고, 구성요소를 구분하고, 상태를 전달하고, 브랜드를 표현합니다.

도형 정의

Compose는 Shapes 클래스에 확장 매개변수를 제공하여 새 M3 도형을 구현합니다. 서체 스케일과 마찬가지로 M3 도형 배율을 사용하면 UI에서 다양한 표현의 도형을 사용할 수 있습니다.

도형 배율에는 다양한 크기의 도형이 있습니다.

  • 아주 작게
  • 작게
  • 보통
  • 크게
  • 아주 크게

기본적으로 각 도형에는 재정의할 수 있는 기본값이 있습니다. 앱에서는 보통 크기의 도형을 사용하여 목록 항목을 수정하지만 다른 도형도 선언할 수 있습니다. ui/theme 패키지에 Shape.kt라는 새 파일을 만들고 도형 코드를 추가합니다.

Shape.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val shapes = Shapes(
   extraSmall = RoundedCornerShape(4.dp),
   small = RoundedCornerShape(8.dp),
   medium = RoundedCornerShape(16.dp),
   large = RoundedCornerShape(24.dp),
   extraLarge = RoundedCornerShape(32.dp)
)

shapes를 정의했고 색상과 서체에서 한 것처럼 M3 MaterialTheme에 전달합니다.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       shapes = shapes,
       content = content
   )
}

도형을 사용한 작업

색상 및 서체와 마찬가지로 MaterialTheme.shape를 사용하여 Material 구성요소에 도형을 적용할 수 있습니다. 그러면 Shape 인스턴스가 Material 도형에 액세스할 수 있습니다.

많은 Material 구성요소에 이미 기본 도형이 적용되어 있지만, 사용 가능한 슬롯을 통해 자체 도형을 제공하고 구성요소에 적용할 수 있습니다.

Card(shape = MaterialTheme.shapes.medium) { /* card content */ }
FloatingActionButton(shape = MaterialTheme.shapes.large) { /* fab content */}

모든 Material 3 구성요소의 기본 도형 값다양한 유형의 도형을 사용한 Material 구성요소 매핑

도형 문서에서 모든 구성요소의 도형 매핑을 확인할 수 있습니다.

사용할 수 있는 다른 두 도형은 Compose의 일부인 RectangleShapeCircleShape입니다. 직사각형 도형에는 테두리 반경이 없으며, 원형 도형은 원의 가장자리를 완전히 표시합니다.

또한 도형을 가져오는 Modifier.clip, Modifier.background, Modifier.border 등의 Modifiers를 사용하여 구성요소에 도형을 적용할 수 있습니다.

앱 바 도형

앱 바의 모서리 배경을 둥글게 처리하려고 합니다.

f873392abe535494.png

TopAppBar는 배경 색상이 있는 Row를 사용합니다. 모서리 배경을 둥글게 만들려면 CircleShape를 배경 수정자에 전달하여 배경의 도형을 정의합니다.

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(
               MaterialTheme.colorScheme.background,
               CircleShape
           ),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

f873392abe535494.png

세부정보 목록 항목 도형

홈 화면에서는 기본적으로 Shape.Medium을 사용하는 카드를 사용하고 있습니다. 그러나 세부정보 페이지에서는 배경 색상이 있는 열을 사용했습니다. 동일한 목록 모양을 위해 보통 크기의 도형을 목록에 적용합니다.

3412771e95a45f36.png 80ee881c41a98c2a.png

세부정보 목록 항목 열. 목록 항목에 도형이 없음(왼쪽) 및 목록에 보통 크기 도형이 있음(오른쪽)

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(8.dp)
           .background(
               MaterialTheme.colorScheme.background,
               MaterialTheme.shapes.medium
           )
           .padding(16.dp)

   ) {
      // List item content
      
   }
}

이제 앱을 실행하면 medium 도형으로 설정된 세부적인 화면 목록 항목이 표시됩니다.

8. 강조

UI를 강조하면 제목과 부제목을 구분하려는 경우와 같이 일부 콘텐츠를 나머지 콘텐츠보다 강조표시할 수 있습니다. M3에서 강조는 색상 변형과 on-color 조합을 사용합니다. 강조를 추가하는 방법은 두 가지가 있습니다.

  1. 확장된 M3 색상 시스템에서 on-surface 및 on-surface-variant 색상과 함께 표면, 표면 변형, 배경을 사용합니다.

예를 들어 표면은 on-surface-variant와 함께, surface-variant는 on-surface와 함께 사용하여 다양한 강조 수준을 제공할 수 있습니다.

또한 표면 변형은 on-accent 색상보다 덜 강조되도록 강조 색상과 함께 사용할 수도 있지만 여전히 접근성이 있고 대비율을 따릅니다.

표면, 배경 및 표면 변형 색상 역할

표면, 배경 및 표면 변형 색상 역할

  1. 텍스트에 서로 다른 글꼴 두께를 사용합니다. 서체 섹션에서 본 것과 같이 서체 스케일에 맞춤 두께를 제공하여 다양하게 강조할 수 있습니다.

다음으로, 표면 변형을 사용하여 강조 차이를 제공하도록 ReplyEmailListItem.kt를 업데이트합니다. 기본적으로 카드의 콘텐츠는 배경에 따라 기본 콘텐츠 색상을 사용합니다.

시간 텍스트 및 본문 텍스트 컴포저블의 색상을 onSurfaceVariant로 업데이트합니다. 이렇게 하면 기본적으로 제목 및 제목 텍스트 컴포저블에 적용되는 onContainerColors에 비해 강조가 줄어듭니다.

2c9b7f2bd016edb8.png 6850ff391f21e4ba.png

제목과 비교했을 때 동일하게 강조된 시간 및 본문 텍스트(왼쪽)

제목에 비해 강조된 줄어든 시간 및 본문 텍스트(오른쪽)

ReplyEmailListItem.kt

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant,
   overflow = TextOverflow.Ellipsis
)

배경이 secondaryContainer인 중요 이메일 카드의 경우 모든 텍스트 색상은 기본적으로 onSecondaryContainer 색상입니다. 나머지 이메일의 경우 배경은 surfaceVariant,이므로 모든 텍스트가 기본적으로 onSurfaceVariant 색상입니다.

9. 축하합니다

축하합니다. 이 Codelab을 완료했습니다. Compose에서 색상, 서체, 도형을 동적 색상과 함께 사용하여 Material Theming 설정을 구현하고 애플리케이션의 테마를 설정하고 맞춤 환경을 제공했습니다.

2d8fcabf15ac5202.png 5a4d31db0185dca6.png ce009e4ce560834d.png

동적 색상과 색상 테마가 적용된 테마 설정 완료 결과

다음 단계

Compose 개발자 과정의 다른 Codelab도 확인하세요.

추가 자료

샘플 앱

  • 전체 Material 3 테마 설정이 포함된 Reply 샘플
  • 동적 테마 설정을 보여주는 JetChat

참조 문서