Swipeable content

박채빈·2025년 4월 13일

Jetpack Compose에서 Swipe로 옵션 버튼 노출하기 - SwipeContent 코드 리뷰

리스트 아이템에서 오른쪽으로 스와이프하면 '삭제'나 '수정' 같은 버튼이 나오는 UI는 많은 앱에서 사용된다. 이번 포스트에서는 Jetpack Compose로 해당 기능을 구현한 SwipeContent.kt 파일을 하나하나 리뷰하며 설명해보려고 한다.

Android Compose에서 draggableAnimatable을 어떻게 활용할 수 있는지, 그리고 커스터마이징 가능한 Swipe 컴포저블을 어떻게 구성할 수 있는지를 중점적으로 다룬다.


구성 목표

  • 메인 콘텐츠는 항상 화면 전체 너비를 차지한다.
  • 오른쪽에서 왼쪽으로 드래그하면 옵션 버튼들이 순차적으로 노출된다.
  • 드래그가 절반 이상 진행되었을 경우 완전히 열리고, 그 이하면 닫힌다.
  • 메인 콘텐츠와 옵션 버튼은 각각 클릭 이벤트를 가진다.
  • 버튼 수와 너비는 동적으로 지정할 수 있다.

SwipeAction

data class SwipeAction(
    val content: @Composable (Modifier) -> Unit,
    val onClick: () -> Unit,
    val width: Dp = 80.dp
)

옵션 버튼 하나를 의미하는 데이터 클래스다. 버튼마다 보여줄 콘텐츠(content)와 클릭 시 동작(onClick), 그리고 버튼의 너비(width)를 지정할 수 있다. width의 기본값은 80dp로 설정되어 있다.


SwipeContent

@Composable
fun SwipeContent(
    modifier: Modifier = Modifier,
    mainContent: @Composable (modifier: Modifier) -> Unit,
    onMainClick: () -> Unit,
    actions: List<SwipeAction>
)

스와이프 가능한 전체 UI를 구성하는 컴포저블이다. mainContent는 실제 리스트 항목이나 표시할 내용이고, actions는 드래그 시 등장할 버튼 목록이다. 리스트 형태로 받기 때문에 여러 개의 버튼을 순서대로 배치할 수 있다.


내부 동작 방식

1. 옵션 영역의 너비 계산

val totalOptionWidthDp = actions.sumOf { it.width.value }.dp
val totalOptionWidthPx = with(LocalDensity.current) { totalOptionWidthDp.toPx() }

버튼의 총 너비를 계산한다. Compose에서 위치 조정을 위해선 dp보다 px 단위가 필요하기 때문에 LocalDensity를 통해 변환한다.


2. 드래그 상태 관리

val offsetX = remember { Animatable(0f) }

메인 콘텐츠가 얼마나 밀렸는지를 Animatable로 관리한다. Animatable은 부드러운 애니메이션 이동을 지원하고, 현재 위치 값을 보관하는 역할도 한다.


3. 드래그 처리

.draggable(
    orientation = Orientation.Horizontal,
    state = rememberDraggableState { delta ->
        scope.launch {
            val newOffset = (offsetX.value + delta).coerceIn(-totalOptionWidthPx, 0f)
            offsetX.snapTo(newOffset)
        }
    },
    onDragStopped = {
        scope.launch {
            val settle = if (offsetX.value < -totalOptionWidthPx / 2) -totalOptionWidthPx else 0f
            offsetX.animateTo(settle, tween(250))
        }
    }
)

드래그가 발생하면 delta 값을 받아 현재 offsetX에 누적시킨다. 이때 최대한도는 옵션 영역의 너비까지만 허용된다. 드래그가 끝났을 땐 절반 이상 밀렸는지를 판단해서 자동으로 열거나 닫는다.


4. 콘텐츠 배치 순서

Box {
    Row { actions.forEach { ... } } // 옵션 버튼
    Box(modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) }) { mainContent(...) } // 메인 콘텐츠
}
  • 옵션 버튼은 오른쪽 끝에 정렬되고,
  • 메인 콘텐츠는 offsetX 값에 따라 이동하며 그 위에 겹쳐진다.
  • 스와이프 시 메인 콘텐츠가 이동하면서 옵션이 드러나는 구조다.

커스터마이징 포인트

  • 버튼의 수, 콘텐츠, 너비를 모두 외부에서 제어할 수 있어 다양한 UI에서 활용 가능하다.
  • SwipeAction을 활용한 구조 덕분에 삭제, 수정, 공유 등 다양한 동작을 넣을 수 있다.

개선할 수 있는 부분

  • 스와이프 열림 상태에서 다른 곳을 터치하면 자동으로 닫히도록 구현하면 더 자연스러운 UX를 제공할 수 있다.
  • drag 방향을 설정할 수 있도록 확장하면 오른쪽으로 열리는 구조도 가능하다.
profile
안드로이드 개발자

0개의 댓글