Jetpack Compose 테마 설정

1. 소개

이 Codelab에서는 Jetpack Compose의 테마 설정 API를 사용하여 애플리케이션의 스타일을 지정하는 방법을 알아봅니다. 밝은 테마와 어두운 테마 등 여러 테마를 지원하여 애플리케이션 전체에서 일관되게 사용되도록 색상과 도형, 서체를 맞춤설정하는 방법을 알아봅니다.

학습할 내용

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

  • Material Design 기본 지침서 및 브랜드에 맞게 맞춤설정하는 방법
  • Compose에서 Material Design 시스템을 구현하는 방법
  • 앱 전체에서 색상과 서체, 도형을 정의하고 사용하는 방법
  • 구성요소의 스타일을 지정하는 방법
  • 밝은 테마와 어두운 테마를 지원하는 방법

빌드할 항목

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

스타일을 적용하기 전 뉴스 읽기 앱 Jetnews를 보여주는 이미지

스타일을 적용한 후 뉴스 읽기 앱 Jetnews를 보여주는 이미지

어두운 테마로 스타일이 지정된 뉴스 읽기 앱 Jetnews를 보여주는 이미지

이전: 스타일이 지정되지 않은 앱

이후: 스타일이 지정된 앱

이후: 어두운 테마

기본 요건

2. 설정

이 단계에서는 스타일을 지정할 간단한 뉴스 리더 앱을 구성하는 코드를 다운로드합니다.

필요한 항목

코드 다운로드

git을 설치했다면 아래 명령어를 실행하면 됩니다. git이 설치되어 있는지 확인하려면 터미널이나 명령줄에 git --version을 입력하여 올바르게 실행되는지 확인합니다.

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ThemingCodelabM2

git이 없는 경우 다음 버튼을 클릭하여 이 Codelab을 위한 모든 코드를 다운로드할 수 있습니다.

Android 스튜디오에서 프로젝트를 열고 'File > Import Project'를 선택한 후 ThemingCodelabM2 디렉터리로 이동합니다.

프로젝트에는 세 가지 기본 패키지가 포함되어 있습니다.

  • com.codelab.theming.data: 모델 클래스와 샘플 데이터가 포함되어 있습니다. 이 Codelab을 진행하는 동안에는 이 패키지를 수정할 필요가 없습니다.
  • com.codelab.theming.ui.start: 이 Codelab의 시작점입니다. 이 Codelab에서 요구되는 모든 변경사항을 이 패키지에서 실행해야 합니다.
  • com.codelab.theming.ui.finish: Codelab의 최종 상태로, 참조용입니다.

앱 빌드 및 실행

애플리케이션에는 Codelab의 시작 상태와 최종 상태를 반영하는 실행 구성이 2개 있습니다. 구성을 선택하고 실행 버튼을 누르면 기기나 에뮬레이터에 코드가 배포됩니다.

a43ae3c4fa75836e.png

애플리케이션에는 Compose 레이아웃 미리보기도 포함되어 있습니다. start/finish 패키지에서 Home.kt로 이동하여 디자인 보기를 열면 UI 코드에서 빠르게 반복할 수 있는 여러 미리보기가 표시됩니다.

758a285ad8a6cd51.png

3. Material Theming

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

Material Theming에 관한 이해는 Jetpack Compose 앱의 테마를 설정하는 방법을 이해하는 데 도움이 되므로 여기서는 개념을 간략하게 설명합니다. Material Theming에 이미 익숙하다면 앞으로 건너뛰어도 됩니다.

색상

Material Design은 앱 전체에서 사용할 수 있는, 의미론적으로 이름이 지정된 여러 색상을 정의합니다.

62ccfe5761fd9eda.png

기본 색상은 주요 브랜드 색상이며 보조 색상은 강조 표시에 사용됩니다. 대비되는 영역에 더 어둡거나 밝은 변형을 제공할 수 있습니다. 배경과 표면 색상은 애플리케이션의 '표면'에 개념적으로 존재하는 구성요소를 보유한 컨테이너에 사용됩니다. 또한 Material은 'on' 색상을 정의합니다. 이 색상은 이름이 지정된 색상 중 하나 위에 있는 콘텐츠에 사용됩니다(예: '표면' 색상 컨테이너의 텍스트는 'on surface'에 색상이 지정되어야 함). Material 구성요소는 이러한 테마 색상을 사용하도록 구성됩니다. 예를 들어 기본적으로 플로팅 작업 버튼secondary 색상이 지정되고 카드surface로 기본값이 설정됩니다.

이름이 지정된 색상을 정의하면 밝은 테마 및 어두운 테마 둘 다와 같은 대체 색상 팔레트를 제공할 수 있습니다.

1a9b78141ddfa87b.png

작은 색상 팔레트를 정의하여 앱 전체에서 일관되게 사용할 수도 있습니다. Material 색상 도구를 사용하면 쉽게 색상을 선택하여 색상 팔레트를 만들 수 있어 조합에도 액세스할 수 있도록 합니다.

서체

마찬가지로 Material은 의미론적으로 이름이 지정된 여러 서체 스타일을 정의합니다.

1d44de3ff2f7fd1c.png

테마별로 서체 스타일을 변경할 수는 없지만 서체 스케일을 사용하면 애플리케이션 내에서 일관성이 높아집니다. 자체 글꼴 및 기타 맞춤설정 서체를 제공하면 앱에서 사용하는 Material 구성요소에 반영됩니다(예: 앱 바는 기본적으로 h6 스타일 사용, 버튼button 사용). Material 서체 스케일 생성기 도구를 사용하면 서체 스케일을 빌드하는 데 도움이 됩니다.

도형

Material은 도형을 체계적으로 사용하여 브랜드를 전달할 수 있도록 지원합니다. 소형, 중형, 대형 구성요소라는 3가지 카테고리를 정의합니다. 각각은 모서리 스타일(잘리거나 둥근 스타일)과 크기를 맞춤설정하여 사용할 도형을 정의할 수 있습니다.

886b811cc9cad18e.png

도형 테마를 맞춤설정하면 다양한 구성요소에 반영됩니다. 예를 들어 버튼텍스트 필드는 소형 도형 테마를 사용하고 카드대화상자는 중형 도형 테마를 사용하며 시트는 대형 도형 테마를 기본적으로 사용합니다. 여기에서 도형 테마에 관한 구성요소의 전체 매핑을 확인할 수 있습니다. Material 도형 맞춤설정 도구를 사용하면 도형 테마를 생성할 수 있습니다.

기준

Material은 기본적으로 '기준' 테마로 설정됩니다. 즉, 위 이미지와 같은 자주색 색 구성표, Roboto 서체 스케일, 약간 둥근 도형입니다. 테마를 지정하거나 맞춤설정하지 않으면 구성요소에 기준 테마가 사용됩니다.

4. 테마 정의

MaterialTheme

Jetpack Compose에서 테마 설정을 구현하는 핵심 요소는 MaterialTheme 컴포저블입니다. 이 컴포저블을 Compose 계층 구조에 배치하면 그 안의 모든 구성요소의 색상과 서체, 도형 맞춤설정을 지정할 수 있습니다. 라이브러리에서 이 컴포저블이 정의되는 방법은 다음과 같습니다.

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

나중에 colors, typography, shapes 속성을 노출하는 MaterialTheme object를 사용하여 이 컴포저블에 전달된 매개변수를 검색할 수 있습니다. 각각에 관해서는 뒷부분에서 자세히 살펴봅니다.

Home.kt를 열고 구성 가능한 Home 함수를 찾습니다. 이 함수가 앱의 기본 진입점입니다. MaterialTheme을 선언하는 동안 매개변수를 지정하지 않으므로 기본적인 '기준' 스타일이 적용됩니다.

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

색상, 서체, 도형 매개변수를 만들어 앱의 테마를 구현해 보겠습니다.

테마 만들기

스타일을 중앙 집중화하려면 MaterialTheme을 래핑하고 구성하는 자체 컴포저블을 만드는 것이 좋습니다. 이렇게 하면 테마 맞춤설정을 한곳에서 지정하고 여러 화면 또는 @Preview 등 여러 위치에서 쉽게 재사용할 수 있습니다. 필요하다면 여러 테마 컴포저블을 만들 수 있습니다. 앱의 여러 섹션에 스타일을 다양하게 지원하려는 경우를 예로 들 수 있습니다.

com.codelab.theming.ui.start.theme 패키지에서 Theme.kt라는 새 파일을 만듭니다. 다른 컴포저블을 콘텐츠로 허용하고 MaterialTheme을 래핑하는 JetnewsTheme이라는 새로운 구성 가능한 함수를 추가합니다.

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

이제 다시 Home.kt로 전환하여 MaterialThemeJetnewsTheme으로 바꾸고 가져옵니다.

-  MaterialTheme {
+  JetnewsTheme {
    ...

이 화면의 @Preview에는 아직 변경사항이 표시되지 않습니다. PostItemPreviewFeaturedPostPreview를 업데이트하여 새로운 JetnewsTheme 컴포저블로 콘텐츠를 래핑합니다. 그러면 미리보기에서 새 테마를 사용할 수 있습니다.

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

색상

다음은 앱에서 구현하려는 색상 팔레트입니다. 지금은 밝은 팔레트지만 뒷부분에서 어두운 테마도 다룰 것입니다.

B2635ed3ec4bfc8f.png

Compose의 색상은 Color 클래스를 사용하여 정의합니다. 색상을 ULong으로 또는 별도의 색상 채널로 지정할 수 있는 생성자가 여러 개 있습니다.

theme 패키지에 새 파일 Color.kt를 만듭니다. 이 파일에 다음 색상을 최상위 공개 속성으로 추가합니다.

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

앱 색상을 정의했으므로 이제 MaterialTheme에 필요한 Colors 객체로 함께 가져와 Material의 이름이 지정된 색상에 특정 색상을 할당해 보겠습니다. Theme.kt로 다시 전환하고 다음을 추가합니다.

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

여기서는 lightColors 함수를 사용하여 Colors를 빌드합니다. 이렇게 하면 적절한 기본값이 제공되므로 Material 색상 팔레트를 구성하는 모든 색상을 지정할 필요가 없습니다. 예를 들어 background 색상이나 많은 'on' 색상은 지정하지 않았으므로 기본값을 사용합니다.

이제 앱에서 이러한 색상을 사용해 보겠습니다. 새 Colors를 사용하도록 JetnewsTheme 컴포저블을 업데이트합니다.

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Home.kt를 열고 미리보기를 새로고침합니다. 새로운 색 구성표가 TopAppBar와 같은 구성요소에 반영되었습니다.

서체

다음은 앱에서 구현하고자 하는 서체 스케일입니다.

54c420f78529b77d.png

Compose에서는 TextStyle 객체를 정의하여 일부 텍스트의 스타일을 지정하는 데 필요한 정보를 정의할 수 있습니다. 속성 샘플은 다음과 같습니다.

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

제목에 Montserrat를 사용하고 본문 텍스트에 Domine을 사용하는 서체 스케일이 좋습니다. 관련 글꼴 파일은 프로젝트의 res/fonts 폴더에 이미 추가되어 있습니다.

theme 패키지에 Typography.kt라는 새 파일을 만듭니다. 먼저 각 Font의 여러 가중치를 결합하는 FontFamily를 정의해 보겠습니다.

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

이제 MaterialTheme에서 허용하는 Typography 객체를 만들어 스케일의 각 시맨틱 스타일에 관한 TextStyle을 지정합니다.

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Theme.kt를 열고 새 Typography를 사용하도록 JetnewsTheme 컴포저블을 업데이트합니다.

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Home.kt를 열고 미리보기를 새로고침하여 새 서체가 적용되는지 확인합니다.

도형

도형을 사용하여 앱에서 브랜드를 표현하고자 합니다. 다음과 같이 여러 요소에 잘린 모서리 도형을 사용합니다.

9b60c78a78c61570.png

Compose는 도형 테마를 정의하는 데 사용할 수 있는 RoundedCornerShapeCutCornerShape 클래스를 제공합니다.

theme 패키지에 새 파일 Shape.kt를 만들고 다음을 추가합니다.

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Theme.kt를 열고 다음 Shapes를 사용하도록 JetnewsTheme 컴포저블을 업데이트합니다.

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Home.kt를 열고 미리보기를 새로고침하여 추천 게시물을 표시하는 Card가 새로 적용된 도형 테마를 어떻게 반영하는지 확인합니다.

어두운 테마

앱에서 어두운 테마를 지원하면 앱이 사용자 기기(Android 10부터 전역 어두운 테마 전환이 가능함)에서 더 잘 통합될 뿐만 아니라 전력 사용량을 줄이고 접근성 요구사항을 지원할 수 있습니다. Material은 어두운 테마를 만드는 방법에 관한 디자인 안내를 제공합니다. 다음은 어두운 테마에 구현하려는 대체 색상 팔레트입니다.

21768b33f0ccda5f.png

Color.kt를 열고 다음 색상을 추가합니다.

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

이제 Theme.kt를 열고 다음을 추가합니다.

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

이제 JetnewsTheme을 업데이트합니다.

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

여기서는 어두운 테마를 사용할지에 관한 새로운 매개변수를 추가하고 전역 설정에 관해 기기를 쿼리하는 것으로 기본값을 설정했습니다. 이렇게 하면 좋은 기본값이 제공되지만 특정 화면을 항상 어둡게 또는 어둡지 않게 하거나 어두운 테마의 @Preview로 만들려면 여전히 재정의하기 쉽습니다.

Home.kt를 열고 어두운 테마로 표시되는 FeaturedPost 컴포저블의 새 미리보기를 만듭니다.

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

미리보기 창을 새로고침하여 어두운 테마 미리보기를 확인합니다.

84f93b209ce4fd46.png

5. 색상 사용

이전 단계에서는 자체 테마를 만들어 앱의 색상, 서체 스타일, 도형을 설정하는 방법을 살펴봤습니다. 모든 Material 구성요소는 기본적으로 이러한 맞춤설정을 사용합니다. 예를 들어 FloatingActionButton 컴포저블의 기본값은 테마의 secondary 색상을 사용하지만 이 매개변수에 다른 값을 지정하여 대체 색상을 설정할 수 있습니다.

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

기본 설정을 사용하지 않는 경우도 있습니다. 이 섹션에서는 앱에서 색상을 사용하는 방법을 설명합니다.

원색

앞에서 본 바와 같이 Compose는 Color 클래스를 제공합니다. 이를 로컬에서 만들고 object 등에 보관할 수 있습니다.

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color에는 다양한 알파/빨간색/녹색/파란색 값으로 새 색상을 만들 수 있는 copy와 같은 여러 유용한 메서드가 있습니다.

테마 색상

더 유연한 접근 방식은 테마에서 색상을 가져오는 것입니다.

Surface(color = MaterialTheme.colors.primary)

여기서는 colors 속성이 MaterialTheme 컴포저블에 설정된 Colors를 반환하는 MaterialTheme object를 사용합니다. 즉, 테마에 여러 색상 세트를 제공하여 다양한 디자인과 분위기를 지원할 수 있으므로 애플리케이션 코드를 터치할 필요가 없습니다. 예를 들어 AppBarprimary 색상을 사용하고 화면 배경은 surface입니다. 테마 색상을 변경하면 다음 컴포저블에 반영됩니다.

b0b0ca02b52453a7.png

253ab041d7ea904e.png

테마의 각 색상이 Color 인스턴스이므로 copy 메서드를 사용하여 쉽게 색상을 파생할 수도 있습니다.

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

여기서는 onSurface 색상의 사본을 만들지만 불투명도는 10%입니다. 이렇게 하면 정적 색상을 하드 코딩하지 않고 다양한 테마에서 색상을 사용할 수 있습니다.

표면 및 콘텐츠 색상

많은 구성요소가 한 쌍의 색상 및 '콘텐츠 색상'을 허용합니다.

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

이를 통해 컴포저블의 색상을 설정할 수 있을 뿐만 아니라 '콘텐츠'(그 안에 있는 컴포저블)의 기본 색상을 제공할 수도 있습니다. 많은 컴포저블은 기본적으로 이 콘텐츠 색상(예: Text 색상 또는 Icon 색조)을 사용합니다. contentColorFor 메서드는 테마 색상에 적절한 'on' 색상을 가져옵니다. 예를 들어 primary 배경을 설정하면 onPrimary가 콘텐츠 색상으로 반환됩니다. 테마가 아닌 배경 색상을 설정하면 적절한 콘텐츠 색상을 직접 제공해야 합니다.

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

LocalContentColor CompositionLocal을 사용하여 현재 배경과 대비되는 색상을 가져올 수 있습니다.

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

요소의 색상을 설정할 때는 Surface를 사용하는 것이 좋습니다. 적절한 콘텐츠 색상 CompositionLocal 값을 설정하기 때문입니다. 적절한 콘텐츠 색상을 설정하지 않는 Modifier.background를 직접 호출할 때는 주의해야 합니다.

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

현재 Header 구성요소에는 항상 Color.LightGray 배경이 있습니다. 이는 밝은 테마에서는 괜찮아 보이지만 어두운 테마의 배경에서는 고대비를 이룹니다. 또한 특정 텍스트 색상을 지정하지 않으므로 배경과 대비되지 않을 수 있는 현재 콘텐츠 색상을 상속받습니다.

7329ac6ead5097eb.png

이 문제를 해결해 보겠습니다. Home.ktHeader 컴포저블에서 하드 코딩 색상을 지정하는 background 수정자를 삭제합니다. 대신 SurfaceText를 테마에서 파생된 색상으로 래핑하고 콘텐츠의 색상을 primary로 설정해야 한다고 지정합니다.

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

콘텐츠 알파

중요도를 전달하고 시각적 계층 구조를 제공하려고 콘텐츠를 강조하거나 덜 강조할 때가 많습니다. Material Design에서는 다양한 수준의 불투명도를 사용하여 다양한 중요도 수준을 전달하도록 권장합니다.

Jetpack Compose에서는 LocalContentAlpha를 사용해 이를 구현합니다. CompositionLocal 값을 제공하여 계층 구조의 콘텐츠 알파를 지정할 수 있습니다. 하위 컴포저블에서 이 값을 사용합니다. 예를 들어 TextIcon은 기본적으로 LocalContentAlpha를 사용하도록 조정된 LocalContentColor 조합을 사용합니다. Material에서는 ContentAlpha 객체에 의해 모델링된 일부 표준 알파 값(high, medium, disabled)을 지정합니다. MaterialThemeLocalContentAlpha의 기본값을 ContentAlpha.high로 설정합니다.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

이를 통해 구성요소의 중요도를 쉽고 일관되게 전달할 수 있습니다.

콘텐츠 알파를 사용하여 추천 게시물의 정보 계층 구조를 명확히 합니다. Home.ktPostMetadata 컴포저블에서 메타데이터 medium을 강조합니다.

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

어두운 테마

앞서 확인했듯이 Compose에서 어두운 테마를 구현하려면 다양한 색상 세트를 제공하고 테마를 통해 색상을 쿼리하기만 하면 됩니다. 다음 예외사항에 주의하세요.

밝은 테마에서 실행 중인지 확인할 수 있습니다.

val isLightTheme = MaterialTheme.colors.isLight

이 값은 lightColors/darkColors 빌더 함수로 설정됩니다.

머티리얼의 어두운 테마에서는 고도가 높은 표면이 고도 오버레이를 수신합니다(배경이 밝아짐). 이는 어두운 색상 팔레트를 사용하면 자동으로 구현됩니다.

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

사용 중인 TopAppBarCard 구성요소에서 모두 앱의 이러한 자동 동작을 확인할 수 있습니다. 기본적으로 고도가 4dp와 1dp이므로 이 고도를 잘 전달할 수 있도록 어두운 테마에서 배경이 자동으로 밝아집니다.

cb8c617b8c151820.png

Material Design은 어두운 테마에서 넓은 밝은 색상 영역을 피할 것을 권장합니다. 일반적인 패턴은 밝은 테마에서 컨테이너 primary 색상을 지정하고 어두운 테마에서 surface 색상을 지정하는 것입니다. 앱 바하단 탐색 메뉴와 같은 많은 구성요소에서 기본적으로 이 전략을 사용합니다. 이를 더 쉽게 구현할 수 있도록 Colors는 정확하게 이 동작을 제공하는 primarySurface 색상을 제공하고 이러한 구성요소에서 기본적으로 사용됩니다.

앱은 현재 앱 바를 primary 색상으로 설정하고 있지만 primarySurface로 전환하거나 기본값이므로 이 매개변수를 삭제하여 이 안내를 따를 수 있습니다. AppBar 컴포저블에서 TopAppBarbackgroundColor 매개변수를 변경합니다.

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. 텍스트 사용

텍스트를 사용할 때는 Text 컴포저블을 사용하여 텍스트를 표시하고 TextFieldOutlinedTextField를 텍스트 입력에 사용하며 TextStyle을 사용하여 텍스트에 단일 스타일을 적용합니다. AnnotatedString을 사용하여 텍스트에 여러 스타일을 적용할 수 있습니다.

색상에서 확인했듯이 텍스트를 표시하는 Material 구성요소는 테마 서체 맞춤설정을 선택합니다.

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

이는 색상에서 확인한 것처럼 기본 매개변수 사용보다 약간 더 복잡합니다. 구성요소가 텍스트를 직접 표시하지 않는 경향이 있기 때문입니다. 대신 Text 컴포저블을 전달할 수 있는 '슬롯 API'를 제공합니다. 그렇다면 구성요소가 테마 서체 스타일을 설정하는 방법은 무엇일까요? 내부적으로는 ProvideTextStyle 컴포저블(자체적으로 CompositionLocal 사용)을 사용하여 '현재' TextStyle을 설정합니다. Text 컴포저블은 구체적인 textStyle 매개변수를 제공하지 않으면 이 '현재' 스타일을 쿼리하도록 기본 설정됩니다.

Compose의 ButtonText 클래스의 예는 다음과 같습니다.

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

테마 텍스트 스타일

색상과 마찬가지로 현재 테마에서 TextStyle을 검색하는 것이 가장 좋으며 작고 일관된 스타일 세트를 사용하고 유지관리가 쉽도록 하는 것이 좋습니다. MaterialTheme.typographyMaterialTheme 컴포저블에 설정된 Typography 인스턴스를 검색하므로 개발자가 정의한 스타일을 사용할 수 있습니다.

Text(
  style = MaterialTheme.typography.subtitle2
)

TextStyle을 맞춤설정해야 한다면 copy하여 속성을 재정의하거나(data class일 뿐임) Text 컴포저블이 TextStyle 위에 오버레이드될 여러 스타일 지정 매개변수를 허용하면 됩니다.

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

앱의 여러 위치에서 TextStyle 테마가 자동으로 적용됩니다. 예를 들어 TopAppBartitle의 스타일을 h6로 지정하고 ListItem은 기본 및 보조 텍스트의 스타일을 subtitle1body2로 각각 지정합니다.

나머지 앱에 테마 서체 스타일을 적용해 보겠습니다. subtitle2를 사용하도록 Header를 설정하고 FeaturedPost의 텍스트는 제목에 h6, 저자와 메타데이터에 body2를 사용하도록 설정합니다.

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

여러 스타일

일부 텍스트에 여러 스타일을 적용해야 하는 경우 마크업을 적용하는 AnnotatedString 클래스를 사용하면 SpanStyle을 텍스트 범위에 추가할 수 있습니다. 동적으로 이를 추가하거나 DSL 문법을 사용하여 콘텐츠를 만들 수 있습니다.

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

앱의 각 게시물을 설명하는 태그의 스타일을 지정해 보겠습니다. 현재 나머지 메타데이터와 동일한 텍스트 스타일을 사용합니다. 이를 구분하기 위해 overline 텍스트 스타일과 배경 색상을 사용합니다. PostMetadata 컴포저블의 경우:

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. 도형 사용

색상 및 서체와 마찬가지로 도형 테마를 설정하면 Material 구성요소에 반영됩니다. 예를 들어 Button은 작은 구성요소를 위한 도형 세트를 선택합니다.

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

색상과 마찬가지로 Material 구성요소는 기본 매개변수를 사용하므로 간단하게 구성요소가 사용할 도형 카테고리를 확인하거나 대안을 제공할 수 있습니다. 구성요소를 도형 카테고리에 완전히 매핑하는 방법은 문서를 참고하세요.

일부 구성요소는 상황에 맞게 수정된 테마 도형을 사용합니다. 예를 들어 TextField는 기본적으로 작은 도형 테마를 사용하지만 하단 모서리에는 모서리 크기 0을 적용합니다.

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

테마 도형

물론 자체 구성요소를 만들 때 도형(예: Surface, Modifier.clip, Modifier.background, Modifier.border 등)을 허용하는 컴포저블이나 Modifier를 사용하여 직접 도형을 사용할 수 있습니다.

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

PostItem에 표시된 이미지에 도형 테마 설정을 추가해 보겠습니다. 테마의 small 도형을 clip Modifier로 적용하여 왼쪽 상단 모서리를 잘라냅니다.

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. '스타일' 구성요소

Compose는 Android 뷰 스타일이나 CSS 스타일과 같이 구성요소의 스타일을 추출하는 명시적인 방법을 제공하지 않습니다. 모든 Compose 구성요소는 Kotlin으로 작성되므로 동일한 목표를 달성하는 다른 방법이 있습니다. 대신 맞춤설정된 구성요소의 자체 라이브러리를 만들어 앱 전체에서 사용하세요.

이 작업은 이미 앱에서 하고 있습니다.

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

Header 컴포저블은 기본적으로 스타일이 지정된 Text이며 앱 전체에서 사용할 수 있습니다.

지금까지 모든 구성요소가 하위 수준의 기본 요소로 구성되는 것을 확인했습니다. 이러한 동일한 기본 요소를 사용하여 Material의 구성요소를 맞춤설정할 수 있습니다. 예를 들어 ButtonProvideTextStyle 컴포저블을 사용하여, 전달된 콘텐츠의 기본 텍스트 스타일을 설정했습니다. 똑같은 메커니즘을 사용하여 자체 텍스트 스타일을 설정할 수 있습니다.

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

이 예에서는 표준 Button 클래스를 래핑하여 LoginButton의 자체 '스타일'을 만들고 다른 backgroundColor 및 텍스트 스타일과 같은 특정 속성을 지정합니다.

구성요소 유형의 기본 모양을 맞춤설정하는 방법과 같은 기본 스타일 지정이라는 개념도 없습니다. 라이브러리 구성요소를 래핑하고 맞춤설정하는 자체 구성요소를 만들어 이를 달성할 수 있습니다. 예를 들어 앱 전체에서 모든 Button의 모양을 맞춤설정하려고 하지만 Button이 아닌 다른 구성요소에 영향을 줄 수 있는 작은 도형 테마는 변경하고 싶지 않다고 가정해 보겠습니다. 이를 위해서는 자체 컴포저블을 만들고 다음을 전체적으로 사용하세요.

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. 축하합니다

축하합니다. 이 Codelab을 완료하고 Jetpack Compose 앱의 스타일을 지정했습니다.

머티리얼 테마를 구현하여 앱 전반에서 사용되는 색상, 서체, 도형을 맞춤설정해 브랜드를 표현하고 일관성을 높였습니다. 밝은 테마 및 어두운 테마 지원을 모두 추가했습니다.

다음 단계

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

추가 자료

샘플 앱

  • 여러 테마를 보여주는 Owl
  • 동적 테마 설정을 보여주는 Jetcaster
  • 맞춤 디자인 시스템 구현을 보여주는 Jetsnack

참조 문서