머티리얼 테마

Jetpack Compose에서 테마를 구현하기 위한 핵심 요소는 MaterialTheme 컴포저블 함수다. 이 컴포저블을 컴포즈 계층에 배치하면 그 안의 모든 컴포넌트에 대한 색상, 유형 및 모양에 대한 커스터마이징을 할 수 있다. 이 컴포저블이 라이브러리에서 정의되는 방법은 다음과 같다.

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

나중에 색상, 타이포그래피 및 모양 속성을 노출하는 MaterialTheme 객체를 사용하여, 이 컴포저블에 전달된 매개변수를 개선할 수 있다. 나중에 각각에 대해 자세히 살펴보자.

Home.kt를 열고 Home 컴포저블 함수로 이동한다. 이것은 앱의 주요한 진입점이다. MaterialTheme를 선언하는 동안, 매개변수를 지정하지 않으므로 기본 “baseline” 스타일이 적용된다.

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

앱의 테마를 구현하기 위해 색상, 유형 및 모양 매개변수를 생성해보자.

테마 만들기

스타일코드를 중앙 집중화하여 관리하려면, MaterialTheme를 래핑하고 구성하는 자신만의 컴포저블을 만드는 것이 좋다. 이렇게 하면 커스텀 테마 지정할 수 있는 단일 위치를 제공하고, 여러곳에서 쉽게 재사용할 수 있다.

com.codelab.theming.ui.start.theme 패키지에서 Theme.kt라는 새 파일을 만든다. 다른 컴포저블을 콘텐츠로 받아들이고 MaterialTheme를 래핑하는 JetnewsTheme라는 새로운 컴포저블 함수를 추가한다.

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

이제 Home.kt로 돌아와 MaterialTheme를 JetnewsTheme로 교체한다.

-  MaterialTheme {
+  JetnewsTheme {
    ...

Note: com.codelab.theming.ui.finish.theme.JetnewsTheme가 아닌 com.codelab.theming.ui.start.theme.JetnewsTheme를 가져오도록 유의하자.

@Preview 화면 에서는 아직 변경 사항을 알수가 없다. 미리보기가 새로운 테마를 사용하도록 PostItemPreview 및 FeaturedPostPreview를 업데이트하여 새로운 JetnewsTheme이 콘텐츠를 래핑하고, 미리보기에서 새로운 테마를 사용하도록 한다.

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

Note: Android Studio의 컴포즈 프로젝트 템플릿은 이 구조를 자동으로 생성하지만, 우리는 개념을 이해하기 위해서 처음부터 구축해보고 있다.

Colors

다음은 앱에서 구현하려는 색상 팔레트입니다. 우선은 라이트 테마용 팔레트를 구현하고, 돌아와 다크 테마를 곧 지원할 것이다.

16a0a3d57f49b71d.png

Compose의 색상은 Color클래스를 사용하여 정의된다. 색상을 ULong 또는 색상 채널을 분리하여 지정할 수 있는 여러 생성자가 있다.

Note: 색상 지정을 위해 일반적인 ‘#dd0d3c’ 형식으로 부터 변환하려면 ‘#’을 ‘0xff’, 즉 Color(0xffdd0d3c)로 바꾸자. 여기서 ‘ff’는 alpha채널값이며, 완전히 불투명함을 의미한다.

theme 패키지에 새 파일 Color.kt을 만든다. 이 파일의 최상위 공용 속성으로 다음 색상을 추가한다.

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

Note : 색상을 정의 할 때, 색상의 이름을 “의미론적인 것보다” 색상 값을 기준으로한 색상 이름을 “그대로” 사용하여 이름을 짓는다. 예를들어 Red500이라고 짓지 primary라고 짓지 않는다. 이를 통해 여러 테마를 정의할 수 있다. 예를 들어 다른 색상을 다크테마의 primary로 사용하거나 화면상에서 다르게 스타일링 될 수 있다.

Note: Compose의 색상 유형을 android.graphics.Color로 가져 오지 않도록 주의하자.  androidx.compose.ui.graphics.Color로 가져와야한다.

이제 앱 색상을 정의했으므로 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 함수를 사용하여 색상을 만든다. 이는 합리적인 기본값을 제공하므로 머티리얼 색상 팔레트를 구성하는 모든 색상을 지정할 필요가 없다. 예를 들어 우리는 background 색상을 지정하지 않았고 또는 많은 ‘on’ 색상을 지정하지 않았다. 이 경우 기본값을 사용한다.

Note: 브랜드에 별도의 기본 색상(primary)과 보조 색상(secondary)이 없는 경우 둘 다 동일한 색상을 제공하는 것이 좋다.

이제 앱에서 이 색상을 사용해 보자. JetnewsTheme 컴포저블을 업데이트 하여 새로운 Colors를 사용하자.

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

Home.kt 을 열고 미리보기를 새로고치자.  TopAppBar와 같은 컴포넌트내에서 반영된 새로운 색상 스킴을 확인하자.

타이포그래피

다음은 앱에서 구현하려는 글자 크기다.

985064b5f0dbd8bd.png

Compose에서 TextStyle객체를 정의하여, 일부 텍스트의 스타일을 지정하는 데 필요한 정보를 정의 할 수 있다. 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에 추가되어 있다.

Note: 불행히도 컴포즈는 현재 Android의 Downloadable Fonts 기능은 지원하고 있지 않다.

 theme 패키지에 새 파일 Typography.kt을 만든다. 먼저 FontFamily들(각 Font의 서로 다른 가중치를 결합함)을 정의해보자.

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 객체를 만들고, 크기별로 각 의미론적인 스타일에 대해 TextStyles를 지정한다.

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를 열고 JetnewsTheme 컴포저블을 업데이트하여 새로운 Typography를 사용하자.

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

Home.kt 열고 미리보기를 새로고침하여 새로운 타이포그래피가 적용되는지 확인하자.

모양

앱에서 shape을 사용하여 브랜드를 표현하고 싶다. 우리는 여러 요소에 모서리가 잘린 모양을 사용하려고 한다.

ebcdf2fb3364f0d3.png

컴포즈는 당신이 정의한 shape theme을 사용할 수 있는 RoundedCornerShape및 CutCornerShape클래스를 제공한다.

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

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

Theme.kt를 열고 JetnewTheme 컴포저블을 업데이트하여 Shapes를 사용할 수 있도록 한다.

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

Home.kt를 열고 미리보기를 새로고침하여, 추천 게시물을 표시하는 카드가 새로 적용된 모양 테마를 어떻게 반영하는지 확인하자.

다크 테마

앱에서 다크 테마 를 지원하면 앱이 사용자의 기기(Android 10부터 전역적으로 적용되는 다크 테마 토글이 있음)에 더 잘 통합될 뿐만 아니라 전력 사용량을 줄이고 접근성 요구 사항을 지원할 수 있다. Material은 다크 테마 생성에 대한 디자인 지침을 제공한다. 다음은 다크 테마에 대해 구현하려는 대체 색상 팔레트다.

2523f19026837a19.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
donaricano-btn

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.