๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/Nutri Capture

Nutri Capture ํ”„๋ก ํŠธ์—”๋“œ - ์ปค์Šคํ…€ BottomSheetScaffold ๊ฐœ๋ฐœ ์œ ์˜ˆ

interfacer_han 2025. 3. 19. 18:23

#1 ๊ฐœ์š”

#1-1 ๊ฐœ๋ฐœ ์ด์œ 

๋ง๋กœ ์„ค๋ช…ํ•˜๊ธฐ ํž˜๋“ค์ง€๋งŒ, BottomSheetScaffold์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ์‚ด์ง๋งŒ ๋ฐ”๊พธ๋ฉด ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋™์ž‘์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๊ณ  internal์ด๋‚˜ private ์ ‘๊ทผ ์ง€์ •์ž๊ฐ€ ๋ถ™์€ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ๋‚ด๊ฐ€ ์ปค์Šคํ…€ํ•  ์ˆ˜๋„ ์—†๋Š” ๋…ธ๋ฆ‡์ด์—ˆ๋‹ค. publicํ•œ ํ•จ์ˆ˜๋“ค๋กœ ๊ตฌํ˜„๋„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๊ฑธ ํ•ด๋ดค๋‹ค. ๊ฒฐ๊ตญ ๊นŠ์ˆ™ํžˆ ํŒŒ๊ณ ๋“ค์–ด๋ณธ ๊ฒฐ๊ณผ, ๋ฐ˜๋“œ์‹œ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ๋ฐ”๊ฟ”์•ผ๋งŒ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋™์ž‘์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑธ ๊นจ๋‹ฌ์•˜๋‹ค.

 

์ •ํ™•ํžˆ๋Š”, NestedScrollConnection์˜ ๊ธฐ์ œ๋ฅผ ์•Œ๊ฒŒ๋˜๊ณ ๋‚˜์„œ ๊นจ๋‹ฌ์•˜๋‹ค. ์ˆœ์ • BottomSheetScaffold์— ๊ฐ€ํ•ด์ง€๋Š” ์‚ฌ์šฉ์ž์˜ ํ„ฐ์น˜ ์ž…๋ ฅ(Pointer input)์€ ์ตœ์ƒ์œ„ ๋ถ€๋ชจ(์ธ ๋‚ด๋ถ€ ์ฝ”๋“œ)๊นŒ์ง€ ์˜ฌ๋ผ๊ฐ€๋Š”๋ฐ, ์ด๋ฅผ ๋ง‰์„ ๋ฐฉ๋ฒ•์ด ์—†๋‹ค๋Š” ๊ฑธ ์•Œ๊ฒŒ๋œ ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋ช‡ ์ฃผ๊ฐ„ ์ˆœ์ • BottomSheetScaffold๋ฅผ ๋ชจ๋ฐฉํ•œ BottomSheetScaffold๋ฅผ ๋งŒ๋“ค๋ ค ํ–ˆ๋‹ค. ์ปค์Šคํ…€ BottomSheetScaffold๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„  ์ˆœ์ • BottomSheetScaffold ๋‚ด๋ถ€ ์ฝ”๋“œ๋“ค์„ ์ „๋ถ€ publicํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ํ•œ ๊ฒƒ์ด๋‹ค.

 

#1-2 ์œ ์˜ˆ ์ด์œ 

๊ทธ๋Ÿฌ๋‚˜, ์ˆœ์ • BottomSheetScaffold์˜ ์ฝ”๋“œ๋Š” ๋‚ด๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ๋ฒ…์ฐผ๋‹ค. ๋จผ์ € ์ผ๋‹จ์€ (์ˆœ์ • BottomSheetScaffold์˜) ์ฝ”๋“œ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ฐ€๋ น NestedScroll์˜ ๊ธฐ์ œ์— ๋Œ€ํ•œ ํŒŒ์•…์€, BottomSheet์˜ ์Šคํฌ๋กค ๋™์ž‘์„ ์ดํ•ดํ•˜๋Š”๋ฐ ํ•„์ˆ˜์ ์ด๋‹ค. ๋˜, NestedScroll์˜ ๊ธฐ์ œ๋ฅผ ์•Œ๋ ค๋ฉด ๋” ๊นŠ์ˆ™ํžˆ ๋“ค์–ด๊ฐ€ Pointer input๊นŒ์ง€ ์•Œ๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋‚œ ๊ณต์‹ ๋ฌธ์„œ์˜ Pointer input ๋ถ€๋ถ„์„ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‚˜๋งŒ์˜ ์–ธ์–ด๋กœ ์ •๋ฆฌํ•˜๋ฉฐ ๊ณต๋ถ€ํ–ˆ๋‹ค.

 

 

[Android] Pointer input - PointerInputChange, PointerEvent

#1 ๊ฐœ์š” ๋™์ž‘ ์ดํ•ดํ•˜๊ธฐ  |  Jetpack Compose  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋™์ž‘ ์ดํ•ดํ•˜๊ธฐ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €

kenel.tistory.com

์Šคํฌ๋กค ๋™์ž‘์„ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋‹ˆ, ํ›จ์”ฌ ๋” BottomSheetScaffold์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์› ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ๊ทธ ์ดํ›„์—๋„ ๋„˜์–ด์•ผ ํ•  ์‚ฐ(๋“œ๋ž˜๊ทธ ๋™์ž‘ ๋ฐ ์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•œ ์ดํ•ด)์€ ๋งŽ์•˜๋‹ค. ํ•˜๋‚˜ํ•˜๋‚˜ ์ ๋ นํ•˜๋ฉด ๊ทธ๋งŒ์ด์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ๋‚ด๊ฐ€ ์ด๋ฏธ ์†Œ๋ชจํ•œ ์‹œ๊ฐ„ ๊ทธ๋ฆฌ๊ณ  ์•ž์œผ๋กœ ์†Œ๋ชจํ•ด์•ผํ•  ์‹œ๊ฐ„์ด ์•„๋ฅธ๊ฑฐ๋ ธ๋‹ค. 

 

#1-3 ์‹คํŒจ๊ฐ€ ์•„๋‹Œ ์œ ์˜ˆ

์ •๋ง ์‹คํŒจ์˜€๋‹ค๋ฉด, ๊ตณ์ด ๊ฒŒ์‹œ๊ธ€๋กœ ๋‚จ๊ธธ ์ด์œ ๊ฐ€ ์—†๋‹ค. ์‹คํŒจ๊ฐ€ ์•„๋‹Œ ์œ ์˜ˆ๋‹ค. ์‚ด์ง๋งŒ ๋ฐ”๊ฟ”์„œ ํ›จ์”ฌ ๋” ์ข‹์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์–ป์–ด๋‚ผ ์ˆ˜ ์žˆ๋Š” ๋™์ž‘์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„์˜ˆ ํฌ๊ธฐํ•˜๊ธฐ๋Š” ๋„ˆ๋ฌด๋‚˜๋„ ์•„์‰ฝ๋‹ค. ๊ทธ๋ž˜์„œ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•œ ์ฝ”๋“œ๋ฅผ ์—ฌ๊ธฐ์— ๋‚จ๊ฒจ๋‘๊ณ  ํ‹ˆ๋‚  ๋•Œ๋งˆ๋‹ค ๊ณต๋ถ€ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค. 

 

๋˜, ๋ณธ ๊ฒŒ์‹œ๊ธ€์€ ์ง€์†์ ์œผ๋กœ ์ˆ˜์ •ํ•  ๊ฒƒ์ด๋‹ค. ๋‹น๋ถ„๊ฐ„์€ ์ฃผ์„์„ ๊ณ„์† ์ถ”๊ฐ€ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ํ•ด์„ํ•˜๊ฑฐ๋‚˜, ๋‚ด๊ฐ€ ๊ณต๋ถ€ํ•ด์•ผํ•  ๊ฐœ๋…๋“ค์„ ์ ์–ด๋‚˜๊ฐˆ๋“ฏ ํ•˜๋‹ค.

 

#2 ์ฝ”๋“œ - ReplicatedBottomSheetScaffold.kt

#2-1 ๊ณต์‹ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜จ ์ฝ”๋“œ

package com.example.replicatedbottomsheetscaffold

import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffoldState
import androidx.compose.material3.DraggableAnchors // error: Cannot access 'DraggableAnchors': it is internal in 'androidx. compose. material3'
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplicatedBottomSheetScaffold(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
    sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
    sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
    sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
    sheetContainerColor: Color = BottomSheetDefaults.ContainerColor,
    sheetContentColor: Color = contentColorFor(sheetContainerColor),
    sheetTonalElevation: Dp = BottomSheetDefaults.Elevation,
    sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
    sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
    sheetSwipeEnabled: Boolean = true,
    topBar: @Composable (() -> Unit)? = null,
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    containerColor: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(containerColor),
    content: @Composable (PaddingValues) -> Unit
) {
    val peekHeightPx = with(LocalDensity.current) {
        sheetPeekHeight.roundToPx()
    }
    ReplicatedBottomSheetScaffoldLayout(
        modifier = modifier,
        topBar = topBar,
        body = content,
        snackbarHost = {
            snackbarHost(scaffoldState.snackbarHostState)
        },
        sheetPeekHeight = sheetPeekHeight,
        sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
        sheetState = scaffoldState.bottomSheetState,
        containerColor = containerColor,
        contentColor = contentColor,
        bottomSheet = { layoutHeight ->
            ReplicatedStandardBottomSheet(
                state = scaffoldState.bottomSheetState,
                peekHeight = sheetPeekHeight,
                sheetMaxWidth = sheetMaxWidth,
                sheetSwipeEnabled = sheetSwipeEnabled,
                calculateAnchors = { sheetSize ->
                    val sheetHeight = sheetSize.height
                    DraggableAnchors {  // error: Cannot access 'DraggableAnchors': it is internal in 'androidx. compose. material3'
                        if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) { // error: Cannot access 'skipPartiallyExpanded': it is internal in 'SheetState'
                            SheetValue.PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat() // error: Cannot access 'DraggableAnchorsConfig': it is internal in 'androidx. compose. material3'
                        }
                        if (sheetHeight != peekHeightPx) {
                            SheetValue.Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat() // error: Cannot access 'DraggableAnchorsConfig': it is internal in 'androidx. compose. material3'
                        }
                        if (!scaffoldState.bottomSheetState.skipHiddenState) { // error: Cannot access 'skipHiddenState': it is internal in 'SheetState'
                            SheetValue.Hidden at layoutHeight.toFloat() // error: Cannot access 'DraggableAnchorsConfig': it is internal in 'androidx. compose. material3'
                        }
                    }
                },
                shape = sheetShape,
                containerColor = sheetContainerColor,
                contentColor = sheetContentColor,
                tonalElevation = sheetTonalElevation,
                shadowElevation = sheetShadowElevation,
                dragHandle = sheetDragHandle,
                content = sheetContent
            )
        }
    )
}

๋‚ด์šฉ ์ถ”๊ฐ€ ์˜ˆ์ •.

 

#3 ReplicatedBottomSheetScaffoldLayout.kt

#3-1 ๊ณต์‹ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜จ ์ฝ”๋“œ

package com.example.replicatedbottomsheetscaffold

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import kotlin.math.roundToInt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplicatedBottomSheetScaffoldLayout(
    modifier: Modifier,
    topBar: @Composable (() -> Unit)?,
    body: @Composable (innerPadding: PaddingValues) -> Unit,
    bottomSheet: @Composable (layoutHeight: Int) -> Unit,
    snackbarHost: @Composable () -> Unit,
    sheetPeekHeight: Dp,
    sheetOffset: () -> Float,
    sheetState: SheetState,
    containerColor: Color,
    contentColor: Color,
) {
    // b/291735717 Remove this once deprecated methods without density are removed
    val density = LocalDensity.current
    SideEffect {
        sheetState.density = density // error: Cannot access 'density': it is internal in 'SheetState'
    }
    SubcomposeLayout { constraints ->
        val layoutWidth = constraints.maxWidth
        val layoutHeight = constraints.maxHeight
        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) {
            bottomSheet(layoutHeight)
        }[0].measure(looseConstraints)

        val topBarPlaceable = topBar?.let {
            subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0]
                .measure(looseConstraints)
        }
        val topBarHeight = topBarPlaceable?.height ?: 0

        val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight)
        val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) {
            Surface(
                modifier = modifier,
                color = containerColor,
                contentColor = contentColor,
            ) { body(PaddingValues(bottom = sheetPeekHeight)) }
        }[0].measure(bodyConstraints)

        val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0]
            .measure(looseConstraints)

        layout(layoutWidth, layoutHeight) {
            val sheetOffsetY = sheetOffset().roundToInt()
            val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2)

            val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2
            val snackbarOffsetY = when (sheetState.currentValue) {
                SheetValue.PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height
                SheetValue.Expanded, SheetValue.Hidden -> layoutHeight - snackbarPlaceable.height
            }

            // Placement order is important for elevation
            bodyPlaceable.placeRelative(0, topBarHeight)
            topBarPlaceable?.placeRelative(0, 0)
            sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY)
            snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY)
        }
    }
}

private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar }

๋‚ด์šฉ ์ถ”๊ฐ€ ์˜ˆ์ •.

 

#4 ReplicatedStandardBottomSheet.kt

#4-1 ๊ณต์‹ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜จ ์ฝ”๋“œ

package com.example.replicatedbottomsheetscaffold

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection // error: Cannot access 'ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection': it is internal in 'androidx. compose. material3'
import androidx.compose.material3.DraggableAnchors // error: Cannot access 'DraggableAnchors': it is internal in 'androidx. compose. material3'
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.SheetValue.Hidden
import androidx.compose.material3.SheetValue.PartiallyExpanded
import androidx.compose.material3.Strings // error: Cannot access 'Strings': it is internal in 'androidx. compose. material3'
import androidx.compose.material3.Surface
import androidx.compose.material3.anchoredDraggable // error: Cannot access 'anchoredDraggable': it is internal in 'androidx. compose. material3'
import androidx.compose.material3.getString // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplicatedStandardBottomSheet(
    state: SheetState,
    calculateAnchors: (sheetSize: IntSize) -> DraggableAnchors<SheetValue>, // error: Cannot access 'DraggableAnchors': it is internal in 'androidx. compose. material3'
    peekHeight: Dp,
    sheetMaxWidth: Dp,
    sheetSwipeEnabled: Boolean,
    shape: Shape,
    containerColor: Color,
    contentColor: Color,
    tonalElevation: Dp,
    shadowElevation: Dp,
    dragHandle: @Composable (() -> Unit)?,
    content: @Composable ColumnScope.() -> Unit
) {
    val scope = rememberCoroutineScope()

    val orientation = Orientation.Vertical

    Surface(
        modifier = Modifier
            .widthIn(max = sheetMaxWidth)
            .fillMaxWidth()
            .requiredHeightIn(min = peekHeight)
            .nestedScroll(
                remember(state.anchoredDraggableState) { // error: Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
                    ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( // error: Cannot access 'ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection': it is internal in 'androidx. compose. material3
                        sheetState = state,
                        orientation = orientation,
                        onFling = { scope.launch { state.settle(it) } } // error: Cannot access 'settle': it is internal in 'SheetState'
                    )
                }
            )
            .anchoredDraggable( // error: Cannot access 'anchoredDraggable': it is internal in 'androidx. compose. material3'
                state = state.anchoredDraggableState,
                orientation = orientation,
                enabled = sheetSwipeEnabled
            )
            .onSizeChanged { layoutSize ->
                val newAnchors = calculateAnchors(layoutSize)
                val newTarget = when (state.anchoredDraggableState.targetValue) { // Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
                    Hidden, PartiallyExpanded -> PartiallyExpanded
                    Expanded -> {
                        if (newAnchors.hasAnchorFor(Expanded)) Expanded else PartiallyExpanded // Cannot access 'DraggableAnchors': it is internal in 'androidx. compose. material3'
                    }
                }
                state.anchoredDraggableState.updateAnchors(newAnchors, newTarget) // Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
            },
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        tonalElevation = tonalElevation,
        shadowElevation = shadowElevation,
    ) {
        Column(Modifier.fillMaxWidth()) {
            if (dragHandle != null) {
                val partialExpandActionLabel =
                    getString(Strings.BottomSheetPartialExpandDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
                val dismissActionLabel = getString(Strings.BottomSheetDismissDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
                val expandActionLabel = getString(Strings.BottomSheetExpandDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
                Box(
                    Modifier
                        .align(CenterHorizontally)
                        .semantics(mergeDescendants = true) {
                            with(state) {
                                // Provides semantics to interact with the bottomsheet if there is more
                                // than one anchor to swipe to and swiping is enabled.
                                if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) { // error: Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
                                    if (currentValue == PartiallyExpanded) {
                                        if (anchoredDraggableState.confirmValueChange(Expanded)) { // error: Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
                                            expand(expandActionLabel) {
                                                scope.launch { expand() }; true
                                            }
                                        }
                                    } else {
                                        if (anchoredDraggableState.confirmValueChange( // error: Cannot access 'anchoredDraggableState': it is internal in 'SheetState'
                                                PartiallyExpanded
                                            )
                                        ) {
                                            collapse(partialExpandActionLabel) {
                                                scope.launch { partialExpand() }; true
                                            }
                                        }
                                    }
                                    if (!state.skipHiddenState) { // error: Cannot access 'skipHiddenState': it is internal in 'SheetState'
                                        dismiss(dismissActionLabel) {
                                            scope.launch { hide() }
                                            true
                                        }
                                    }
                                }
                            }
                        },
                ) {
                    dragHandle()
                }
            }
            content()
        }
    }
}

๋‚ด์šฉ ์ถ”๊ฐ€ ์˜ˆ์ •.

 

#4-2 ์‹œ๋ฉ˜ํ‹ฑ ์ฝ”๋“œ

#4-1์˜ ์ƒ๋‹น ๋ถ€๋ถ„์ด Semantics(์‹œ๋ฉ˜ํ‹ฑ) ์ฝ”๋“œ์— ๋‚œ ์—๋Ÿฌ๋‹ค. ์‹œ๋ฉ˜ํ‹ฑ ์ •๋ณด์— internal ์ ‘๊ทผ์ œ์–ด์ž๊ฐ€ ๋ถ™์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋ฆฌ๋ผ. ์‹œ๋ฉ˜ํ‹ฑ์€ ํ•œ ๋งˆ๋””๋กœ, ์Šค๋งˆํŠธํฐ'์„' ์œ„ํ•œ UI๋‹ค. ์Šค๋งˆํŠธํฐ์ด ํ™”๋ฉด ์† ์š”์†Œ์˜ ์„ค๋ช…๊ณผ ์ƒํƒœ ๋“ฑ์„ ์•Œ๊ฒŒํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๊ฒŒ ๋ฌด์Šจ ์˜๋ฏธ๊ฐ€ ์žˆ์„๊นŒ? ๋ถ„๋ช… ์ผ๋ฐ˜์ ์ธ ์ƒํ™ฉ์—์„œ๋Š” ์˜๋ฏธ๊ฐ€ ์—†๋‹ค. ํ•˜์ง€๋งŒ, ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹œ๊ฐ ์žฅ์• ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋Ÿฐ Sematics๊ฐ€ ๋„์›€์ด ๋œ๋‹ค. ์Œ์„ฑ ๋ช…๋ น ๋“ฑ์œผ๋กœ UI๋ฅผ ์กฐ์ž‘ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž. ์‚ฌ์šฉ์ž๊ฐ€ "์ „ํ™” ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ค˜"๋ผ๊ณ  ๋งํ•œ๋‹ค. ์Šค๋งˆํŠธํฐ์€ "์ „ํ™” ๋ฒ„ํŠผ"์ด ๋ฌด์—‡์ธ์ง€ ์•Œ์•„์•ผ ํ•œ๋‹ค. ์‹œ๋ฉ˜ํ‹ฑ์€ "์ „ํ™” ๋ฒ„ํŠผ"๊ณผ ๊ฐ™์€ ์ •๋ณด๋ฅผ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋‹จ๊ณ„์—์„œ ๋ฏธ๋ฆฌ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์Šค๋งˆํŠธํฐ์ด ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋ง์ด๋‹ค. ๋”ฐ๋ผ์„œ ์‹œ๋ฉ˜ํ‹ฑ์€ ์Šค๋งˆํŠธํฐ'์„' ์œ„ํ•œ UI๋‹ค.

 

val partialExpandActionLabel = "TODO: internal ์‹œ๋ฉ˜ํ‹ฑ ๋ผ๋ฒจ ๋Œ€์‹ , ๋‚ด๊ฐ€ ์ง์ ‘ ์จ๋„ฃ์„ ๊ฒƒ 1" // ์›๋ž˜ ์ฝ”๋“œ: getString(Strings.BottomSheetPartialExpandDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
val dismissActionLabel = "TODO: internal ์‹œ๋ฉ˜ํ‹ฑ ๋ผ๋ฒจ ๋Œ€์‹ , ๋‚ด๊ฐ€ ์ง์ ‘ ์จ๋„ฃ์„ ๊ฒƒ 2" // ์›๋ž˜ ์ฝ”๋“œ: getString(Strings.BottomSheetDismissDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'
val expandActionLabel = "TODO: internal ์‹œ๋ฉ˜ํ‹ฑ ๋ผ๋ฒจ ๋Œ€์‹ , ๋‚ด๊ฐ€ ์ง์ ‘ ์จ๋„ฃ์„ ๊ฒƒ 3" // ์›๋ž˜ ์ฝ”๋“œ: getString(Strings.BottomSheetExpandDescription) // error: Cannot access 'getString': it is internal in 'androidx. compose. material3'

partialExpandActionLabel, dismissActionLabel, expandActionLabel๋Š” internal ์‹œ๋ฉ˜ํ‹ฑ ๋ผ๋ฒจ ์ŠคํŠธ๋ง์ด ํ• ๋‹น๋œ๋‹ค. ๋”ฐ๋ผ์„œ (๋นจ๊ฐ„ ์ค„์„ ์—†์• ๊ธฐ ์œ„ํ•ด์„œ) ์ž„์‹œ๋กœ ์•„๋ฌด ๋ผ๋ฒจ์„ ๋ถ€์—ฌํ–ˆ๋‹ค.

 

#4-3 AnchoredDraggable

๋”ฑ ๋ด๋„, Drag ๋™์ž‘์„ ์•Œ์•„์•ผ ์ดํ•ด ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‚œ Darg ์ฝ”๋“œ์˜ ๋™์ž‘ ๊ธฐ์ œ๋ฅผ ์ž˜ ๋ชจ๋ฅธ๋‹ค. ์ž˜ ๋ชจ๋ฅด๋Š” ์ฝ”๋“œ๋ฅผ ๊ฒ‰ํ•ฅ๊ธฐ์‹์œผ๋กœ ์ดํ•ดํ•˜๋Š” '์ฒ™'ํ•˜๋Š” ๊ฑด ์ฃ„์•…์ด๋‹ค. ๋ฐ˜๋“œ์‹œ ๊ทธ ์—…๋ณด๋ฅผ ์น˜๋ฃจ๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ์•„์˜ˆ Drag ๋™์ž‘์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์„ ๋งŒ๋“ค์—ˆ๋‹ค.

 

 

[Android] Pointer input - Drag

#1 ๊ฐœ์š”#1-1 ๊ณต์‹ ๋ฌธ์„œ Swipeable์—์„œ AnchoredDraggable๋กœ ์ด์ „  |  Jetpack Compose  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Swipeable์—์„œ AnchoredDraggable๋กœ ์ด์ „ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉ

kenel.tistory.com

๋‚ด์šฉ ์ถ”๊ฐ€ ์˜ˆ์ •.

 

#3 ํ›„์ผ๋‹ด

์ถ”๊ฐ€ ์˜ˆ์ •