기본적으로 Android Studio에서 Base Compose Activity를 생성하면 다음과 같은 기본적인 파일을 얻을 수 있습니다.
setContent{
ProjectTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background){
Greeting("ANDROID")
}
}
}
위의 Theme 내부에 있는 요소들은 Material 디자인 가이드에 따라, 앱에 일관된 모양이나 Typography, Color, Shape들을 제공할 수 있는 파일들이다.
코드를 보면 ProjectTheme{}안에 우리가 원하는 Composable을 넣는 것을 볼 수 있다.
저 ProjectTheme{}는 무엇일까?
Theme{}?
@Composable
fun ProjectTheme(darkTheme: Boolean= isSystemInDarkTheme(), content : @Composable () -> Unit){
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors= colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
Theme.kt를 들어가면 생성한 프로젝트 이름으로 Theme컴포저블 함수가 생성된 것을 볼 수 있다. 코드는 현재 시스템의 모드에 따라서 다크 컬러나 밝은 컬러로 설정하고 MaterialTheme라는 컴포저블의 매개변수로 넣어준다.
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
) {
val rememberedColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [colors]
// provided, and overwrite the values set in it.
colors.copy()
}.apply { updateColorsFrom(colors) }
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColors)
CompositionLocalProvider(
LocalColors provides rememberedColors,
LocalContentAlpha provides ContentAlpha.high,
LocalIndication provides rippleIndication,
LocalRippleTheme provides MaterialRippleTheme,
LocalShapes provides shapes,
LocalTextSelectionColors provides selectionColors,
LocalTypography provides typography
) {
ProvideTextStyle(value = typography.body1) {
PlatformMaterialTheme(content)
}
}
}
MaterialTheme는 위와 같이 작성이 되어있다.
MaterialTheme의 color, typography, shapes 을 인자로 받고, content에 CompositionLocalProvider를 통해서 제공을 하고 있고 우리는 다음과 같이 접근하여 사용할 수 있다.
@Composable
fun SomethingText(){
Text(text = "hi", color = MaterialTheme.colors.primary)
}
그럼 CompositionLocalProvider는 무엇일까?
CompositionLocal
Compose에서 데이터는 각 Composable함수의 매개변수로 UI트리를 통해 아래로 흐르게 된다. 따라서 컴포저블의 필요한 값이 명시적으로 매개변수에 쓰이게 되는데, 색상이나 스타일 등등과 같이 매우 자주 널리 사용되는 데이터의 경우 계속 넘겨주는게 번거로울 수 있다.
@Composable
fun MyApp() {
// Theme information tends to be defined near the root of the application
val color = …
SomeTextLabel(labelText = "somthing", color = color)
}
// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String, color : Color) {
Text(
text = labelText,
color = color// ← need to access colors here
)
}
색상 등등 대부분의 컴포저블에 명시적 매개변수로 전달할 필요가 없도록 지원하기 위해서 Compose는 CompositionLocal을 제공한다.
이를 통해서 UI 트리를 통해 데이터 흐름이 발생하는 암시적 방법으로 사용할 수 있는 트리 범위의 명명된 객체를 만들 수 있다.
CompositionLocal 요소는 특정 값과 함께제공되고 이것은 Composable함수에서 매개변수로 선언하지 않아도 사용할 수 있다.
CompositionLocal은 Material Theme에서 내부적으로 사용하는 것이다.
MaterialTHEME는 세 개의 CompositionLocal 인스턴스(색상, 도형, 서체)를 제공하는 객체다.
각각 LocalColors, LocalShapes, LocalTypography 속성으로 , 각각 MaterialTheme.(colors, shapes, typograhpy).something으로 액세스할 수 있다.
이제 CompositionLocalProvider가 무엇이냐면, 새값을 CompositionLocal에 제공해주는 함수입니다.
CompositionLocal key를 value에 연결하는 provides 중위함수를 사용한다고 합니다.
CompositionLocalProvider의 content 람다는 CompositionLocal의 current속성에 액세스 할 때 제공된 값을 가져옵니다.
@Composable
fun CompositionLocalExample() {
MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProviders also work across composable functions
Text("This Text uses the disabled alpha now")
}
CompositionLocalProvider를 통해서 contentAlpha값에 여러가지 값을 제공할 수 있다.
CompositionLocal에 대한 추가적인 설명은 글의 하단에 링크로 남겨진 공식문서를 읽는 것을 추천합니다.
MaterialTheme Color수정
이제 다시 테마이야기로 넘어와서 우리는 Shape, Typography, Shape를 설정하고 수정할 수 있다.
Material의 디자인 가이드를 따르고 있는 디자인 시스템이라면 수정하기가 간단할 것이다.
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
이미 제공되는 스키마를 수정하면 되기 때문입니다.
기본적으로 lightColorScheme , darkColorScheme를 제공하는데, Material color를 따르지 않는 여러가지 컬러 스키마를 제공하고 싶다면, Custom한 ColorScheme를 사용해야할 것입니다.
다음으로는 CustomColorScheme를 Theme에 따라 해당값들을 제공해보는 코드를 만들어보겠습니다.
Custom ColorScheme만들기
1. 첫번째로 사용할 컬러를 정의합니다.
val Red1 = Color(0xFFFF0000)
val Red2 = Color(0xFFFF06A4)
val Red3 = Color(0xFFB80F0F)
val Red4 = Color(0xFFFF3B3B)
val Blue1 = Color(0xFF3F51B5)
val Blue2 = Color(0xFF2196F3)
val Blue3 = Color(0xFF03A9F4)
val Blue4 = Color(0xFF00BCD4)
val Yellow1 = Color(0xFFFFEB3B)
val Yellow2 = Color(0xFFFFC107)
val Yellow3 = Color(0xFFFF9800)
val Yellow4 = Color(0xFFFFF495)
val Green1 = Color(0xFF4CAF50)
val Green2 = Color(0xFF8BC34A)
val Green3 = Color(0xFFC8EBCA)
val Green4 = Color(0xFF07A293)
Color스키마를 총 4개를 만들어서
RED BLUE YELLOE GREEN 테마를 적용할것이기때문에 위와 같이 추가했습니다.
2. 두번째로 ColorScheme라는 class를 만들어줍니다. ( 이름은 자유 )
@Stable
class ColorScheme(
primary: Color, // 이름은 알아서 custom해도됩니당
secondary: Color,
tertiary: Color,
surface: Color,
) {
var primary by mutableStateOf(primary, structuralEqualityPolicy())
internal set
var secondary by mutableStateOf(secondary, structuralEqualityPolicy())
internal set
var tertiary by mutableStateOf(tertiary, structuralEqualityPolicy())
internal set
var surface by mutableStateOf(surface, structuralEqualityPolicy())
internal set
fun copy(
primary: Color = this.primary,
secondary: Color = this.secondary,
tertiary: Color = this.tertiary,
surface: Color = this.surface,
): ColorScheme = ColorScheme(
primary = primary,
secondary = secondary,
tertiary = tertiary,
surface = surface,
)
}
MaterialTheme.colors를 대체하는, 컬러스키마라는 클래스를 만들어주었습니다. 내부코드는 MaterialTheme.color안의 코드와 동일하고, 컬러이름들은 자유롭게 지정해도됩니다.
3. 세번째로 ColorScheme를 토대로 여러가지 컬러스키마를 만들어줍니다.
fun redColorScheme(
primary: Color = Red1,
secondary: Color = Red2,
tertiary: Color = Red3,
surface: Color = Red4,
): ColorScheme = ColorScheme(
primary = primary,
secondary = secondary,
tertiary = tertiary,
surface = surface,
)
4. 네번째로 현제 테마에 따른 컬러 스키마를 제공하는 함수를 만듭니다.
fun getCustomColor(currentTheme: ThemeColor): ColorScheme {
return when (currentTheme) {
ThemeColor.RED -> {
redColorScheme()
}
ThemeColor.GREEEN -> {
greenColorScheme()
}
ThemeColor.YELLOW -> {
yellowColorScheme()
}
ThemeColor.BLUE -> {
blueColorScheme()
}
}
}
5. 다섯번째로 CompositionLocalProvider로 제공할 컬러스키마를 CompositionLocal로 만듭니다.
val LocalCustomColor = staticCompositionLocalOf {
redColorScheme()
}
CustomTheme만들기
아까 위에서 보았던 MaterialTheme를 토대로 CustomTheme 컴포저블을 만들어 보겠습니다.
@Composable
fun CustomTheme(
colors: ColorScheme, // custom color scheme
typography: OmyudaTypography = omyudaTextStyle, // custom typography
content: @Composable () -> Unit,
) {
val rememberedColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [colors]
// provided, and overwrite the values set in it.
colors.copy()
}.apply { updateColorsFrom(colors) }
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColors)
CompositionLocalProvider(
LocalCustomColor provides rememberedColors,
LocalContentAlpha provides ContentAlpha.high,
LocalIndication provides rippleIndication,
LocalCustomTypography provides typography,
LocalTextSelectionColors provides selectionColors,
LocalRippleTheme provides MaterialRippleTheme,
) {
ProvideTextStyle(value = typography.bodyNormal12) {
content()
}
}
}
internal로 인하여 접근이 되지 않는 코드들은 직접 복사해서 사용했습니다. ex) rememberTextSelectionColors
ThemeObject만들기
마지막으로 컴포저블에서 테마 시스템으로부터 액세스를 편하게 하기 위해서, 테마함수와 동일한 이름을 갖는 테마객체를 생성합니다.
object ProjectTheme{
val color : ColorScheme
@Composable
get() = LocalCustomColor.current
val typograhpy : OmyudaTypograhpy
@Composable
get() = LocalCustomTypography.current
}
ProjectTheme Composable
@Composable
fun ProjectTheme(currentTheme: ThemeColor , content : @Compsoable () ->Unit){
val colors= getCustomColor(currentTheme)
CustomTheme(
colors = colors,
typograhpy = ProjectTheme.typography
content = content
)
}
이제 현재 테마에 따라 컬러 스키마를 가져와서 이를 사용하는 모든 컴포저블에 테마별로 색상이 변경될 수 있게 되었습니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var currentThemeColor by remember { mutableStateOf(ThemeColor.RED) }
ThemeComposeTheme(
currentTheme = currentThemeColor,
) {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting(changeTheme = {
currentThemeColor = it
})
}
}
}
}
}
@Composable
fun Greeting(
changeTheme: (ThemeColor) -> Unit,
) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(
modifier = Modifier.width(100.dp).clickable {
changeTheme(ThemeColor.RED)
}.background(color = ThemeComposeTheme.color.primary),
) {
Text(style = ThemeComposeTheme.typography.bodyNormal14, text = "빨강 테마를 적용할꺼야")
}
Row(
modifier = Modifier.width(100.dp).clickable {
changeTheme(ThemeColor.BLUE)
}.background(color = ThemeComposeTheme.color.secondary),
) {
Text(style = ThemeComposeTheme.typography.bodyNormal16, text = "파랑 테마를 적용할꺼야")
}
Row(
modifier = Modifier.width(100.dp).clickable {
changeTheme(ThemeColor.YELLOW)
}.background(color = ThemeComposeTheme.color.tertiary),
) {
Text(style = ThemeComposeTheme.typography.bodyNormal12, text = "노랑 테마를 적용할꺼야")
}
Row(
modifier = Modifier.width(100.dp).clickable {
changeTheme(ThemeColor.GREEEN)
}.background(color = ThemeComposeTheme.color.surface),
) {
Text(
style = ThemeComposeTheme.typography.bodyNormal16,
text = "초록 테마를 적용할꺼야",
)
}
}
}
enum class ThemeColor {
RED, BLUE, YELLOW, GREEEN
}
짜잔 혹시나 앱에서 설정에 따라 테마를 적용하고 싶다면 이렇게 만들어보는 것은 어떨까요?
https://developer.android.com/jetpack/compose/compositionlocal?hl=ko
CompositionLocal을 사용한 로컬 범위 지정 데이터 | Jetpack Compose | Android Developers
CompositionLocal을 사용한 로컬 범위 지정 데이터 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. CompositionLocal은 암시적으로 컴포지션을 통해 데이터를 전달하
developer.android.com
https://developer.android.com/jetpack/compose/themes/anatomy?hl=ko
Compose 내 테마 분석 | Jetpack Compose | Android Developers
Compose 내 테마 분석 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose의 테마는 여러 개의 하위 수준 구성과 관련 API로 이루어져 있습니다. 이러한
developer.android.com
'Compose' 카테고리의 다른 글
Compose UI Test 맛보기 (0) | 2023.06.14 |
---|---|
Compose와 함께 Motion Layout을 사용하여 애니메이션 구현하기 (0) | 2023.05.24 |
rememberUpdatedState (0) | 2023.02.15 |
SwipeToDismiss Jetpack Compose로 구현하기 (0) | 2023.02.02 |
Jetpack Compose GapBuffer (1) | 2022.10.27 |