Compose Theme

Material Theme

Compose는 기본적으로 하위 수준 API를 래핑하여 MaterialTheme라는 테마 시스템을 제공한다.

Material 2 및 Material 3 테마 시스템을 제공한다. Material 3 테마 시스템을 사용하려면 다음 의존성을 추가해야 한다.

implementation "androidx.compose.material3:material3:$material3_version"

Material 3 디자인 가이드라인은 https://m3.material.io/ 에서 확인할 수 있다.

구성 요소(Material 3)

  • Color scheme: 색상 시스템
  • Typography: 서체 시스템
  • Shapes: 도형 시스템

Customizing Theme

기본 제공되는 테마를 커스터마이징하거나 완전히 새로운 테마 시스템을 이용하여 UI를 제작할 수 있다.

시작하기 전에

CompositionLocal은 Compose 전역에 광범위하게 사용되는 값에 사용하기 적합하다. 이런 값은 대표적으로 현재 다루고 있는 Theme System에 관련된 값(Color, shape, …)에 사용하기 적합한 모델이다.

Material Theme 또한 CompositionLocal을 이용하여 구현이 되어 있으며 이를 확장하거나 새로운 테마를 만들기 위해서 CompositionLocal에 대한 이해가 필요하다.

Material Theme 구현 방식

Material Theme은 값을 어떤 형태로 저장하는가

//ColorScheme.kt
@Stable  
class ColorScheme(  
    primary: Color,  
    onPrimary: Color,  
    primaryContainer: Color,  
    onPrimaryContainer: Color,  
    /* ... */ 
    onErrorContainer: Color,  
    outline: Color,  
    outlineVariant: Color,  
    scrim: Color
)

ColorScheme이라는 클래스를 이용하여 Material Theme의 컬러 시스템을 담당하고 있다(Typography, Shapes 또한 유사한 방법으로 저장).

Compose 컴파일러의 Smart Recomposition 기능을 활용하기 위해 @Stable, @Immutable annotation을 이용한다.

값을 가져올 땐 어떻게 하는가 Composable 함수 내에서 MaterialTheme.colorScheme.primary를 통해 Primary Color를 가져올 수 있다. 이 방법은 CompositionLocal과 이를 래핑하는 MaterialTheme object를 통해 구현하고 있다.

internal val LocalColorScheme = staticCompositionLocalOf { lightColorScheme() }

LocalColorScheme이라는 Composition Local을 정의하고 있다(기본값을 제공하여 사용자가 MaterialTheme을 정의하지 않고 사용하더라도 기본 색상으로 사용 가능).

object MaterialTheme {   
		val colorScheme: ColorScheme  
        @Composable  
        @ReadOnlyComposable        
        get() = LocalColorScheme.current  
		
		val typography: Typography  
        @Composable  
        @ReadOnlyComposable        
        get() = LocalTypography.current      
        
        val shapes: Shapes  
        @Composable  
        @ReadOnlyComposable        
        get() = LocalShapes.current  
}

MaterialTheme.colorSchemeLocalColorScheme.current를 리턴하도록 하여 사용자가 MaterialTheme.colorScheme.primary와 같은 방법으로 Primary 색에 접근할 수 있도록 한다.

테마의 진입점이 되는 MaterialTheme Composable에서 CompositionLocal에 사용자가 정의한 colorScheme을 주입한다.

@Composable  
fun MaterialTheme(  
    colorScheme: ColorScheme = MaterialTheme.colorScheme,  
    shapes: Shapes = MaterialTheme.shapes,  
    typography: Typography = MaterialTheme.typography,  
    content: @Composable () -> Unit  
) {
	// ...
	CompositionLocalProvider(  
	    LocalColorScheme provides rememberedColorScheme,
	    /* ... */
	) {  
	    ProvideTextStyle(value = typography.bodyLarge, content = content)  
	}
}

Material Theme 확장

Material Theme에 추가 값을 적용하거나 래핑하여 테마를 확장하는 방법이 있다. 이 방법은 Material Themeing을 그대로 사용하여 제공된 Material Composable(Scaffold, Floting Action Button 등)을 사용하는 데 문제가 없으며 추가 값을 통해 확장할 수 있는 형태이다.

확장 함수를 이용한 정의

// 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)

확장 함수를 이용하여 기본 Material Theme 시스템에 값을 추가하여 사용할 수 있다. 큰 비용을 들이지 않고 Material theme 시스템을 그대로 사용하며 기능을 확장할 수 있다.

Wrap Material Theme Material Theme의 구조와 동일한 방법으로 Material Theme를 래핑하여 확장 테마 시스템을 정의할 수 있다.

@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  
}

확장된 Material Theme의 값을 Material Composables(Text, Button, TextField, …)에 이용하려면 Material Composable 함수를 래핑하여 새 Composable 함수를 만든다.

@Composable  
fun ExtendedButton(  
    onClick: () -> Unit,  
    modifier: Modifier = Modifier,  
    content: @Composable RowScope.() -> Unit  
) {  
    Button(  
        colors = ButtonDefaults.buttonColors(  
            backgroundColor = ExtendedTheme.colors.tertiary,  
            contentColor = ExtendedTheme.colors.onTertiary  
            /* Other colors use values from MaterialTheme */  
        ),  
        onClick = onClick,  
        modifier = modifier,  
        content = content  
    )  
}

Material Theme 를 대체하는 새로운 테마 시스템 제작

MaterialTheme를 (일부 또는 전부) 사용하지 않고 테마 시스템을 완전히 커스터마이징할 수 있다. 어떤 애플리케이션의 디자인은 Material Theme에 국한되지 않으며 전용 디자인 시스템을 갖추고 있을 경우에 사용하기 유용하다.

@Immutable  
data class CustomColors(  
    val content: Color,  
    val component: Color,  
    val background: List<Color>  
)  
  
@Immutable  
data class CustomTypography(  
    val body: TextStyle,  
    val title: TextStyle  
)  
  
@Immutable  
data class CustomElevation(  
    val default: Dp,  
    val pressed: Dp  
)  
  
val LocalCustomColors = staticCompositionLocalOf {  
    CustomColors(  
        content = Color.Unspecified,  
        component = Color.Unspecified,  
        background = emptyList()  
    )  
}  
val LocalCustomTypography = staticCompositionLocalOf {  
    CustomTypography(  
        body = TextStyle.Default,  
        title = TextStyle.Default  
    )  
}  
val LocalCustomElevation = staticCompositionLocalOf {  
    CustomElevation(  
        default = Dp.Unspecified,  
        pressed = Dp.Unspecified  
    )  
}  
  
@Composable  
fun CustomTheme(  
    /* ... */  
    content: @Composable () -> Unit  
) {  
    val customColors = CustomColors(  
        content = Color(0xFFDD0D3C),  
        component = Color(0xFFC20029),  
        background = listOf(Color.White, Color(0xFFF8BBD0))  
    )  
    val customTypography = CustomTypography(  
        body = TextStyle(fontSize = 16.sp),  
        title = TextStyle(fontSize = 32.sp)  
    )  
    val customElevation = CustomElevation(  
        default = 4.dp,  
        pressed = 8.dp  
    )  
    CompositionLocalProvider(  
        LocalCustomColors provides customColors,  
        LocalCustomTypography provides customTypography,  
        LocalCustomElevation provides customElevation,  
        content = content  
    )  
}  
  
// Use with eg. CustomTheme.elevation.small  
object CustomTheme {  
    val colors: CustomColors  
        @Composable  
        get() = LocalCustomColors.current  
    val typography: CustomTypography  
        @Composable  
        get() = LocalCustomTypography.current  
    val elevation: CustomElevation  
        @Composable  
        get() = LocalCustomElevation.current  
}

이 방법을 적용한 뒤 Material Composables을 그대로 사용하면 Material Theme의 기본 테마를 표시한다. 따라서 Material Composable를 적절히 래핑하거나 새로운 Composable을 만들어야 한다.

@Composable  
fun CustomButton(  
    onClick: () -> Unit,  
    modifier: Modifier = Modifier,  
    content: @Composable RowScope.() -> Unit  
) {  
    Button(  
        colors = ButtonDefaults.buttonColors(  
            backgroundColor = CustomTheme.colors.component,  
            contentColor = CustomTheme.colors.content,  
            disabledBackgroundColor = CustomTheme.colors.content  
                .copy(alpha = 0.12f)  
                .compositeOver(CustomTheme.colors.component),  
            disabledContentColor = CustomTheme.colors.content  
                .copy(alpha = ContentAlpha.disabled)  
        ),  
        shape = ButtonShape,  
        elevation = ButtonDefaults.elevation(  
            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)

© 2021. All rights reserved.

Powered by Hydejack v9.1.6