Jetpack Compose - Material Design, Scaffold,

2024. 1. 29. 23:02IT/Android

728x90

<미리 보기>

 

 

<소스 코드>

https://github.com/SeongHyunJeon/android-kotlin-practice/tree/d44b1e1aada5384dd47692aa2c92d3119bb6684a/Woof

 

 

<정리>

Material Design - 앱의 전체적인 테마를 지정할 수 있는 디자인 시스템.

 

 

Theme.kt 파일 - 색상, 서체, 도형 같은 전반적인 테마를 정의한 파일로, 컴포저블 함수의 선언을 통해 각각의 요소를 정의하여 사용할 수 있다. 여기서 사용된 테마 색상 구성은 Material 테마 빌더 사이트를 이용하면 쉽게 구현할 수 있다.

 

테마 색상의 역할

primary - UI 주요 구성 요소.

secondary - UI에서 눈에 덜 띄는 구성 요소.

tertiary - 기본 색상과 보조 색상의 균형을 맞추는 색상으로 입력란 같은 특정 요소.

on - 텍스트, 아이콘, 획 같은 요소.

@Composable
fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(), //기기의 시스템 설정이 다크 모드라면 true, 그렇지 않으면 false를 반환.
    dynamicColor: Boolean = false, //Android 12 이후부터 사용 가능하고, 동적 색상 테마를 지원한다면 true, 그렇지 않으면 false를 설정.
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { //동적 색상 테마를 지원하고 Android 12 이후 버전이라면,
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        } //그리고 다크 모드를 설정 했다면, 최종적으로 동적 색상 다크 모드가 되고 그렇지 않으면 동적 색상 밝기 모드가 된다.

        //동적 색상 테마가 적용이 되지 않는다면, 다크 모드 혹은 밝기 모드.
        darkTheme -> DarkColors
        else -> LightColors
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.primary.toArgb()
            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
        }
    }

    //최종적인 테마 정의.
    MaterialTheme(
        colorScheme = colorScheme, //위에서 정의한 테마.
        typography = Typography, //Type.kt에서 정의한 서체.
        shapes = Shapes, //Shape.ket에서 정의한 도형.
        content = content
    )
}
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WoofTheme { //Theme.kt에서 컴포저블 함수로 정의한 테마를 컴포지션 바깥에 선언하여 적용할 수 있다.
                Surface(
                    modifier = Modifier.fillMaxSize()
                ) {
                    WoofApp()
                }
            }
        }
    }
}

*동적 색상 - 사용자의 선호 즉, 배경화면의 색상을 기반으로 설정되는 색상.

*Theme.kt 파일에는 다크 모드와 밝기 모드를 정의하는 변수가 선언되어 있음. 위의 코드의 DarkColors, LightColors.

 

ChatGPT가 알려준 ColorScheme.

1. primary: 앱의 주요 색상으로, 버튼, 툴바 등 주요 UI 요소에 사용됩니다.

2. onPrimary: primary 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

3. primaryContainer: primary 색상을 배경으로 하는 컨테이너의 색상입니다. 예를 들어, 카드의 배경색으로 사용될 수 있습니다.

4. onPrimaryContainer: primaryContainer 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

5. inversePrimary: primary 색상의 반대색입니다. 보통 다크 모드에서 사용됩니다.

6. secondary: 앱의 보조 색상으로, primary 색상과 대비를 이루며 추가적인 UI 요소에 사용됩니다.

7. onSecondary: secondary 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

8. secondaryContainer: secondary 색상을 배경으로 하는 컨테이너의 색상입니다.

9. onSecondaryContainer: secondaryContainer 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

10. tertiary: 앱의 세 번째 주요 색상으로, 추가적인 강조 요소에 사용됩니다.

11. onTertiary: tertiary 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

12. tertiaryContainer: tertiary 색상을 배경으로 하는 컨테이너의 색상입니다.

13. onTertiaryContainer: tertiaryContainer 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

14. background: 앱의 기본 배경 색상입니다.

15. onBackground: background 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

16. surface: 카드와 같은 표면 요소의 색상입니다.

17. onSurface: surface 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

18. surfaceVariant: 기본 surface 색상의 변형으로, 더 많은 변화를 줄 때 사용됩니다.

19. onSurfaceVariant: surfaceVariant 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

20. surfaceTint: 표면 요소의 색조를 조절하는 색상입니다.

21. inverseSurface: 다크 모드에서의 반전된 표면 색상입니다.

22. inverseOnSurface: inverseSurface 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

23. error: 오류 상태를 표시하는 색상입니다.

24. onError: error 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

25. errorContainer: 오류 상태를 표시하는 컨테이너의 색상입니다.

26. onErrorContainer: errorContainer 색상이 적용된 요소 위에 표시되는 텍스트나 아이콘의 색상입니다.

27. outline: 요소의 테두리 색상입니다.

28. outlineVariant: outline 색상의 변형으로, 추가적인 강조를 위해 사용됩니다.

29. scrim: 배경을 흐리게 하는 데 사용되는 색상입니다.

30. surfaceBright: 밝은 표면 색상입니다.

31. surfaceContainer: 컨테이너 표면 색상입니다.

32. surfaceContainerHigh: 높은 강조를 위한 컨테이너 표면 색상입니다.

33. surfaceContainerHighest: 가장 높은 강조를 위한 컨테이너 표면 색상입니다.

34. surfaceContainerLow: 낮은 강조를 위한 컨테이너 표면 색상입니다.

35. surfaceContainerLowest: 가장 낮은 강조를 위한 컨테이너 표면 색상입니다.

36. surfaceDim: 어두운 표면 색상입니다.


Color.kt 파일 - Theme.kt 파일에서 사용되는 색상을 정의한다.

val md_theme_light_primary = Color(0xFF006C4C)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFF89F8C7)
...

*색상 표현인 #FF006C4C는 앞에서 부터 16진수 두 자리씩 투명도, Red, Green, Blue를 의미한다.


dimens.kt 파일 - 텍스트, 레이아웃에 사용되는 dp, sp 같은 크기를 정의하여 사용할 수 있다.

<resources>
   <dimen name="padding_small">8dp</dimen>
   <dimen name="padding_medium">16dp</dimen>
   <dimen name="image_size">64dp</dimen>
</resources>

Row, Column과 같은 컨테이너들을 Card로 감싸 레이아웃 구성 요소 중 일부를 배경 색상과 구분할 수 있다.

Card(modifier = modifier) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}

 

01
Row -> Card + Row


 

Shape.kt 파일 - 도형을 정의하는 데 사용되고, 각각의 컴포저블은 구성 요소 크기 7종류 중 하나에 포함된다.

val Shapes = Shapes(
    small = RoundedCornerShape(50.dp),
    medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp) //Card는 medium에 포함된다.
)

*RoundedCornerShape()는 모서리를 원형으로 만드는 데 사용.

*Image같이 어떤 구성 요소 크기에도 포함되지 않는 컴포저블들은 명시적으로 적어줘서 적용할 수 있다.

ex) MaterialTheme.shapes.small


Type.kt 파일 - 서체 스타일을 정의하는 데 사용되고, 구성 요소 크기에는 총 15가지가 존재한다. 특정 폰트를 다운로드하여 사용할 수 있고, 해당 폰트 파일은 res/font 디렉터리에 저장되어야 한다.

val Nomosans = FontFamily(
    Font(R.font.notosans_regular),
    Font(R.font.notosans_bold, FontWeight.Bold)
)

val Typography = Typography(
    displayLarge = TextStyle(
        fontFamily = Nomosans,
        fontWeight = FontWeight.Normal,
        fontSize = 36.sp
    ),
    labelSmall = TextStyle(
        fontFamily = Nomosans,
        fontWeight = FontWeight.Bold,
        fontSize = 14.sp
    )
)

*폰트 파일의 이름은 소문자와 언더바로 구성되어야 한다.

*해당 속성의 적용은 텍스트 style 속성에 명시적으로 적어주어야 한다. 

ex) MaterialTheme.typography.displayLarge


Scaffold - 일반적인 앱 구조를 생성하는데 유용한 컴포저블로, 상단바, 하단바, 측면 내비게이션, 부착된 아이콘 버튼 등 레이아웃을 편리하게 정의할 수 있다.

@Composable
fun WoofApp() {
    Scaffold(
        topBar = {
            WoofTopAppBar()
        }
    ) { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

*Scaffold 후행 람다 content의 파라미터 입력 값인 it은 원래 Scaffold 파라미터 contentWindowInsets로부터 전달되는 WindowInsets 타입 값인데, 이 값은 내부적으로 자동 처리되어 내부에 선언된 컴포저블의 contentPadding값으로 적용하면 레이아웃의 위치가 겹치지 않고 올바르게 정렬된다.


Icons - 기본 아이콘들은 디폴트로 제공되지만 다양한 형태의 아이콘들을 사용하기 위해선 종속 항목 추가가 요구된다.

IconButton(
    onClick = onClick,
    modifier = modifier
) {
    Icon(
        imageVector = if(expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
        contentDescription = stringResource(R.string.expand_button_content_description),
        tint = MaterialTheme.colorScheme.secondary
    )
}

*아이콘 색상은 tint 속성을 통해 적용할 수 있다.


다른 컴포저블 사이에 Spacer에만 가중치를 적용하여 상위 레이아웃의 빈 공간을 모두 차지할 수 있다.

@Composable
fun DogItem(
    dog: Dog,
    modifier: Modifier = Modifier
) {
    var expanded by remember { mutableStateOf(false) }

    Card(modifier = modifier) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(dimensionResource(R.dimen.padding_small))
        ) {
            DogIcon(dog.imageResourceId)
            DogInformation(dog.name, dog.age)
            Spacer(modifier = Modifier.weight(1f))
            DogItemButton(
                expanded = expanded,
                onClick = { expanded = expanded.not() },
            )
        }
    }
}

 

01
without Spacer -> with Spacer


animateContentSize - 크기를 변경하는 애니메이션을 적용할 때 사용된다.

animateContextSize.animateSpec - 애니메이션 사양을 맞춤 설정하기 위한 파라미터.

spring() - 시작 값과 끝 값 사이에 물리학 기반 애니메이션을 만들어 AnimateSpec타입을 반환한다.

spring.dampingRatio - 탄성 값.

spring.stiffness - 속도.

animateColorAsState - 최종 값(targetValue)만 제공하면 현재 값에서 지정된 값으로 애니메이션을 실행한다.

@Composable
fun DogItem(
    dog: Dog,
    modifier: Modifier = Modifier
) {
    var expanded by remember { mutableStateOf(false) }
    val color by animateColorAsState(
        targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
        else MaterialTheme.colorScheme.primaryContainer,
    )

    Card(modifier = modifier) {
        Column(
            modifier = Modifier
                .animateContentSize(
                    animationSpec = spring(
                        dampingRatio = Spring.DampingRatioNoBouncy,
                        stiffness = Spring.StiffnessMedium
                    )
                )
                .background(color = color)
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(dimensionResource(R.dimen.padding_small))
            ) {
                DogIcon(dog.imageResourceId)
                DogInformation(dog.name, dog.age)
                Spacer(modifier = Modifier.weight(1f))
                DogItemButton(
                    expanded = expanded,
                    onClick = { expanded = expanded.not() },
                )
            }
            if(expanded) {
                DogHobby(
                    dogHobby = dog.hobbies,
                    modifier = Modifier.padding(
                        start = dimensionResource(R.dimen.padding_medium),
                        top = dimensionResource(R.dimen.padding_small),
                        end = dimensionResource(R.dimen.padding_medium),
                        bottom = dimensionResource(R.dimen.padding_medium)
                    )
                )
            }
        }
    }
}

 

728x90