https://developer.android.com/develop/ui/compose/designsystems
Compose Material 3 종속성 추가 필요
implementation “androidx.compose.material3:material3:$material3_version”
MaterialTheme(
colorScheme = /* ...
typography = /* ...
shapes = /* ...
) {
// M3 app content
}
// 낮밤에 대한 ColorScheme 정의
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
// ..
)
private val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
// ..
)
@Composable
fun ReplyTheme(
darkTheme: Boolean = isSystemInDarkTheme(), // 다크 모드 활성화 여부
content: @Composable () -> Unit
) {
val colorScheme =
if (!darkTheme) {
LightColorScheme
} else {
DarkColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
// 컬러 사용
Text(
text = "Hello theming",
color = MaterialTheme.colorScheme.primary
)
isSystemInDarkTheme() 정의 내부적으로 LocalConfiguration의 uiMode에 NIGHT로 체크 중
@Composable
@ReadOnlyComposable
internal actual fun _isSystemInDarkTheme(): Boolean {
val uiMode = LocalConfiguration.current.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
Dynamic color schemes
Dynamic color는 지원 여부를 체크하여 처리 가능
// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colors = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
Material Design 2 + Type scale로 정의 (기본 15개의 Typo 제공)
typography 정의 : Font 스타일 및 글꼴
val replyTypography = Typography(
titleLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.SansSerif,
fontStyle = FontStyle.Italic,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp,
baselineShift = BaselineShift.Subscript
),
// ..
)
MaterialTheme(
typography = replyTypography,
) {
// M3 app Content
}
// 텍스트 스타일 사용
Text(
text = "Hello M3 theming",
style = MaterialTheme.typography.titleLarge
)
5개의 크기로 각 컴포넌트 모양을 정의 가능
shape 정의
val replyShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp)
)
MaterialTheme(
shapes = replyShapes,
) {
// M3 app Content
}
// Shape 사용
Card(shape = MaterialTheme.shapes.medium) { /* card content */ }
이미 정의된 Shape
Material 3에서는 tonal color overlays를 사용하여 높이를 표현
Surface(
modifier = Modifier,
tonalElevation = /*...
shadowElevation = /*...
) {
Column(content = content)
}
하나의 앱에서 M2/M3를 혼용해서 사용해서는 안된다.
https://developer.android.com/develop/ui/compose/designsystems/material2-material3#m2_3
MaterialTheme(
colors = // ...
typography = // ...
shapes = // ...
) {
// app content
}
낮밤 테마 컬러 정의
private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...
private val DarkColors = darkColors(
primary = Yellow200,
secondary = Blue200,
// ...
)
private val LightColors = lightColors(
primary = Yellow500,
primaryVariant = Yellow400,
secondary = Blue700,
// ...
)
// 테마에 컬러 적용
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors
) {
// app content
}
// 컬러 사용
Text(
text = "Hello theming",
color = MaterialTheme.colors.primary
)
Composable의 색상을 설정 가능하며, 컨텐츠의 기본 색상도 제공 가능
Surface(
color = MaterialTheme.colors.surface,
contentColor = contentColorFor(color),
// ...
) { /* ... */ }
TopAppBar(
backgroundColor = MaterialTheme.colors.primarySurface,
contentColor = contentColorFor(backgroundColor),
// ...
) { /* ... */ }
실제 contentColorFor는 이미 정의된 Color에 맞는 contentColor를 가져오는 형태로 구현되어 있음.
색상을 찾지 못하면 Color.Unspecified가 사용됨
CompositionLocal을 사용하여 LocalContentAlpha에 값을 지정
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
// ...
)
}
MaterialTheme의 LocalContentAlpha 기본 값은 ContentAlpha.high
// MaterialTheme에 Light/Dark theme에 따른 Colors 정의
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
/*...*/
content = content
)
}
// 테마에 따른 Drawable 리소스 지정
val isLightTheme = MaterialTheme.colors.isLight
Icon(
painterResource(
id = if (isLightTheme) {
R.drawable.ic_sun_24
} else {
R.drawable.ic_moon_24
}
),
contentDescription = "Theme"
)
Elevation overlays
Surface(
elevation = 2.dp,
color = MaterialTheme.colors.surface, // elevation에 따라 색상이 조정
/*...*/
) { /*...*/ }
// Elevation overlays
// Surface에서 구현
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
color, elevation
)
// 동작을 중지하려면 null을 제공
CompositionLocalProvider(LocalElevationOverlay provides null) {
// Content without elevation overlays
}
강조색 표현
Surface(
color = MaterialTheme.colors.primarySurface,
/*...*/
) { /*...*/ }
컴포넌트에 따라서 Shape가 적용됨
https://m2.material.io/design/shape/applying-shape-to-ui.html#shape-scheme
val shapes = Shapes(
small = RoundedCornerShape(percent = 50),
medium = RoundedCornerShape(0f),
large = CutCornerShape(
topStart = 16.dp,
topEnd = 0.dp,
bottomEnd = 0.dp,
bottomStart = 16.dp
)
)
MaterialTheme(shapes = shapes, /*...*/) {
/*...*/
}
기본 스타일 컴포넌트 정의
새로운 버튼 스타일을 만들려면 Button을 Composable 함수로 감싸고, 변경하려는 파라미터 정의 및 노출하면 된다.
@Composable
fun MyButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = onClick,
modifier = modifier,
content = content
)
}
View 기반 화면을 Compose로 이전할 때 android:theme
속성을 대신, Compose UI 트리의 관련 부분에 새로운 MaterialTheme로 정의가 필요할 수 있다.
@Composable
fun DetailsScreen(/* ... */) {
PinkTheme {
// other content
RelatedSection()
}
}
@Composable
fun RelatedSection(/* ... */) {
BlueTheme {
// content
}
}
컴포넌트 상태에 따라서 color/elevation을 적용 가능
Button(
onClick = { /* ... */ },
enabled = true,
// Custom colors for different states
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary,
disabledBackgroundColor = MaterialTheme.colors.onBackground
.copy(alpha = 0.2f)
.compositeOver(MaterialTheme.colors.background)
// Also contentColor and disabledContentColor
),
// Custom elevation for different states
elevation = ButtonDefaults.elevation(
defaultElevation = 8.dp,
disabledElevation = 2.dp,
// Also pressedElevation
)
) { /* ... */ }
MaterialTheme을 사용하는 경우 clickable/indication과 같은 Modifier에서 리플이 사용됨
RippleTheme를 사용하여 color/alpha 변경 가능
@Composable
fun MyApp() {
MaterialTheme {
CompositionLocalProvider(
LocalRippleTheme provides SecondaryRippleTheme
) {
// App content
}
}
}
@Immutable
private object SecondaryRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = RippleTheme.defaultRippleColor(
contentColor = MaterialTheme.colors.secondary,
lightTheme = MaterialTheme.colors.isLight
)
@Composable
override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
contentColor = MaterialTheme.colors.secondary,
lightTheme = MaterialTheme.colors.isLight
)
}
커스텀 디자인 시스템을 만드는 방법
1) 추가 값은 MaterialTheme를 사용하여 Color, Typo, Shape를 확장
// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
get() = if (isLight) Red300 else Red700
// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
get() = TextStyle(/* ... */)
// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
get() = RoundedCornerShape(size = 20.dp)
2) MaterialTheme를 랩핑하여 값을 정의
@Immutable
data class ExtendedColors(
val tertiary: Color,
val onTertiary: Color
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
tertiary = Color.Unspecified,
onTertiary = Color.Unspecified
)
}
@Composable
fun ExtendedTheme(
/* ... */
content: @Composable () -> Unit
) {
val extendedColors = ExtendedColors(
tertiary = Color(0xFFA8EFF0),
onTertiary = Color(0xFF002021)
)
CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
MaterialTheme(
/* colors = ..., typography = ..., shapes = ... */
content = content
)
}
}
// Use with eg. ExtendedTheme.colors.tertiary
object ExtendedTheme {
val colors: ExtendedColors
@Composable
get() = LocalExtendedColors.current
}
@Composable
fun ExtendedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = ExtendedTheme.colors.tertiary,
contentColor = ExtendedTheme.colors.onTertiary
/* Other colors use values from MaterialTheme */
),
onClick = onClick,
modifier = modifier,
content = content
)
}
자체 Composable 함수로 래핑하여 관련 시스템의 값을 직접 설정
@Immutable
data class ReplacementTypography(
val body: TextStyle,
val title: TextStyle
)
val LocalReplacementTypography = staticCompositionLocalOf {
ReplacementTypography(
body = TextStyle.Default,
title = TextStyle.Default
)
}
@Composable
fun ReplacementTheme(
/* ... */
content: @Composable () -> Unit
) {
val replacementTypography = ReplacementTypography(
body = TextStyle(fontSize = 16.sp),
title = TextStyle(fontSize = 32.sp)
)
CompositionLocalProvider(
LocalReplacementTypography provides replacementTypography
) {
MaterialTheme(
/* colors = ... */
content = content
)
}
}
// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
val typography: ReplacementTypography
@Composable
get() = LocalReplacementTypography.current
}
@Composable
fun ReplacementButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
shape = ReplacementTheme.shapes.component,
onClick = onClick,
modifier = modifier,
content = {
ProvideTextStyle(
value = ReplacementTheme.typography.body
) {
content()
}
}
)
}
기존 시스템을 수정하고 새로운 class/type으로 새로운 시스템 테마를 정의 가능
@Composable
fun CustomButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = CustomTheme.colors.component,
contentColor = CustomTheme.colors.content,
disabledContainerColor = CustomTheme.colors.content
.copy(alpha = 0.12f)
.compositeOver(CustomTheme.colors.component),
disabledContentColor = CustomTheme.colors.content
.copy(alpha = ContentAlpha.disabled)
),
shape = ButtonShape,
elevation = ButtonDefaults.elevatedButtonElevation(
defaultElevation = CustomTheme.elevation.default,
pressedElevation = CustomTheme.elevation.pressed
/* disabledElevation = 0.dp */
),
onClick = onClick,
modifier = modifier,
content = {
ProvideTextStyle(
value = CustomTheme.typography.body
) {
content()
}
}
)
}
val ButtonShape = RoundedCornerShape(percent = 50)
Material Theme Builder를 사용하여 XML 테마에서 Compose 테마로 마이그레이션 가능
comments powered by Disqus
Subscribe to this blog via RSS.
LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유
Posted on 30 Nov 2024