AnchoredDraggable
포스팅에 앞서 Anchored draggable이 무엇인지 간단하게 설명해보고자 합니다.
AnchoredDraggableState
AnchoredDraggableState
는 드래그 가능한 요소를 관리하고 제어하는 데 사용합니다.
간단하게 설명하자면, 특정 앵커 포인트 사이에서 드래그를 관리하는 데 사용되는 상태 객체 입니다.
class AnchoredDraggableState<T>(
initialValue: T,
internal val positionalThreshold: (totalDistance: Float) -> Float,
internal val velocityThreshold: () -> Float,
val animationSpec: AnimationSpec<Float>,
internal val confirmValueChange: (newValue: T) -> Boolean = { true }
)
AnchoredDraggalbeState
class
는 다음과 같이 정의되어 있습니다.
가장 먼저 인자에 대해 간단하게 설명하면,
intitialValue
: 초기 위치 입니다. 드래그 요소의 시작 위치를 설정할 수 있습니다.Threshold
: 임계값으로 두 가지의 임계값을 정의할 수 있습니다.positionThreshold
: 드래그 동작 중 컴포넌트가 이동해야 하는 최소 거리를 정의한다. 이게 무슨 소리인지 의문이 들 수 있지만, 쉽게 설명하자면positionThreshold
값을30dp
로 설정하였다면, 해당 값 만큼 컴포넌트를 이동시켜야 드래그가 인식된다는 뜻입니다.velocityThreshold
: 드래그 동작을 끝내는 데 필요한 최소 속도의 정의입니다. 드래그 동작을 끝낼 때, 설정된 값의 임계점을 넘어야 드래그 동작이 완료되는 것으로 간주합니다.
animationSpec
: 드래그 동작이 끝나고 애니메이션 동작을 정의하는 객체입니다.confirmValueChange
: 드래그 동작으로 값이 변경될 때, 변경을 확인하는 함수입니다.
anchoredDraggable
anchoredDraggable
함수는 AnchoredDraggableState
를 사용하여, 드래그 동작 상태를 관리합니다.
@ExperimentalFoundationApi
fun <T> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null
) = draggable(
state = state.draggableState,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
reverseDirection = reverseDirection,
startDragImmediately = state.isAnimationRunning,
onDragStopped = { velocity -> launch { state.settle(velocity) } }
)
각 매개변수에 대해 설명해보겠습니다.
state
: 상태로achoredDraggableState
를 받아 상태를 관리합니다.orientation
: 방향으로 드래그 동작의 방향을 정의합니다.vertical
,horizontal
중 하나를 선택하여 진행 방향을 결정합니다.enabled
: 드래그 기능을 활성화/비활성화를 결정하는 변수로bool
을 통해 지정이 가능합니다.reverseDirection
: 드래그의 경우 기본적으로 수평의 경우에는왼 -> 오
수직의 경우에는위 -> 아래
방향으로 이동을 합니다. 하지만, 반대 방향으로 적용을 하고 싶다면,true
값을 지정하면 반대로 이동이 가능합니다.
Swipe Action 기능 만들기
오늘의 결과물
How to
다음과 같은 형태의 모양을 완성하기 위해
Box
를 사용해서 보라색 배경을 만들어주고, 수정/삭제/즐겨찾기
기능을 각각 배치하여, 스와이프
에 따라 화면에 보여지도록 구현하였습니다.
ActionItem
먼저 수정/삭제/즐겨찾기
각각의 기능을 담당하는, item
를 만들어보도록 하겠습니다.
3가지 기능은 동일한 크기를 가지고, 아이콘/이름/색상
값만 각각 다르게 적용되므로, ActionItem
이라는 함수를 통해 생성하도록 하겠습니다.
@Composable
private fun ActionItem(
modifier: Modifier,
color: Color,
imageVector: ImageVector,
content :String
){
Box(
modifier = modifier.background(color = color.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
modifier = Modifier
.padding(top = 10.dp, bottom = 4.dp)
.padding(horizontal = 20.dp)
.size(30.dp),
imageVector = imageVector,
contentDescription = null,
tint = Color.White
)
Text(
text = content,
color = Color.Black,
style = MaterialTheme.typography.bodySmall
)
}
}
}
DraggableItem
다음은 보라색 배경과, 아이템이 적용된 컴포저블
입니다.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DraggableItem(
state: AnchoredDraggableState<DragAnchors>,
content: @Composable BoxScope.() -> Unit,
startAction: @Composable (BoxScope.() -> Unit) = {},
endAction: @Composable (BoxScope.() -> Unit) = {}
) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(100.dp)
.clip(RectangleShape)
) {
endAction()
startAction()
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart)
.offset {
IntOffset(
x = -state
.requireOffset()
.roundToInt(),
y = 0,
)
}
.anchoredDraggable(state, Orientation.Horizontal, reverseDirection = true),
content = content
)
}
}
높이
는 100dp
에 너비
는 max
인 Box
를 생성해주었습니다.
매개변수로 들어가는 값의 경우에는
state
: 드래그 상태입니다.content
:보라색
영역을 의미하며,스와이프
전 사용자에게 보여지는 컴포저블입니다.startAction
: 가장 좌측에 위치하는action
으로 본 포스팅에서는즐겨찾기
가 정의된 컴포저블입니다.endAction
: 가장 우측에 위치하는action
으로수정/삭제
가 정의된 컴포저블입니다.
다음은 코드 설명입니다.
Box
의 경우에는 코드 상 작성된 순서대로 랜더링 되므로, 나중에 작성된 코드가 가장 상단에 위치하게 됩니다.이러한 점을 생각하여, endAction
과 startAction
을 먼저 배치하고 그 위에 content
를 배치하여 스와이프
이전에는 안보이다가 스와이프
를 통해 content
가 안보이게 되면 화면에 나타나도록 하였습니다.
Modifier
의 achoredDraggable
에 매개변수로 받은 state
를 적용하였습니다.
offset
을 이용하여 box
위치를 동적으로 이동시켜 주었습니다. state
객체의 requireOffset
을 이용하여 계산을 합니다. requireOffset
은 수평 이동거리를 반환하기 때문에, 해당 값을 이용하여 Box
를 이동시켜 줍니다.
적용하기
작성한 두 가지 함수를 이용하여, 기능을 구현하였습니다.
가장 먼저 각 상태를 enum class
로 정의했습니다.
enum class DragAnchors {
Start,
Center,
End,
}
시작 위치는 기본으로 Center
가 적용이 됩니다.
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun swipeAction() {
val density = LocalDensity.current
val defaultActionSize = 80.dp
val actionSizePx = with(density) { defaultActionSize.toPx() }
val endActionSizePx = with(density) { (defaultActionSize * 2).toPx() }
val state = remember {
AnchoredDraggableState(
initialValue = DragAnchors.Center,
anchors = DraggableAnchors {
DragAnchors.Start at -actionSizePx
DragAnchors.Center at 0f
DragAnchors.End at endActionSizePx
},
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { with(density) { 100.dp.toPx() } },
animationSpec = tween(),
)
}
DraggableItem(state = state,
startAction = {
Box(
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterStart),
) {
ActionItem(
modifier = Modifier
.width(defaultActionSize)
.fillMaxHeight()
.offset {
IntOffset(
((-state
.requireOffset() - actionSizePx))
.roundToInt(), 0
)
}
,
color = Color.Yellow,
imageVector = Icons.Outlined.Star,
content = "즐겨찾기"
)
}
},
endAction = {
Row(
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterEnd)
.offset {
IntOffset(
((-state
.requireOffset()) + endActionSizePx)
.roundToInt(), 0
)
}
)
{
ActionItem(
modifier = Modifier
.width(defaultActionSize)
.fillMaxHeight(),
color = Color.Green,
imageVector = Icons.Default.Edit,
content = "수정"
)
ActionItem(
modifier = Modifier
.width(defaultActionSize)
.fillMaxHeight(),
color = Color.Red,
imageVector = Icons.Default.Delete,
content = "삭제"
)
}
}, content = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue.copy(alpha = 0.3f)), contentAlignment = Alignment.Center
) {
Text(text = "Swipe", color = Color.White, style = MaterialTheme.typography.bodyLarge)
}
})
}
AnchoredDrragableState
스와이프의 상태를 관리하기 위해 state
변수를 생성하였습니다.
initialValue
: 엥커의 중앙에서 시작하기 위해 초기 위치를 중앙으로 설정하였습니다.anchors
: 스와이프시 사용할 엥커의 포인트를 지정하였습니다.시작/중앙/끝
을 각각 지정해주었습니다. 예를 들어Start
로 가기 위해서는 사용자가-actionSizePx
만큼 이동해야Start
앵커 포인트에 도달하게 됩니다.positionalThreshold
: 스와이프 동작이 앵커 포인트를 변경하기 위한 최소 값을 지정하였습니다.velocityThreshold
: 스와이프 동작이 멈추기 위해 넘어야 하는 속도입니다.
DraggableItem
처음에 만들어둔 함수의 매개변수에 state
와 컴포저블을 사용하여 구현하였습니다.
IntOffset(
((-state
.requireOffset() - actionSizePx))
.roundToInt(), 0
)
start
와 end
를 구현할 때 각 Box
의 offset
을 조정하도록 하였습니다.
state
의 경우에는 requireOffset
을 포함하는데, 이는 스와이프 이동 거리입니다.
사용자가 오른쪽으로 드래그를 한다면, 양수의 값을 반환하게 됩니다. 하지만, 오른쪽으로 스와이프를 하게 되면, UI는 왼쪽으로 이동이 되어져야 합니다.
간단하게 설명하면, 가장 좌측에 있는 즐겨찾기 버튼을 보고 싶다면, 오른쪽으로 스와이프를 하여, 해당 UI를 좌측으로 이동시켜야 화면에 보이게 됩니다. 결국 requireOffset
의 계산 결과의 반대방향으로 이동해야 사용자에게 보이게 됩니다.
또한 아이템의 크기만큼 추가적으로 화면에 보여져야 하므로 actionSizePx
의 경우에 아이템의 크기를 계산한 값을 이동시켜주어야 합니다.
github
https://github.com/Myeongcheol-shin/slideUnlock
GitHub - Myeongcheol-shin/slideUnlock: slideUnlock
slideUnlock. Contribute to Myeongcheol-shin/slideUnlock development by creating an account on GitHub.
github.com
'안드로이드 > Compose' 카테고리의 다른 글
[Kotlin/Compose] Slide-Unlock(밀어서 잠금해제) 기능 개발하기 (2) (0) | 2023.12.15 |
---|---|
[Kotlin/Compose] Slide-Unlock(밀어서 잠금해제) 기능 개발하기 (1) (0) | 2023.12.15 |
[Android/Kotlin] MotionLayout (0) | 2023.12.08 |
[Compose UI] 텍스트/이미지 반짝임 로딩화면 만들기 (1) | 2023.11.29 |
[Compose UI] Cutstom Pager를 이용하여 swipe transition 구현하기 (0) | 2023.11.28 |