Material 구성요소 및 레이아웃

Jetpack Compose는 디지털 인터페이스를 만들기 위한 포괄적인 디자인 시스템인 Material Design 구현을 제공합니다. Material 구성요소(버튼, 카드, 스위치 등) 및 레이아웃(예: Scaffold)은 구성 가능한 함수로 사용할 수 있습니다.

머티리얼 구성요소는 사용자 인터페이스를 만드는 대화형 구성요소입니다. Compose는 이러한 여러 구성요소를 바로 제공합니다. 사용할 수 있는 항목을 보려면 Compose Material API 참조를 확인하세요.

Material 구성요소는 앱의 MaterialTheme에서 제공하는 값을 사용합니다.

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

테마 설정에 관한 자세한 내용은 Compose의 디자인 시스템 가이드를 참고하세요.

콘텐츠 슬롯

내부 콘텐츠(텍스트 라벨, 아이콘 등)를 지원하는 Material 구성요소는 구성 가능한 콘텐츠를 허용하는 일반 람다인 '슬롯'과 함께 공개 상수(예: 크기, 패딩)를 제공하여 Material 사양과 일치하도록 내부 콘텐츠 배치를 지원하는 경향이 있습니다.

Button을 예로 들 수 있습니다.

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

그림 1. content 슬롯과 기본 패딩(왼쪽)을 사용하는 Button과 맞춤 contentPadding(오른쪽)을 제공하는 content 슬롯을 사용하는 Button

Button에는 일반 content 후행 람다 슬롯이 있고 이 슬롯은 RowScope를 사용하여 행에서 콘텐츠 컴포저블을 배치합니다. 내부 콘텐츠에 패딩을 적용하는 contentPadding 매개변수도 있습니다. ButtonDefaults를 통해 제공된 상수나 맞춤 값을 사용할 수 있습니다.

또 다른 예로는 ExtendedFloatingActionButton이 있습니다.

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

그림 2. icontext 슬롯을 사용하는 ExtendedFloatingActionButton

일반 content 람다가 아닌 ExtendedFloatingActionButton에는 icontext 라벨용 슬롯 2개가 있습니다. 각 슬롯은 구성 가능한 일반 콘텐츠를 지원하지만 구성요소는 이러한 내부 콘텐츠 배치 방식에 관해 독단적입니다. 내부적으로 패딩과 정렬, 크기를 처리합니다.

Scaffold

Compose는 머티리얼 구성요소를 일반 화면 패턴으로 결합하는 편리한 레이아웃을 제공합니다. Scaffold와 같은 컴포저블은 다양한 구성요소와 기타 화면 요소를 위한 슬롯을 제공합니다.

화면 콘텐츠

Scaffold에는 일반 content 후행 람다 슬롯이 있습니다. 람다는 콘텐츠 루트에 적용해야 하는 PaddingValues 인스턴스를 수신(예: Modifier.padding을 통해)하여 상단과 하단 막대가 있으면 이를 오프셋합니다.

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

앱 바

Scaffold상단 앱 바하단 앱 바의 슬롯을 제공합니다. 컴포저블의 배치는 내부적으로 처리됩니다.

topBar 슬롯과 TopAppBar를 사용할 수 있습니다.

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

bottomBar 슬롯과 BottomAppBar를 사용할 수 있습니다.

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

이러한 슬롯은 BottomNavigation과 같은 다른 머티리얼 구성요소에 사용할 수 있습니다. 맞춤 컴포저블을 사용할 수도 있습니다. 예를 들어 Owl 샘플에서 온보딩 화면을 살펴보세요.

플로팅 작업 버튼

Scaffold플로팅 작업 버튼 슬롯을 제공합니다.

floatingActionButton 슬롯과 FloatingActionButton을 사용할 수 있습니다.

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

FAB 컴포저블의 하단 배치는 내부적으로 처리됩니다. floatingActionButtonPosition 매개변수를 사용하여 가로 위치를 조정할 수 있습니다.

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

Scaffold 컴포저블의 bottomBar 슬롯을 사용하고 있다면 isFloatingActionButtonDocked 매개변수를 사용하여 FAB를 하단 앱 바와 겹칠 수 있습니다.

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

그림 3. floatingActionButton 슬롯과 bottomBar 슬롯을 사용하는 Scaffold. isFloatingActionButtonDocked 매개변수는 상단은 false, 하단은 true로 설정됩니다.

BottomAppBar는 모든 Shape를 허용하는 cutoutShape 매개변수를 사용하여 FAB 컷아웃을 지원합니다. 고정된 구성요소에 사용되는 같은 Shape를 제공하는 것이 좋습니다. 예를 들어 FloatingActionButton은 모서리 크기가 50%인 MaterialTheme.shapes.smallshape 매개변수의 기본값으로 사용합니다.

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

그림 4. BottomAppBar와 고정된 FloatingActionButton이 있는 Scaffold. BottomAppBar에는 FloatingActionButton에서 사용하는 Shape와 일치하는 맞춤 cutoutShape가 있습니다.

스낵바

Scaffold스낵바를 표시하는 방법을 제공합니다.

SnackbarHostState 속성이 포함된 ScaffoldState를 통해 제공됩니다. rememberScaffoldState를 사용하여 scaffoldState 매개변수로 Scaffold에 전달해야 하는 ScaffoldState 인스턴스를 만들 수 있습니다. SnackbarHostState를 통해 showSnackbar 함수에 액세스할 수 있습니다. 이 정지 함수는 CoroutineScope가 필요하고(예: rememberCoroutineScope 사용) UI 이벤트에 대한 응답으로 호출되어 Scaffold 내의 Snackbar를 표시할 수 있습니다.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

선택적 작업을 제공하고 Snackbar의 지속 시간을 조정할 수 있습니다. snackbarHostState.showSnackbar 함수는 추가 actionLabelduration 매개변수를 허용하고 SnackbarResult를 반환합니다.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

snackbarHost 매개변수를 사용하여 맞춤 Snackbar를 제공할 수 있습니다. 자세한 내용은 SnackbarHost API reference docs를 참고하세요.

Scaffold모달 탐색 창 슬롯을 제공합니다. 컴포저블의 드래그 가능한 시트와 레이아웃은 내부적으로 처리됩니다.

ColumnScope를 사용하여 열에서 창 콘텐츠 컴포저블을 배치하는 drawerContent 슬롯을 사용할 수 있습니다.

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold는 다양한 추가 창 매개변수를 허용합니다. 예를 들어 drawerGesturesEnabled 매개변수를 사용하여 창이 드래그에 응답하는지 여부를 전환할 수 있습니다.

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

프로그래매틱 방식으로 창을 여닫는 작업은 ScaffoldState를 통해 실행되는데 ScaffoldState에는 scaffoldState 매개변수로 Scaffold에 전달해야 하는 DrawerState 속성이 포함되어 있습니다. DrawerState를 통해 현재 창 상태와 관련된 속성뿐만 아니라 openclose 함수에도 액세스할 수 있습니다. 이러한 정지 함수는 CoroutineScope가 필요하고(예: rememberCoroutineScope 사용) UI 이벤트에 응답하여 호출될 수 있습니다.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Scaffold 없이 모달 탐색 창을 구현하려면 ModalDrawer 컴포저블을 사용하면 됩니다. Scaffold와 비슷한 창 매개변수를 허용합니다.

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

하단 탐색 창을 구현하려면 BottomDrawer 컴포저블을 사용하면 됩니다.

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

하단 시트

표준 하단 시트를 구현하려면 BottomSheetScaffold 컴포저블을 사용하면 됩니다. Scaffold와 비슷한 매개변수(예: topBar, floatingActionButton, snackbarHost)를 허용합니다. 하단 시트를 표시하는 방법을 제공하는 추가 매개변수가 포함되어 있습니다.

ColumnScope를 사용하여 열에서 시트 콘텐츠 컴포저블을 배치하는 sheetContent 슬롯을 사용할 수 있습니다.

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold는 여러 추가 시트 매개변수를 허용합니다. 예를 들어 sheetPeekHeight 매개변수를 사용하여 시트의 미리보기 높이를 설정할 수 있습니다. sheetGesturesEnabled 매개변수를 사용하여 창이 드래그에 응답하는지 여부를 전환할 수도 있습니다.

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

프로그래매틱 방식으로 시트를 확장하고 축소하는 작업은 BottomSheetState 속성이 포함된 BottomSheetScaffoldState를 통해 실행됩니다. rememberBottomSheetScaffoldState를 사용하여 scaffoldState 매개변수로 BottomSheetScaffold에 전달해야 하는 BottomSheetScaffoldState 인스턴스를 만들 수 있습니다. BottomSheetState를 통해 현재 시트 상태와 관련된 속성뿐만 아니라 expandcollapse 함수에도 액세스할 수 있습니다. 이러한 정지 함수는 CoroutineScope가 필요하고(예: rememberCoroutineScope 사용) UI 이벤트에 응답하여 호출될 수 있습니다.

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

모달 하단 시트를 구현하려면 ModalBottomSheetLayout 컴포저블을 사용하면 됩니다.

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

배경화면

배경화면을 구현하려면 BackdropScaffold 컴포저블을 사용하면 됩니다.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold는 여러 추가 배경화면 매개변수를 허용합니다. 예를 들어 peekHeightheaderHeight 매개변수를 사용하여 후면 레이어의 미리보기 높이와 전면 레이어의 최소 비활성 높이를 설정할 수 있습니다. gesturesEnabled 매개변수를 사용하여 배경화면이 드래그에 응답하는지 여부를 전환할 수도 있습니다.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

프로그래매틱 방식으로 배경화면을 표시하고 숨기는 작업은 BackdropScaffoldState를 통해 실행됩니다. rememberBackdropScaffoldState를 사용하여 scaffoldState 매개변수로 BackdropScaffold에 전달해야 하는 BackdropScaffoldState 인스턴스를 만들 수 있습니다. BackdropScaffoldState를 통해 현재 배경화면 상태와 관련된 속성뿐만 아니라 revealconceal 함수에도 액세스할 수 있습니다. 이러한 정지 함수는 CoroutineScope가 필요하고(예: rememberCoroutineScope 사용) UI 이벤트에 응답하여 호출될 수 있습니다.

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)