1. 소개
이 Codelab에서는 Jetpack Compose의 테마 설정 API를 사용하여 애플리케이션의 스타일을 지정하는 방법을 알아봅니다. 밝은 테마와 어두운 테마 등 여러 테마를 지원하여 애플리케이션 전체에서 일관되게 사용되도록 색상과 도형, 서체를 맞춤설정하는 방법을 알아봅니다.
학습할 내용
이 Codelab에서는 다음에 관해 알아봅니다.
- Material Design 기본 지침서 및 브랜드에 맞게 맞춤설정하는 방법
- Compose에서 Material Design 시스템을 구현하는 방법
- 앱 전체에서 색상과 서체, 도형을 정의하고 사용하는 방법
- 구성요소의 스타일을 지정하는 방법
- 밝은 테마와 어두운 테마를 지원하는 방법
빌드할 항목
이 Codelab에서는 뉴스 읽기 앱의 스타일을 지정합니다. 스타일이 지정되지 않은 애플리케이션으로 시작하여 학습한 내용을 애플리케이션 테마 설정에 적용하고 어두운 테마를 지원합니다.
이전: 스타일이 지정되지 않은 앱 | 이후: 스타일이 지정된 앱 | 이후: 어두운 테마 |
기본 요건
- 람다를 포함한 Kotlin 문법 사용 경험
- Compose에 관한 기본 이해
- Compose 레이아웃(예:
Row
,Column
,Modifier
)에 관한 기본 지식
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개 있습니다. 구성을 선택하고 실행 버튼을 누르면 기기나 에뮬레이터에 코드가 배포됩니다.
애플리케이션에는 Compose 레이아웃 미리보기도 포함되어 있습니다. start
/finish
패키지에서 Home.kt
로 이동하여 디자인 보기를 열면 UI 코드에서 빠르게 반복할 수 있는 여러 미리보기가 표시됩니다.
3. Material Theming
Jetpack Compose는 디지털 인터페이스를 만들기 위한 포괄적인 디자인 시스템인 Material Design 구현을 제공합니다. Material Design 구성요소(버튼, 카드, 스위치 등)는 제품 브랜드를 효과적으로 반영하기 위해 Material Design을 체계적으로 맞춤설정하는 Material Theming을 기반으로 빌드됩니다. Material 테마는 색상, 서체, 도형 속성으로 구성됩니다. 이를 맞춤설정하면 앱을 빌드하는 데 사용하는 구성요소에 자동으로 반영됩니다.
Material Theming에 관한 이해는 Jetpack Compose 앱의 테마를 설정하는 방법을 이해하는 데 도움이 되므로 여기서는 개념을 간략하게 설명합니다. Material Theming에 이미 익숙하다면 앞으로 건너뛰어도 됩니다.
색상
Material Design은 앱 전체에서 사용할 수 있는, 의미론적으로 이름이 지정된 여러 색상을 정의합니다.
기본 색상은 주요 브랜드 색상이며 보조 색상은 강조 표시에 사용됩니다. 대비되는 영역에 더 어둡거나 밝은 변형을 제공할 수 있습니다. 배경과 표면 색상은 애플리케이션의 '표면'에 개념적으로 존재하는 구성요소를 보유한 컨테이너에 사용됩니다. 또한 Material은 'on' 색상을 정의합니다. 이 색상은 이름이 지정된 색상 중 하나 위에 있는 콘텐츠에 사용됩니다(예: '표면' 색상 컨테이너의 텍스트는 'on surface'에 색상이 지정되어야 함). Material 구성요소는 이러한 테마 색상을 사용하도록 구성됩니다. 예를 들어 기본적으로 플로팅 작업 버튼은 secondary
색상이 지정되고 카드는 surface
로 기본값이 설정됩니다.
이름이 지정된 색상을 정의하면 밝은 테마 및 어두운 테마 둘 다와 같은 대체 색상 팔레트를 제공할 수 있습니다.
작은 색상 팔레트를 정의하여 앱 전체에서 일관되게 사용할 수도 있습니다. Material 색상 도구를 사용하면 쉽게 색상을 선택하여 색상 팔레트를 만들 수 있어 조합에도 액세스할 수 있도록 합니다.
서체
마찬가지로 Material은 의미론적으로 이름이 지정된 여러 서체 스타일을 정의합니다.
테마별로 서체 스타일을 변경할 수는 없지만 서체 스케일을 사용하면 애플리케이션 내에서 일관성이 높아집니다. 자체 글꼴 및 기타 맞춤설정 서체를 제공하면 앱에서 사용하는 Material 구성요소에 반영됩니다(예: 앱 바는 기본적으로 h6
스타일 사용, 버튼은 button
사용). Material 서체 스케일 생성기 도구를 사용하면 서체 스케일을 빌드하는 데 도움이 됩니다.
도형
Material은 도형을 체계적으로 사용하여 브랜드를 전달할 수 있도록 지원합니다. 소형, 중형, 대형 구성요소라는 3가지 카테고리를 정의합니다. 각각은 모서리 스타일(잘리거나 둥근 스타일)과 크기를 맞춤설정하여 사용할 도형을 정의할 수 있습니다.
도형 테마를 맞춤설정하면 다양한 구성요소에 반영됩니다. 예를 들어 버튼과 텍스트 필드는 소형 도형 테마를 사용하고 카드와 대화상자는 중형 도형 테마를 사용하며 시트는 대형 도형 테마를 기본적으로 사용합니다. 여기에서 도형 테마에 관한 구성요소의 전체 매핑을 확인할 수 있습니다. 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
로 전환하여 MaterialTheme
을 JetnewsTheme
으로 바꾸고 가져옵니다.
- MaterialTheme {
+ JetnewsTheme {
...
이 화면의 @Preview
에는 아직 변경사항이 표시되지 않습니다. PostItemPreview
및 FeaturedPostPreview
를 업데이트하여 새로운 JetnewsTheme
컴포저블로 콘텐츠를 래핑합니다. 그러면 미리보기에서 새 테마를 사용할 수 있습니다.
@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
FeaturedPost(post = post)
+ }
}
색상
다음은 앱에서 구현하려는 색상 팔레트입니다. 지금은 밝은 팔레트지만 뒷부분에서 어두운 테마도 다룰 것입니다.
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
와 같은 구성요소에 반영되었습니다.
서체
다음은 앱에서 구현하고자 하는 서체 스케일입니다.
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
를 열고 미리보기를 새로고침하여 새 서체가 적용되는지 확인합니다.
도형
도형을 사용하여 앱에서 브랜드를 표현하고자 합니다. 다음과 같이 여러 요소에 잘린 모서리 도형을 사용합니다.
Compose는 도형 테마를 정의하는 데 사용할 수 있는 RoundedCornerShape
및 CutCornerShape
클래스를 제공합니다.
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은 어두운 테마를 만드는 방법에 관한 디자인 안내를 제공합니다. 다음은 어두운 테마에 구현하려는 대체 색상 팔레트입니다.
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)
}
}
미리보기 창을 새로고침하여 어두운 테마 미리보기를 확인합니다.
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
를 사용합니다. 즉, 테마에 여러 색상 세트를 제공하여 다양한 디자인과 분위기를 지원할 수 있으므로 애플리케이션 코드를 터치할 필요가 없습니다. 예를 들어 AppBar
는 primary
색상을 사용하고 화면 배경은 surface
입니다. 테마 색상을 변경하면 다음 컴포저블에 반영됩니다.
테마의 각 색상이 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
배경이 있습니다. 이는 밝은 테마에서는 괜찮아 보이지만 어두운 테마의 배경에서는 고대비를 이룹니다. 또한 특정 텍스트 색상을 지정하지 않으므로 배경과 대비되지 않을 수 있는 현재 콘텐츠 색상을 상속받습니다.
이 문제를 해결해 보겠습니다. Home.kt
의 Header
컴포저블에서 하드 코딩 색상을 지정하는 background
수정자를 삭제합니다. 대신 Surface
의 Text
를 테마에서 파생된 색상으로 래핑하고 콘텐츠의 색상을 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
값을 제공하여 계층 구조의 콘텐츠 알파를 지정할 수 있습니다. 하위 컴포저블에서 이 값을 사용합니다. 예를 들어 Text
및 Icon
은 기본적으로 LocalContentAlpha
를 사용하도록 조정된 LocalContentColor
조합을 사용합니다. Material에서는 ContentAlpha
객체에 의해 모델링된 일부 표준 알파 값(high
, medium
, disabled
)을 지정합니다. MaterialTheme
은 LocalContentAlpha
의 기본값을 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.kt
의 PostMetadata
컴포저블에서 메타데이터 medium
을 강조합니다.
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = text,
modifier = modifier
)
+ }
어두운 테마
앞서 확인했듯이 Compose에서 어두운 테마를 구현하려면 다양한 색상 세트를 제공하고 테마를 통해 색상을 쿼리하기만 하면 됩니다. 다음 예외사항에 주의하세요.
밝은 테마에서 실행 중인지 확인할 수 있습니다.
val isLightTheme = MaterialTheme.colors.isLight
이 값은 lightColors/darkColors 빌더 함수로 설정됩니다.
머티리얼의 어두운 테마에서는 고도가 높은 표면이 고도 오버레이를 수신합니다(배경이 밝아짐). 이는 어두운 색상 팔레트를 사용하면 자동으로 구현됩니다.
Surface(
elevation = 2.dp,
color = MaterialTheme.colors.surface, // color will be adjusted for elevation
...
사용 중인 TopAppBar
및 Card
구성요소에서 모두 앱의 이러한 자동 동작을 확인할 수 있습니다. 기본적으로 고도가 4dp와 1dp이므로 이 고도를 잘 전달할 수 있도록 어두운 테마에서 배경이 자동으로 밝아집니다.
Material Design은 어두운 테마에서 넓은 밝은 색상 영역을 피할 것을 권장합니다. 일반적인 패턴은 밝은 테마에서 컨테이너 primary
색상을 지정하고 어두운 테마에서 surface
색상을 지정하는 것입니다. 앱 바와 하단 탐색 메뉴와 같은 많은 구성요소에서 기본적으로 이 전략을 사용합니다. 이를 더 쉽게 구현할 수 있도록 Colors
는 정확하게 이 동작을 제공하는 primarySurface
색상을 제공하고 이러한 구성요소에서 기본적으로 사용됩니다.
앱은 현재 앱 바를 primary
색상으로 설정하고 있지만 primarySurface
로 전환하거나 기본값이므로 이 매개변수를 삭제하여 이 안내를 따를 수 있습니다. AppBar
컴포저블에서 TopAppBar
의 backgroundColor
매개변수를 변경합니다.
@Composable
private fun AppBar() {
TopAppBar(
...
- backgroundColor = MaterialTheme.colors.primary
+ backgroundColor = MaterialTheme.colors.primarySurface
)
}
6. 텍스트 사용
텍스트를 사용할 때는 Text
컴포저블을 사용하여 텍스트를 표시하고 TextField
및 OutlinedTextField
를 텍스트 입력에 사용하며 TextStyle
을 사용하여 텍스트에 단일 스타일을 적용합니다. AnnotatedString
을 사용하여 텍스트에 여러 스타일을 적용할 수 있습니다.
색상에서 확인했듯이 텍스트를 표시하는 Material 구성요소는 테마 서체 맞춤설정을 선택합니다.
Button(...) {
Text("This text will use MaterialTheme.typography.button style by default")
}
이는 색상에서 확인한 것처럼 기본 매개변수 사용보다 약간 더 복잡합니다. 구성요소가 텍스트를 직접 표시하지 않는 경향이 있기 때문입니다. 대신 Text
컴포저블을 전달할 수 있는 '슬롯 API'를 제공합니다. 그렇다면 구성요소가 테마 서체 스타일을 설정하는 방법은 무엇일까요? 내부적으로는 ProvideTextStyle
컴포저블(자체적으로 CompositionLocal
사용)을 사용하여 '현재' TextStyle
을 설정합니다. Text
컴포저블은 구체적인 textStyle
매개변수를 제공하지 않으면 이 '현재' 스타일을 쿼리하도록 기본 설정됩니다.
Compose의 Button
및 Text
클래스의 예는 다음과 같습니다.
@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.typography
는 MaterialTheme
컴포저블에 설정된 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
테마가 자동으로 적용됩니다. 예를 들어 TopAppBar
는 title
의 스타일을 h6
로 지정하고 ListItem
은 기본 및 보조 텍스트의 스타일을 subtitle1
및 body2
로 각각 지정합니다.
나머지 앱에 테마 서체 스타일을 적용해 보겠습니다. subtitle2
를 사용하도록 Header
를 설정하고 FeaturedPost
의 텍스트는 제목에 h6
, 저자와 메타데이터에 body2
를 사용하도록 설정합니다.
@Composable
fun Header(...) {
...
Text(
text = text,
+ style = MaterialTheme.typography.subtitle2
여러 스타일
일부 텍스트에 여러 스타일을 적용해야 하는 경우 마크업을 적용하는 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()} ")
+ }
}
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
)
) {
테마 도형
물론 자체 구성요소를 만들 때 도형(예: 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)
)
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의 구성요소를 맞춤설정할 수 있습니다. 예를 들어 Button
은 ProvideTextStyle
컴포저블을 사용하여, 전달된 콘텐츠의 기본 텍스트 스타일을 설정했습니다. 똑같은 메커니즘을 사용하여 자체 텍스트 스타일을 설정할 수 있습니다.
@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