SwipeContent 코드 리뷰리스트 아이템에서 오른쪽으로 스와이프하면 '삭제'나 '수정' 같은 버튼이 나오는 UI는 많은 앱에서 사용된다. 이번 포스트에서는 Jetpack Compose로 해당 기능을 구현한 SwipeContent.kt 파일을 하나하나 리뷰하며 설명해보려고 한다.
Android Compose에서 draggable과 Animatable을 어떻게 활용할 수 있는지, 그리고 커스터마이징 가능한 Swipe 컴포저블을 어떻게 구성할 수 있는지를 중점적으로 다룬다.
data class SwipeAction(
val content: @Composable (Modifier) -> Unit,
val onClick: () -> Unit,
val width: Dp = 80.dp
)
옵션 버튼 하나를 의미하는 데이터 클래스다. 버튼마다 보여줄 콘텐츠(content)와 클릭 시 동작(onClick), 그리고 버튼의 너비(width)를 지정할 수 있다. width의 기본값은 80dp로 설정되어 있다.
@Composable
fun SwipeContent(
modifier: Modifier = Modifier,
mainContent: @Composable (modifier: Modifier) -> Unit,
onMainClick: () -> Unit,
actions: List<SwipeAction>
)
스와이프 가능한 전체 UI를 구성하는 컴포저블이다. mainContent는 실제 리스트 항목이나 표시할 내용이고, actions는 드래그 시 등장할 버튼 목록이다. 리스트 형태로 받기 때문에 여러 개의 버튼을 순서대로 배치할 수 있다.
val totalOptionWidthDp = actions.sumOf { it.width.value }.dp
val totalOptionWidthPx = with(LocalDensity.current) { totalOptionWidthDp.toPx() }
버튼의 총 너비를 계산한다. Compose에서 위치 조정을 위해선 dp보다 px 단위가 필요하기 때문에 LocalDensity를 통해 변환한다.
val offsetX = remember { Animatable(0f) }
메인 콘텐츠가 얼마나 밀렸는지를 Animatable로 관리한다. Animatable은 부드러운 애니메이션 이동을 지원하고, 현재 위치 값을 보관하는 역할도 한다.
.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에 누적시킨다. 이때 최대한도는 옵션 영역의 너비까지만 허용된다. 드래그가 끝났을 땐 절반 이상 밀렸는지를 판단해서 자동으로 열거나 닫는다.
Box {
Row { actions.forEach { ... } } // 옵션 버튼
Box(modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) }) { mainContent(...) } // 메인 콘텐츠
}
offsetX 값에 따라 이동하며 그 위에 겹쳐진다.SwipeAction을 활용한 구조 덕분에 삭제, 수정, 공유 등 다양한 동작을 넣을 수 있다.