[메모] Compose 가이드 문서 ~ Theming

[메모] Compose 가이드 문서 ~ Theming

May 23, 2024. | By: pluulove


https://developer.android.com/develop/ui/compose/designsystems


Material Design 3

Compose Material 3 종속성 추가 필요

implementation “androidx.compose.material3:material3:$material3_version”

MaterialTheme(
   colorScheme = /* ...
   typography = /* ...
   shapes = /* ...
) {
   // M3 app content
}

Material theming

Color Scheme

// 낮밤에 대한 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
}

Typography

Material Design 2 + Type scale로 정의 (기본 15개의 Typo 제공)

  • display, headline, title, body, label
  • large, medium, small

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
)

Shapes

5개의 크기로 각 컴포넌트 모양을 정의 가능

  • Extra Small, Small, Medium, Large, Extra Large
   

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

디자인 강조 방법

  • surface 색상은 on-surface/on-surface-variant/surface-variant를 사용하여 강조
  • text : FontWeight를 사용하여 가중치 부여

Elevation

Material 3에서는 tonal color overlays를 사용하여 높이를 표현

Surface(
   modifier = Modifier,
   tonalElevation = /*...
   shadowElevation = /*...
) {
   Column(content = content)
}

Material components

  • NavigationBar : 5개 이하의 아이템 혹은 작은 단말
  • NavigationRail : 중소형 크기의 태블릿/휴대폰 단말
  • PermanentNavigationDrawer/ModalNavigationDrawer/NavigationRail : 중대형 크기의 태블릿

System UI

Ripple

  • 내부적으로 RippleDrawable 사용

Overscroll

  • 스크롤 끝의 늘리기 효과를 사용
  • LazyColumn, LazyRow, LazyVerticalGrid 에서 기본적으로 활성화

Material 2에서 Material 3으로 마이그레이션

하나의 앱에서 M2/M3를 혼용해서 사용해서는 안된다.

https://developer.android.com/develop/ui/compose/designsystems/material2-material3#m2_3

Material 2

MaterialTheme(
   colors = // ...
   typography = // ...
   shapes = // ...
) {
   // app content
}

Color

낮밤 테마 컬러 정의

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
)

Surface/contentColor

Composable의 색상을 설정 가능하며, 컨텐츠의 기본 색상도 제공 가능

Surface(
   color = MaterialTheme.colors.surface,
   contentColor = contentColorFor(color),
   // ...
) { /* ... */ }

TopAppBar(
   backgroundColor = MaterialTheme.colors.primarySurface,
   contentColor = contentColorFor(backgroundColor),
   // ...
) { /* ... */ }

실제 contentColorFor는 이미 정의된 Color에 맞는 contentColor를 가져오는 형태로 구현되어 있음.

색상을 찾지 못하면 Color.Unspecified가 사용됨

Content alpha

CompositionLocal을 사용하여 LocalContentAlpha에 값을 지정

CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
   Text(
      // ...
   )
}

MaterialTheme의 LocalContentAlpha 기본 값은 ContentAlpha.high

Dark theme

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

컴포넌트에 따라서 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
   )
}

Theme overlays

View 기반 화면을 Compose로 이전할 때 android:theme 속성을 대신, Compose UI 트리의 관련 부분에 새로운 MaterialTheme로 정의가 필요할 수 있다.

@Composable
fun DetailsScreen(/* ... */) {
   PinkTheme {
      // other content
      RelatedSection()
   }
}

@Composable
fun RelatedSection(/* ... */) {
   BlueTheme {
      // content
   }
}

Component states

컴포넌트 상태에 따라서 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
   )
) { /* ... */ }

Ripple

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

맞춤 테마

커스텀 디자인 시스템을 만드는 방법

MaterialTheme 확장

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

Material System 교체

자체 Composable 함수로 래핑하여 관련 시스템의 값을 직접 설정

  • ProvideTextStyle를 사용하여 텍스트 스타일을 지정 가능 -> LocalTextStyle로 사용됨
@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)

XML 테마를 Compose로 마이그레이션

Material Theme Builder를 사용하여 XML 테마에서 Compose 테마로 마이그레이션 가능

comments powered by Disqus

Currnte Pages Tags

Android AndroidX Compose

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links