๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/๊ธฐํƒ€

ContentWithSwipeableBottomSheet()

interfacer_han 2025. 8. 6. 15:53

#1 ๊ฐœ์š”

BottomSheetScaffold()์˜ ๋ฌธ์ œ์ ์„ ๊ฐœ์„ ํ•œ, ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ContentWithSwipeableBottomSheet()๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค (๊ฒŒ์‹œ๊ธ€ ๋งจ ์•„๋ž˜์— ์†Œ์Šค์ฝ”๋“œ ์žˆ์Œ).
 

#2 BottomSheetScaffold()

#2-1 ๊ฐœ์š”

 

androidx.compose.material  |  API reference  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

๋ง ๊ทธ๋Œ€๋กœ Bottom์ชฝ์— Swipe ๊ฐ€๋Šฅํ•œ Sheet๋ฅผ ๋ณด์œ ํ•œ Scaffold๋‹ค.
 

#2-2 ํ•œ๊ณ„

(1) content ๋†’์ด์˜ ๋™์  ์กฐ์ ˆ ๋ถˆ๊ฐ€๋Šฅ
sheet์— ์žˆ๋Š” dragHandle์„ swipe ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ, sheet์˜ swipe ๋™์ž‘์œผ๋กœ content์˜ ๋†’์ด์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜๋Š” ์—†๋‹ค.
 
(2) nestedScroll ๊ฐ•์ œ ์ ์šฉ
sheet์— ์ ์šฉ๋œ nestedScroll ๋•Œ๋ฌธ์—, sheet๋ฅผ Expanded ์ƒํƒœ๋กœ ๋‘์–ด์•ผ ๋น„๋กœ์†Œ sheetContent๋ฅผ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๋‹ค.
 

#2-3 ์—…๊ทธ๋ ˆ์ด๋“œ

#2-2์˜ ํ•œ๊ณ„๋•Œ๋ฌธ์— ๊ฐœ๋ฐœ ์ค‘์ด๋˜ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์—†์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ BottomSheetScaffold()์„ ๊ฐœ์„ ํ•œ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ์ด๋ฆ„์€ ContentWithSwipeableBottomSheet()๋‹ค. content๋ฅผ ๊ฐ์‹ธ๋Š” wrapper๋ฉด์„œ, bottomSheet๋ฅผ ๋ณด์œ ํ–ˆ์Œ์„ ํ‘œํ˜„ํ•œ ์ด๋ฆ„์ด๋‹ค.
 

#3 ContentWithSwipeableBottomSheet()

#3-1 ์™„์„ฑ๋œ ๋ชจ์Šต ๋ฏธ๋ฆฌ๋ณด๊ธฐ

ContentWithSwipeableBottomSheet()๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“  ์ƒ˜ํ”Œ ์•ฑ(#6)์ด๋‹ค. swipe ๋™์ž‘์œผ๋กœ sheetContent ๋ฐ content ์˜์—ญ์˜ ํฌ๊ธฐ๊ฐ€ ๋™์ ์œผ๋กœ ๋ณ€ํ•˜๋Š” ๋ชจ์Šต ๊ทธ๋ฆฌ๊ณ  nestScroll์ด ์—†์–ด sheetContent๋ฅผ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
 

#3-2 API (ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ถ€๋ถ„)

@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ContentWithSwipeableBottomSheet(
    sheetContent: @Composable (() -> Unit),
    modifier: Modifier = Modifier,
    sheetState: ContentWithSwipeableBottomSheetState = rememberContentWithSwipeableBottomSheetState(),
    sheetExpandedAnchorOffset: Dp = 0.dp,
    sheetPartiallyExpandedHeight: Dp = 300.dp,
    sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
    sheetContainerColor: Color = BottomSheetDefaults.ContainerColor,
    sheetContentColor: Color = contentColorFor(sheetContainerColor),
    sheetTonalElevation: Dp = 0.dp,
    sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
    sheetDragHandle: @Composable (() -> Unit) = { BottomSheetDefaults.DragHandle() },
    sheetSwipeEnabled: Boolean = true,
    hasContentNavigationBarPadding: Boolean = false,
    content: @Composable (() -> Unit),
) {
    ...
}

์ธ์ˆ˜ ์ด๋ฆ„๊ณผ ๊ธฐ๋Šฅ์€ BottomSheetScaffold()์˜ ๊ฒƒ๊ณผ ์ตœ๋Œ€ํ•œ ๋˜‘๊ฐ™์ด ๋งž์ท„๋‹ค. ์ด์œ ๋Š” 2๊ฐ€์ง€๋‹ค. ์ฒซ์งธ๋กœ BottomSheetScaffold()์— ๊ธฐ๋ฐ˜ํ•œ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋‘˜์งธ๋กœ ๊ธฐ๋ฐ˜์ด ๋œ BottomSheetScaffold()์€ ๊ตฌ๊ธ€ '๊ณต์‹' ์ปดํฌ๋„ŒํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. '๊ณต์‹'์ด ์ œ๊ณตํ•˜๋Š” ์ด๋ฆ„์€ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž ๋ชจ๋‘์—๊ฒŒ ์นœ์ˆ™ํ•  ๊ฒƒ์ด๋ฏ€๋กœ, ์ด ์นœ์ˆ™ํ•œ ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ์ด์œ ๊ฐ€ ์—†๋‹ค.
 
์ธ์ˆ˜ ์ค‘ hasContentNavigationBarPadding๋Š”, content ํ”„๋กœํผํ‹ฐ์— ์ „๋‹ฌํ•œ UI๊ฐ€ '๋„ค๋น„๊ฒŒ์ด์…˜๋ฐ”๋ฅผ ๊ณ ๋ คํ•œ bottomPadding'์„ ์ง€๋…”๋Š”์ง€์˜ ์—ฌ๋ถ€๋‹ค. ๋œฌ๊ธˆ์—†์ด ์™œ ์ด๋Ÿฐ ์ธ์ˆ˜๋ฅผ ๋„ฃ์—ˆ๋Š”์ง€ ๊ถ๊ธˆํ•  ๊ฒƒ์ด๋‹ค. ์ด๋Š” bottomSheet์˜ ์œ„์น˜ ๋ณ€๋™ ๋•Œ๋ฌธ์ด๋‹ค. #4-3์—์„œ ์ž์„ธํžˆ ์„ค๋ช…ํ•œ๋‹ค.
 

#3-3 ๊ตฌํ˜„ (ํ•จ์ˆ˜ ๋ชธํ†ต ๋ถ€๋ถ„)

@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ContentWithSwipeableBottomSheet(
    ...
) {
    AssertSoftInputModeIsAdjustNothing() // #6-2์—์„œ ์„ค๋ช…

    val density = LocalDensity.current

    BoxWithConstraints(
        modifier = modifier
    ) {
        val layoutHeight = this.maxHeight
        val anchoredDraggableState = sheetState.anchoredDraggableState

        LaunchedEffect(density, layoutHeight, sheetExpandedAnchorOffset, sheetPartiallyExpandedHeight) {
            anchoredDraggableState.updateAnchors(
                DraggableAnchors<SheetValue> {
                    SheetValue.Expanded at with(density) { sheetExpandedAnchorOffset.toPx() }
                    SheetValue.PartiallyExpanded at with(density) { (layoutHeight - sheetPartiallyExpandedHeight).toPx() }
                    SheetValue.Hidden at with(density) { layoutHeight.toPx() }
                }
            )
        }

        ContentWithSwipeableBottomSheetLayout(
            anchoredDraggableState = anchoredDraggableState,
            body = content,
            bottomSheet = {
                BottomSheetLayout(
                    anchoredDraggableState = anchoredDraggableState,
                    sheetSwipeEnabled = sheetSwipeEnabled,
                    shape = sheetShape,
                    containerColor = sheetContainerColor,
                    contentColor = sheetContentColor,
                    tonalElevation = sheetTonalElevation,
                    shadowElevation = sheetShadowElevation,
                    dragHandle = sheetDragHandle,
                    content = sheetContent
                )
            },
            hasBodyNavigationBarPadding = hasContentNavigationBarPadding
        )
    }
}

๋ณธ ์ปดํฌ๋„ŒํŠธ๋Š” swipe ๋™์ž‘์— ์˜ํ–ฅ์„ ๋ฐ›์•„ ๋†’์ด๋ฅผ ์กฐ์ ˆํ•œ๋‹ค. ๊ทธ๋ž˜์„œ AnchoredDraggableState๊ฐ€ ์ค‘์š”ํ•œ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. BoxWithConstraints()๋ฅผ ์“ด ์ด์œ ๋Š” AnchoredDraggableState ๋•Œ๋ฌธ์ด๋‹ค. AnchoredDraggableState.updateAnchors()๋ฅผ ํ†ตํ•ด anchor๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ContentWithSwipeableBottomSheet()๋ฅผ ๋‹ด๋Š” ๋ถ€๋ชจ์˜ ๋†’์ด๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 
์ดํ›„๋กœ๋Š” private fun ContentWithSwipeableBottomSheetLayout()์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค. ContentWithSwipeableBottomSheet()๊ฐ€ ๋ฐ›์€ ๋Œ€๋ถ€๋ถ„์˜ ์ธ์ˆ˜๋ฅผ BottomSheetLayout()์ด ๋ฐ›์•„๋‚ด๊ณ  ์žˆ์Œ์ด ๋ณด์ธ๋‹ค.
 

#4 ContentWithSwipeableBottomSheetLayout()

#4-1 ์ฝ”๋“œ ์ดˆ์•ˆ

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ContentWithSwipeableBottomSheetLayout(
    anchoredDraggableState: AnchoredDraggableState<SheetValue>,
    body: @Composable (() -> Unit),
    bottomSheet: @Composable (() -> Unit),
    // hasBodyNavigationBarPadding: Boolean
) {
    if (!anchoredDraggableState.offset.isNaN()) {
        SubcomposeLayout { constraints ->
            val layoutWidth = constraints.maxWidth
            val layoutHeight = constraints.maxHeight
            val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

            val sheetOffsetY = anchoredDraggableState.offset

            val bodyPlaceables = subcompose("body") { body() }.fastMap {
                val sheetPartiallyExpandedOffset = anchoredDraggableState.anchors.positionOf(SheetValue.PartiallyExpanded)
                val sheetHiddenOffset = anchoredDraggableState.anchors.positionOf(SheetValue.Hidden)
                val maxHeightAffectedBySheetOffsetY = sheetOffsetY.coerceIn(
                    minimumValue = sheetPartiallyExpandedOffset,
                    maximumValue = sheetHiddenOffset
                )

                val imePlaceables = subcompose("ime") { Spacer(modifier = Modifier.imePadding()) }.fastMap { it.measure(looseConstraints) }
                val imeOffsetY = layoutHeight - (imePlaceables.fastMaxBy { it.height }?.height ?: 0)
                val maxHeightAffectedByImeOffsetY = imeOffsetY.coerceAtMost(
                    maximumValue = sheetHiddenOffset.fastRoundToInt()
                )

                it.measure(
                    looseConstraints.copy(
                        maxHeight = minOf(
                            maxHeightAffectedBySheetOffsetY.fastRoundToInt(),
                            maxHeightAffectedByImeOffsetY
                        )
                    )
                )
            }

            val bottomSheetPlaceables = subcompose("bottomSheet") { bottomSheet() }.fastMap {
                it.measure(
                    looseConstraints.copy(
                        maxHeight = (layoutHeight - sheetOffsetY).fastRoundToInt()
                    )
                )
            }

            layout(layoutWidth, layoutHeight) {
                bodyPlaceables.fastForEach { it.place(0, 0) }
                bottomSheetPlaceables.fastForEach { it.place(0, sheetOffsetY.fastRoundToInt()) }
            }
        }
    }
}

hasBodyNavigationBarPadding์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€, ์ฝ”๋“œ ์ดˆ์•ˆ์ด๋‹ค.
 

#4-2 content์˜ maxHeight ๊ฒฐ์ • 1

์•„๋ž˜๋Š” #4-1์— ์žˆ๋Š” ์ฝ”๋“œ์˜ bodyPlaceables ๊ตฌํ˜„ ๋ถ€๋ถ„์ด ์ž˜ ๋ณด์ด๊ฒŒ ๊ฐ•์กฐํ•˜๊ณ , ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์„ค๋ช…ํ•œ ๊ฒƒ์ด๋‹ค.
 

...
@Composable
private fun ContentWithSwipeableBottomSheetLayout(
    anchoredDraggableState: AnchoredDraggableState<SheetValue>,
    body: @Composable (() -> Unit),
    bottomSheet: @Composable (() -> Unit),
    // hasBodyNavigationBarPadding: Boolean
) {
    if (...) {
        SubcomposeLayout { ... ->
            ...

            val bodyPlaceables = subcompose("body") { body() }.fastMap {
                val sheetPartiallyExpandedOffset = anchoredDraggableState.anchors.positionOf(SheetValue.PartiallyExpanded)
                val sheetHiddenOffset = anchoredDraggableState.anchors.positionOf(SheetValue.Hidden)
                val maxHeightAffectedBySheetOffsetY = sheetOffsetY.coerceIn(
                    minimumValue = sheetPartiallyExpandedOffset,
                    maximumValue = sheetHiddenOffset
                )

                val imePlaceables = subcompose("ime") { Spacer(modifier = Modifier.imePadding()) }.fastMap { it.measure(looseConstraints) }
                val imeOffsetY = layoutHeight - (imePlaceables.fastMaxBy { it.height }?.height ?: 0)
                val maxHeightAffectedByImeOffsetY = imeOffsetY.coerceAtMost(
                    maximumValue = sheetHiddenOffset.fastRoundToInt()
                )

                it.measure(
                    looseConstraints.copy(
                        maxHeight = minOf(
                            maxHeightAffectedBySheetOffsetY.fastRoundToInt(),
                            maxHeightAffectedByImeOffsetY
                        )
                    )
                )
            }

            ...
        }
    }
}

Jetpack compose์—์„œ X์ถ• offset์€ →์ชฝ์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ์ปค์ง€๊ณ  Y์ถ• offset์€ ↓์ชฝ์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ์ปค์ง„๋‹ค. ๊ทธ๋ฆฌ๊ณ  sheetOffsetY๋Š” sheet์˜ Y์ถ• ์œ„์น˜ offset์„ ์˜๋ฏธํ•˜๋Š” ๋ณ€์ˆ˜๋‹ค. sheetOffsetY๊ฐ€ ์ปค์งˆ์ˆ˜๋ก (= sheet๊ฐ€ ↓์ชฝ์œผ๋กœ ๊ฐˆ์ˆ˜๋ก) content์˜ ๋†’์ด ๋˜ํ•œ ์ปค์ง€๊ณ , sheetContent์˜ ๋†’์ด๋Š” ์ž‘์•„์ง„๋‹ค. ๋ฐ˜๋Œ€๋กœ sheetOffsetY๊ฐ€ ์ž‘์•„์งˆ์ˆ˜๋ก content์˜ ๋†’์ด๋Š” ์ž‘์•„์ง€๊ณ  sheetContent์˜ ๋†’์ด๋Š” ์ปค์ง„๋‹ค. ์ด ๋•Œ, content์˜ ๋†’์ด์—๋Š” ํ•˜ํ•œ์„ ์„ ์„ค์ •ํ–ˆ๋‹ค. ์ด ํ•˜ํ•œ์„ ์€ sheet๊ฐ€ partiallyExpanded ์ผ๋•Œ์˜ content ๋†’์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ime(์†Œํ”„ํŠธ ํ‚ค๋ณด๋“œ)๋Š” ์‹œ์Šคํ…œ ์ฐจ์›์˜ UI์ด๋ฏ€๋กœ ime๋Š” ํ•˜ํ•œ์„ ์„ ๋ฌด์‹œํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค (ํ•˜ํ•œ์„ ์„ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๋‹ค).
 
๊ฒฐ๋ก ์€, Layout Phase์˜ Measurement ๋‹จ๊ณ„์—์„œ sheet์— ์˜ํ–ฅ(affect)์„ ๋ฐ›๋Š” Constraint.maxHeight (์ฝ”๋“œ์— ์žˆ๋Š” maxHeightAffectedBySheetOffsetY)์™€ ime์˜ ์˜ํ–ฅ์„ ๋ฐ›๋Š” Constraint.maxHeight (์ฝ”๋“œ์— ์žˆ๋Š” maxHeightAffectedByImeOffsetY)๋ฅผ ๋™์‹œ์— ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋‘˜ ์ค‘์—์„œ ๋” ์ž‘์€ ๊ฐ’์„ ์ทจํ•œ๋‹ค.
 

#4-3 hasContentNavigationBarPadding ๊ด€๋ จ

์ด ์ธ์ˆ˜๋Š” ์™œ ์กด์žฌํ• ๊นŒ? ์•„๋ž˜ ์Šคํฌ๋ฆฐ์ƒท๊ณผ ๊ฐ™์€ ์ผ์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 

sheet๊ฐ€ hidden ์ƒํƒœ ์ผ ๋•Œ๋Š” ์ž์—ฐ์Šค๋Ÿฝ์ง€๋งŒ, hidden ์ƒํƒœ๊ฐ€ ์•„๋‹๋•Œ๋Š” ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์šด(์˜๋ฏธ์—†๋Š”) bottomPadding์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ฒŒ ๋œ๋‹ค. ์ด ์˜๋ฏธ์—†๋Š” bottomPadding์„ ๊ฐ€๋ฆฌ๋ ค๋ฉด content Measurement ์‹œ์˜ Constraint.maxHeight๋ฅผ bottomPadding๋งŒํผ ๋Š˜๋ ค์ฃผ๋ฉด ๋  ๊ฒƒ์ด๋‹ค. sheet๋Š” content๋ณด๋‹ค zIndex๊ฐ€ ๋†’๊ฒŒ๋” ์„ค์ •ํ•ด๋‘์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ sheet์™€ content๊ฐ€ ๊ฒน์น˜๋ฉด ๊ฒน์นœ ๋ถ€๋ถ„์„ ๋ณด์ด์ง€ ์•Š๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค. '๋ถ€์ž์—ฐ์Šค๋Ÿฌ์šด ๋ถ€๋ถ„'์„ ๋ณด์ด์ง€ ์•Š๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ง์ด๋‹ค.
 

#4-4 content์˜ maxHeight ๊ฒฐ์ • 2

#4-3๋ฅผ ๊ณ ๋ คํ•ด์„œ #4-2์˜ ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
 

...
@Composable
private fun ContentWithSwipeableBottomSheetLayout(
    anchoredDraggableState: AnchoredDraggableState<SheetValue>,
    body: @Composable (() -> Unit),
    bottomSheet: @Composable (() -> Unit),
    hasBodyNavigationBarPadding: Boolean
) {
    if (...) {
        // ↓ โ˜…
        val density = LocalDensity.current
        val navigationBarHeight = if (hasBodyNavigationBarPadding) {
            with(density) { WindowInsets.navigationBars.getBottom(density) }
        } else {
            null
        }

        SubcomposeLayout { ... ->
            ...

            val bodyPlaceables = subcompose("body") { body() }.fastMap {
                ...
                val maxHeightAffectedBySheetOffsetY = (sheetOffsetY + (navigationBarHeight ?: 0)).coerceIn( // ← โ˜…
                    minimumValue = sheetPartiallyExpandedOffset + (navigationBarHeight ?: 0), // ← โ˜…
                    maximumValue = sheetHiddenOffset
                )

                ...
                val maxHeightAffectedByImeOffsetY = (imeOffsetY + (navigationBarHeight ?: 0)).coerceAtMost( // ← โ˜…
                    maximumValue = sheetHiddenOffset.fastRoundToInt()
                )

                it.measure(
                    ...
                )
            }

            ...
        }
    }
}

๋‹ฌ๋ผ์ง„ ์ ์— โ˜… ์ฃผ์„์„ ๋‹ฌ์•˜๋‹ค.
 

#4-5 ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋œ ์Šคํฌ๋ฆฐ์ƒท

๋ถ€์ž์—ฐ์Šค๋Ÿฌ์šด ์—ฌ๋ฐฑ์ด ์‚ฌ๋ผ์กŒ๋‹ค.
 

#5 ContentWithSwipeableBottomSheetState

enum class BottomOccupier { None, Ime, BottomSheet }

class ContentWithSwipeableBottomSheetState(
    val anchoredDraggableState: AnchoredDraggableState<SheetValue>,
    val isImeAppearingState: State<Boolean>
) {
    suspend fun hide() = anchoredDraggableState.animateTo(
        targetValue = SheetValue.Hidden,
        animationSpec = BottomSheetAnimationSpec
    )

    suspend fun partiallyExpand() = anchoredDraggableState.animateTo(
        targetValue = SheetValue.PartiallyExpanded,
        animationSpec = BottomSheetAnimationSpec
    )

    suspend fun expand() = anchoredDraggableState.animateTo(
        targetValue = SheetValue.Expanded,
        animationSpec = BottomSheetAnimationSpec
    )

    val currentValue: SheetValue
        get() = anchoredDraggableState.currentValue

    val settledValue: SheetValue
        get() = anchoredDraggableState.settledValue

    val targetValue: SheetValue
        get() = anchoredDraggableState.targetValue

    val bottomOccupier: BottomOccupier
        get() = if (isImeAppearingState.value) {
            BottomOccupier.Ime
        } else {
            if (anchoredDraggableState.targetValue == SheetValue.Hidden) {
                BottomOccupier.None
            } else {
                BottomOccupier.BottomSheet
            }
        }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun rememberContentWithSwipeableBottomSheetState(
    anchoredDraggableState: AnchoredDraggableState<SheetValue> = AnchoredDraggableState(SheetValue.Hidden),
): ContentWithSwipeableBottomSheetState {
    val imeAppearingState = remember { mutableStateOf(false) }

    val density = LocalDensity.current
    val imeTargetHeight by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density))
    LaunchedEffect(density) {
        snapshotFlow { 0 < imeTargetHeight }
            .distinctUntilChanged()
            .collectLatest { imeAppearingState.value = it }
    }

    return remember {
        ContentWithSwipeableBottomSheetState(
            anchoredDraggableState = anchoredDraggableState,
            isImeAppearingState = imeAppearingState
        )
    }
}

anchoredDraggableState์˜ Wrapper๋‹ค. Wrapper๋กœ์„œ ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์€, bottomOccupier ํ”„๋กœํผํ‹ฐ๋ฅผ ๋ณด์œ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. ์ด ํ”„๋กœํผํ‹ฐ๋Š” ํ˜„์žฌ ํ™”๋ฉด์— bottomSheet๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€, ์•„๋‹ˆ๋ฉด ime๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€, ๊ทธ๊ฒƒ๋„ ์•„๋‹ˆ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”์ง€๋ฅผ ์•Œ๋ ค์ค€๋‹ค. ime๋Š” ์‹œ์Šคํ…œ UI๋กœ ํ•ญ์ƒ bottomSheet๋ณด๋‹ค ์œ„์— ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ime์™€ bottomSheet๊ฐ€ ๋™์‹œ์— ๋–  ์žˆ๋Š” ๊ฒฝ์šฐ bottomOccupier๋Š” ime๊ฐ€ ๋˜๋„๋ก ๋ถ„๊ธฐ๋ฅผ ์งฐ๋‹ค.
 
ํ•˜์ง€๋งŒ, bottomSheet์™€ ime๋Š” ์„œ๋กœ ๋ฐฐํƒ€์ (exclusive)์ด์–ด์•ผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ข‹๋‹ค. bottomSheet๊ฐ€ ime๊ฐ€ ์•Œ์•„์„œ ๋ฐฐํƒ€์ (๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ํ‘œ์‹œ๋˜๊ฒŒ) ๋งŒ๋“ค์–ด์ฃผ๋Š” ImeSheetExclusivityHandler()๋ฅผ ๋งŒ๋“ค์–ด ์ƒ˜ํ”Œ ์•ฑ(#6)์— ์‚ฌ์šฉํ–ˆ๋‹ค (#6-4 ์ฐธ๊ณ ). 
 

#6 ์ƒ˜ํ”Œ ์•ฑ

#6-1 ๊ฐœ์š”

ContentWithSwipeableBottomSheet()๋ฅผ ์ด์šฉํ•œ ์ƒ˜ํ”Œ ์•ฑ์ด๋‹ค. ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๋Š” #6-5์—์„œ ๋‹ค์šด๋กœ๋“œ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด์„œ ์ฝ๋Š” ๊ฑธ ์ถ”์ฒœํ•œ๋‹ค.
 

#6-2 ์ฝ”๋“œ - MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
        setContent {
            ContentWithSwipeableBottomSheetTheme {
                SampleScreen()
            }
        }
    }
}

window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)๋ฅผ ์“ฐ์ง€ ์•Š์œผ๋ฉด,  ContentWithSwipeableBottomSheet() ์‚ฌ์šฉ ์‹œ ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค. ContentWithSwipeableBottomSheet() ๋‚ด๋ถ€์— ์“ฐ์ธ ์•„๋ž˜์˜ ํ•จ์ˆ˜ ๋•Œ๋ฌธ์ด๋‹ค.
 

@Composable
private fun AssertSoftInputModeIsAdjustNothing() {
    val context = LocalView.current.context
    val activity = context as Activity
    val window = activity.window
    val softInputAdjustType by remember { mutableIntStateOf(window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) }
    LaunchedEffect(window) {
        check(softInputAdjustType == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) {
            """
                Expected SOFT_INPUT_ADJUST_NOTHING but was $softInputAdjustType
                ํ•ด๊ฒฐ๋ฒ•: Activity์˜ setContent {} ํ˜ธ์ถœ ์ „์—, window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
            """.trimIndent()
        }
    }
}

ContentWithSwipeableBottomSheet()๋Š” ime์— ์˜ํ•ด ๋ฐ€๋ ค๋‚˜์„œ๋Š” ์•ˆ ๋œ๋‹ค. bottomSheet์™€ ime๊ฐ€ ์„œ๋กœ ๊ฐ™์€ ๊ณต๊ฐ„์„ ์ ์œ ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 

#6-3 ์ฝ”๋“œ - SampleScreen()

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SampleScreen() {
    val scope = rememberCoroutineScope()

    val sheetState = rememberContentWithSwipeableBottomSheetState()
    ImeSheetExclusivityHandler(sheetState.anchoredDraggableState)

    var sheetExpandedAnchorOffset by remember { mutableStateOf(0.dp) }

    ContentWithSwipeableBottomSheet(
        sheetContent = {
            LazyColumn(
                modifier = Modifier.fillMaxWidth()
            ) {
                repeat(30) {
                    item {
                        Text("${it + 1}")
                    }
                }

                item {
                    Spacer(Modifier.navigationBarsPadding())
                }
            }

            BottomSheetBackHandler(sheetState.anchoredDraggableState)
        },
        modifier = Modifier
            .fillMaxSize()
            .background(BottomAppBarDefaults.containerColor),
        sheetState = sheetState,
        sheetExpandedAnchorOffset = sheetExpandedAnchorOffset,
        sheetPartiallyExpandedHeight = storedImeMaxHeight(),
        sheetSwipeEnabled = true,
        hasContentNavigationBarPadding = true
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text("์ œ๋ชฉ")
                    },
                    actions = {
                        IconButton(
                            onClick = {
                                scope.launch {
                                    sheetState.expand()
                                }
                            }
                        ) {
                            Icon(
                                imageVector = ImageVector.vectorResource(id = R.drawable.keyboard_double_arrow_up_24dp_1f1f1f_fill0_wght400_grad0_opsz24),
                                contentDescription = "Bottom Sheet Expand"
                            )
                        }

                        IconButton(
                            onClick = {
                                scope.launch {
                                    when (sheetState.currentValue) {
                                        SheetValue.Hidden -> sheetState.partiallyExpand()
                                        SheetValue.PartiallyExpanded -> sheetState.expand()
                                        SheetValue.Expanded -> {}
                                    }
                                }
                            }
                        ) {
                            Icon(
                                imageVector = Icons.Default.KeyboardArrowUp,
                                contentDescription = "Bottom Sheet Up"
                            )
                        }

                        IconButton(
                            onClick = {
                                scope.launch {
                                    when (sheetState.currentValue) {
                                        SheetValue.Hidden -> {}
                                        SheetValue.PartiallyExpanded -> sheetState.hide()
                                        SheetValue.Expanded -> sheetState.partiallyExpand()
                                    }
                                }
                            }
                        ) {
                            Icon(
                                imageVector = Icons.Default.KeyboardArrowDown,
                                contentDescription = "Bottom Sheet Down"
                            )
                        }

                        IconButton(
                            onClick = {
                                scope.launch {
                                    sheetState.hide()
                                }
                            }
                        ) {
                            Icon(
                                imageVector = ImageVector.vectorResource(id = R.drawable.keyboard_double_arrow_down_24dp_1f1f1f_fill0_wght400_grad0_opsz24),
                                contentDescription = "Bottom Sheet Hide"
                            )
                        }
                    }
                )
            },
            bottomBar = {
                BottomAppBar(
                    containerColor = BottomAppBarDefaults.containerColor
                ) {
                    val imeController = LocalSoftwareKeyboardController.current
                    IconButton(
                        onClick = {
                            scope.launch {
                                when (sheetState.bottomOccupier) {
                                    BottomOccupier.None, BottomOccupier.Ime -> sheetState.partiallyExpand()
                                    BottomOccupier.BottomSheet -> imeController?.show()
                                }
                            }
                        }
                    ) {
                        Icon(
                            imageVector = when (sheetState.bottomOccupier) {
                                BottomOccupier.None, BottomOccupier.Ime ->
                                    ImageVector.vectorResource(id = R.drawable.bottom_drawer_24dp_1f1f1f_fill0_wght400_grad0_opsz24)

                                BottomOccupier.BottomSheet ->
                                    ImageVector.vectorResource(id = R.drawable.keyboard_24dp_1f1f1f_fill0_wght400_grad0_opsz24)
                            },
                            contentDescription = ""
                        )
                    }

                    val focusRequester = remember { FocusRequester() }
                    val interactionSource = remember { MutableInteractionSource() }
                    TextFieldAutoFocusOnceWithoutIme(
                        modifier = Modifier
                            .weight(1f)
                            .focusRequester(focusRequester),
                        interactionSource = interactionSource
                    )
                }
            },
        ) { innerPadding ->
            LaunchedEffect(innerPadding) {
                sheetExpandedAnchorOffset = innerPadding.calculateTopPadding()
            }

            Column {
                LazyColumn(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(innerPadding),
                    reverseLayout = true
                ) {
                    repeat(30) {
                        item {
                            Text("${it + 1}")
                        }
                    }
                }
            }
        }
    }
}

ImeSheetExclusivityHandler(), BottomSheetBackHandler(), storedImeMaxHeight(), TextFieldAutoFocusOnceWithoutIme() ๋“ฑ์˜ ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜(์ปดํฌ๋„ŒํŠธ)๋“ค์€ #6-4์—์„œ ํ•˜๋‚˜ํ•˜๋‚˜ ์„ค๋ช…ํ•œ๋‹ค.
 

#6-4 ์‚ฌ์šฉ๋œ Composable๋“ค

(1) ImeSheetExclusivityHandler()

๋”๋ณด๊ธฐ
@Composable
fun ImeSheetExclusivityHandler(
    anchoredDraggableState: AnchoredDraggableState<SheetValue>
) {
    val imeController = LocalSoftwareKeyboardController.current
    val isImeSettledVisible by rememberIsImeSettledVisible()

    // ↓ LaunchedEffect ์š”์•ฝ: ime๊ฐ€ ์ „๋ถ€ ์˜ฌ๋ผ์˜จ ์งํ›„ ํŠธ๋ฆฌ๊ฑฐ, bottomSheet๋ฅผ hidden ์ƒํƒœ๋กœ
    LaunchedEffect(Unit) {
        snapshotFlow { isImeSettledVisible }
            .filter { it } // ime๊ฐ€ ์™„์ „ํžˆ(settled) ์˜ฌ๋ผ์™”๋Š”๋ฐ(visible),
            .filter { anchoredDraggableState.settledValue != SheetValue.Hidden } // bottomSheet๊ฐ€ ์ˆจ๊ฒจ์ง„(hidden) ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด (= ime์™€ bottomSheet๊ฐ€ ๊ฒน์ณ์ ธ ์žˆ๋Š” ์ƒํƒœ๋ผ๋ฉด),
            .collectLatest {
                anchoredDraggableState.snapTo(SheetValue.Hidden)
            }
    }

    // ↓ LaunchedEffect ์š”์•ฝ: bottomSheet๊ฐ€ ์กฐ๊ธˆ์ด๋ผ๋„ ์˜ฌ๋ผ์˜ค๊ธฐ ์ง์ „ ํŠธ๋ฆฌ๊ฑฐ, bottomSheet๋ฅผ ์ฆ‰์‹œ ๋ฐฐ์น˜ ํ›„ ime๋ฅผ hide
    val isBottomSheetTransitioningFromHidden by remember {
        derivedStateOf {
            anchoredDraggableState.settledValue == SheetValue.Hidden
                    && anchoredDraggableState.targetValue != SheetValue.Hidden
        }
    }
    LaunchedEffect(Unit) {
        snapshotFlow { isBottomSheetTransitioningFromHidden }
            .filter { it } // bottomSheet๊ฐ€ ์ˆจ๊ฒจ์ง„(hidden) ์ƒํƒœ์—์„œ ๋ฒ—์–ด๋‚˜๋ ค ํ•˜๋Š”๋ฐ,
            .filter { isImeSettledVisible } // ์ด๋ฏธ ์™„์ „ํžˆ(settled) ์ž๋ฆฌ์žก์€(visible) ime๊ฐ€ ์žˆ๋‹ค๋ฉด,
            .collectLatest {
                when (anchoredDraggableState.targetValue) {
                    SheetValue.PartiallyExpanded -> {
                        // ๋จผ์ € bottomSheet๋ฅผ ์ฆ‰์‹œ ๋ฐฐ์น˜ํ•˜๊ณ ,
                        anchoredDraggableState.snapTo(SheetValue.PartiallyExpanded)
                        // ime๋ฅผ ์ˆจ๊น€ (์ด๋ ‡๊ฒŒ ํ•ด์•ผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์Šค๋ฅด๋ฅต bottomSheet๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š” ํ™”๋ฉด์ด ๋‚˜์˜ด. ime๋Š” ์‹œ์Šคํ…œ window๋ผ ํ•ญ์ƒ bottomSheet๋ณด๋‹ค ์œ„์— ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ)
                        imeController?.hide()
                    }

                    SheetValue.Expanded -> {
                        anchoredDraggableState.snapTo(SheetValue.PartiallyExpanded)
                        imeController?.hide()
                        anchoredDraggableState.animateTo(SheetValue.Expanded)
                    }

                    else -> {}
                }
            }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun rememberIsImeSettledVisible(): State<Boolean> {
    val density = LocalDensity.current
    val source = WindowInsets.imeAnimationSource
    val target = WindowInsets.imeAnimationTarget

    return remember {
        derivedStateOf {
            val sourceHeight = source.getBottom(density)
            val targetHeight = target.getBottom(density)

            sourceHeight > 0 && sourceHeight == targetHeight
        }
    }
}

ime์™€ bottomSheet๋Š” ์„œ๋กœ ๊ฐ™์€ ๊ณต๊ฐ„์„ ์ ์œ ํ•˜์ง€๋งŒ, ๋™์‹œ์— ๋ฐฐํƒ€์ ์ด์–ด์•ผ ํ•œ๋‹ค. ime๊ฐ€ ์˜ฌ๋ผ์˜ฌ ๋•Œ bottomSheet๊ฐ€ ์ด๋ฏธ ์˜ฌ๋ผ์™€์žˆ์œผ๋ฉด bottomSheet๋ฅผ ๋‚ด๋ ค์ฃผ๊ณ , ๋ฐ˜๋Œ€๋กœ bottomSheet๊ฐ€ ์˜ฌ๋ผ์˜ฌ ๋•Œ ime๊ฐ€ ์ด๋ฏธ ์˜ฌ๋ผ์™€์žˆ์œผ๋ฉด ime๋ฅผ ๋‚ด๋ ค์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ๋‹ค.

 
(2) BottomSheetBackHandler()

๋”๋ณด๊ธฐ
@Composable
fun BottomSheetBackHandler(
    anchoredDraggableState: AnchoredDraggableState<SheetValue>
) {
    val scope = rememberCoroutineScope()
    BackHandler(enabled = (anchoredDraggableState.currentValue != SheetValue.Hidden)) {
        scope.launch {
            when (anchoredDraggableState.currentValue) {
                SheetValue.Expanded -> anchoredDraggableState.animateTo(SheetValue.PartiallyExpanded)
                SheetValue.PartiallyExpanded -> anchoredDraggableState.animateTo(SheetValue.Hidden)
                else -> {}
            }
        }
    }
}

bottomSheet์˜ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ๋™์ž‘ ์ •์˜

 
(3) storedImeMaxHeight()

๋”๋ณด๊ธฐ
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun storedImeMaxHeight(): Dp {
    val dimensDataStore = (LocalContext.current.applicationContext as CwsbsApplication).dimensDataStore

    val density = LocalDensity.current
    val imeTargetHeight by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density))

    LaunchedEffect(density) {
        snapshotFlow { imeTargetHeight }
            .filter { 0 < it }
            .distinctUntilChanged()
            .collectLatest {
                with(density) {
                    dimensDataStore.setImeMaxHeight(it.toDp())
                }
            }
    }

    return dimensDataStore.imeMaxHeightStateFlow.collectAsState().value
}

bottomSheet๊ฐ€ partiallyExpanded์ผ ๋•Œ์˜ ๋†’์ด๋ฅผ ime๊ฐ€ ๋๊นŒ์ง€ ์˜ฌ๋ผ์™”์„ ๋•Œ์˜ ๋†’์ด์™€ ๋งž์ถ”๊ธฐ ์œ„ํ•œ ์ปดํฌ๋„ŒํŠธ๋‹ค. DataStore๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

 
(4) TextFieldAutoFocusOnceWithoutIme()

๋”๋ณด๊ธฐ
@Composable
fun TextFieldAutoFocusOnceWithoutIme(
    value: String = "",
    onValueChange: (String) -> Unit = {},
    @SuppressLint("ModifierParameter") modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource,
) {
    val focusRequester = remember { FocusRequester() }
    val imeController = LocalSoftwareKeyboardController.current
    var showKeyboardOnFocus by remember { mutableStateOf(false) }

    OutlinedTextField(
        value = value,
        onValueChange = {
            onValueChange(it)
        },
        modifier = modifier.focusRequester(focusRequester),
        keyboardOptions = KeyboardOptions.Default.copy(
            showKeyboardOnFocus = showKeyboardOnFocus
        ),
        interactionSource = interactionSource
    )

    LaunchedEffect(Unit) {
        focusRequester.requestFocus()
        imeController?.hide() // showKeyboardOnFocus๊ฐ€ false์—ฌ๋„ ์™„๋ฒฝํžˆ ๋™์ž‘ํ•˜์งˆ ์•Š์Œ. ์ด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋  ๋•Œ๊นŒ์ง€๋Š” ์ด ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๋ณด์กฐํ•จ.
        showKeyboardOnFocus = true
    }
}

์ตœ์ดˆ ์‹คํ–‰ ์‹œ TextField์— focus๋ฅผ ์ฃผ๋˜, ime๊ฐ€ ์˜ฌ๋ผ์˜ค์ง€๋Š” ์•Š๊ฒŒํ•˜๋Š” TextField๋‹ค. ์™œ ๊ตณ์ด ์ด๋Ÿฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ผ๋ƒ๋ฉด focus๋ฅผ ์ตœ์ดˆ๋กœ ํ•œ ๋ฒˆ์€ ๊ฑธ์–ด์ฃผ์–ด์•ผ (#6-3์— ์žˆ๋Š” ์ฝ”๋“œ์ธ) imeController?.show()๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

#6-5 ์™„์„ฑ๋œ ์•ฑ

 

android-practice/playground/ContentWithSwipeableBottomSheet at 5f2073d84b19d38c06dda94957b127c9314a4ea4 · Kanmanemone/android-p

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com

 

#7 ๋ฆฌํŒฉํ† ๋ง๋œ ์ƒ˜ํ”Œ ์•ฑ

#7-1 ๋ชจ๋“ˆํ™”

 

[Android] Module ๊ตฌ์กฐ

#1 Module?#1-1 Whatํ•˜๋‚˜์˜ build.gradle์— ์ข…์†๋œ ์ฝ”๋“œ์˜ ๋ฒ”์œ„= ๋นŒ๋“œ์˜ ์ตœ์†Œ ๋‹จ์œ„= ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ตœ์†Œ ๋‹จ์œ„์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์€ Gradle๋กœ ๋นŒ๋“œ๋˜๊ณ , ๊ทธ ๋นŒ๋“œ์˜ ๋ช…์„ธ๋Š” build.gradle์— ์ ํžŒ๋‹ค. ๋ชจ๋“ˆ์ด build.gradle ํŒŒ

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์— ๊ธฐ๋ฐ˜ํ•ด์„œ "app" ๋ชจ๋“ˆ์— ์ถ”๊ฐ€๋กœ "ui" ๋ฐ "datastore" ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•ด ์ฝ”๋“œ๋ฅผ ๋ถ„์‚ฐ์‹œ์ผฐ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ ๋” ๊น”๋”ํ•œ ์ฝ”๋“œ๋กœ ๋ฆฌํŒฉํ† ๋ง ๋˜์—ˆ๋‹ค.
 

#7-2 ๋ณ€๊ฒฝ ์‚ฌํ•ญ

1. "ui" ๋ชจ๋“ˆ ์ƒ์„ฑ ํ›„ ContentWithSwipeableBottomSheet() ๊ด€๋ จ ์ฝ”๋“œ ์˜ฎ๊น€

2. @OptIn(ExperimentalMaterial3Api::class) ์–ด๋…ธํ…Œ์ด์…˜์„ ์ฃผ๋ ์ฃผ๋  ๋‹ฌ๊ธฐ ์‹ซ์–ด์„œ, androidx.compose.material3.SheetValue ๋Œ€์‹  ์ปค์Šคํ…€ SheetValue๋ฅผ ์ •์˜ํ•ด ์‚ฌ์šฉํ•ด์™”์Œ. ๊ทธ๋Ÿฌ๋‚˜, ์ด๋ ‡๊ฒŒ ์ด๋ฆ„์ด ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” (๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ) ํ˜ผ๋ž€์„ ์ค„ ์—ฌ์ง€๊ฐ€ ๋‹ค๋ถ„ํ•จ. ๋”ฐ๋ผ์„œ ๊ทธ๋ƒฅ androidx.compose.material3.SheetValue์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜๊ณ  ์ด์— ๋งž์ถฐ ์ „์ฒด ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•จ

3. BottomSheetBackHandler()์™€ ImeSheetExclusivityHandler()๋ฅผ "ui" ๋ชจ๋“ˆ์˜ "internal" ํŒจํ‚ค์ง€๋กœ ์ด๋™ ๋ฐ internal ์ ‘๊ทผ์ œ์–ด์ž๋ฅผ ๋ถ™์ž„. ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๋น„์Šทํ•œ ํ•จ์ˆ˜์ธ AssertSoftInputModeIsAdjustNothing()๋„ ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ํฌ์žฅํ•ด์„œ internal ์ ‘๊ทผ์ œ์–ด์ž๋ฅผ ๋ถ™์—ฌ๋‘ 

4. "datastore" ๋ชจ๋“ˆ ์ถ”๊ฐ€ ํ›„, ๊ด€๋ จ ์ฝ”๋“œ๋“ค์„ ์˜ฎ๊น€. ๋˜, DimensDataStore์˜ ๋‚œํ•ดํ•œ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ณต์‹ ๊ฐ€์ด๋“œ(https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore)์˜ ์ฝ”๋“œ์— ๊ธฐ๋ฐ˜ํ•ด์„œ ๋‹ค์‹œ ์”€. ํ•ด๋‹น ํด๋ž˜์Šค์˜ ์ด๋ฆ„์„ SystemPreferences๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , class๊ฐ€ ์•„๋‹ˆ๋ผ object๋กœ ๋ณ€๊ฒฝ

 

#7-3 ์™„์„ฑ๋œ ์•ฑ

 

android-practice/playground/ContentWithSwipeableBottomSheet at 3cd7d083176e5be8a8f919c118ca953a119f1bc9 · Kanmanemone/android-p

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com