클릭 제스처
탭이라고도 하는 클릭 제스처는 modifier로 보이는 모든 컴포저블에서 감지 가능하다.
SomeComposable(
modifier = Modifier.clickable { }
)
참고) selectable : clickable과 비슷한 동작이지만 UI 접근성 측면에서 이점이 있다. 내부 구현은 clickable이다.
Modifier
.selectable(
selected = true or false, // state
onClick = { },
)
하지만 탭, 더블 탭, 프레스, 롱 프레스를 구분하려면 PointerInputScope 를 활용해야한다.
제스처는 다음과 같이 진행된다.
- press -> long press
- press -> tab(터치 해제)
- press -> double tap
Box(
Modifier
.pointerInput(Unit) {
detectTapGestures(
onPress = { tapHandler("onPress Detected") },
onDoubleTap = { tapHandler("onDoubleTap Detected") },
onLongPress = { tapHandler("onLongPress Detected") },
onTap = { tapHandler("onTap Detected") }
)
}
)
참고) 만약 더블탭이나 롱프레스의 시간을 바꾸고 싶다면 ViewConfiguration을 커스텀하여 가능하다.
private class CustomViewConfiguration : ViewConfiguration {
override val doubleTapMinTimeMillis: Long
get() = 1000
override val doubleTapTimeoutMillis: Long
get() = 2000
override val longPressTimeoutMillis: Long
get() = 3000
override val touchSlop: Float
get() = 8f
}
CompositionLocalProvider(
LocalViewConfiguration provides CustomViewConfiguration()
) {
// ...
}
드래그 제스처
modifier에 draggable() 을 적용한다.
움직임 시작 위치로부터 오프셋을 상태로 저장한다. rememberDraggableState()
하지만 수평 혹은 수직으로의 드래그 제스처 구현에만 유용하다. 여러 방향은 PointerInputScope의 detectDragGestures 함수를 이용한다.
var xOffset by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.offset { IntOffset(xOffset.roundToInt(), 0) }
.size(100.dp)
.background(Color.Blue)
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { distance ->
xOffset += distance
}
)
)
rememberDraggableState 같은 경우에는 델타값을 넘겨주어, 해당 값(distance)에 *2 를 하는 등의 조작도 가능하다.
@Composable
fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState {
val onDeltaState = rememberUpdatedState(onDelta)
return remember { DraggableState { onDeltaState.value.invoke(it) } }
}
만약 수직/수평 동시 드래그 조작을 원한다면 pointerInput의 detectDragGestures로 구현 가능하다.
detectDragGestures의 마지막 람다의 두번째 객체가 Offset 객체이다. 이전과 비슷하게 움직일 사물의 오프셋을 상태로 두고 이를 변화시킨다.
var xOffset by remember { mutableStateOf(0f) }
var yOffset by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(xOffset.roundToInt(), yOffset.roundToInt()) }
.background(Color.Blue)
.size(100.dp)
.pointerInput(Unit) {
detectDragGestures { _, distance ->
xOffset += distance.x
yOffset += distance.y
}
}
)
detectDragGestures 에는 onDragStart, onDragEnd 등의 콜백이 있어 다양하게 활용 가능할 것 같다.
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
스크롤 제스처
modifier에 scrollable로 orientation을 지정하여 추가 가능하다.
하지만 이는 단방향 지정이 가능하며, 만약 수평/수직을 모두 감지하려면 2개의 modifier를 함께 사용해야한다.
Box(
Modifier
.fillMaxSize()
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { distance ->
// ... do something
offset += distance
distance
}
)
)
Box(
modifier = Modifier
.size(150.dp)
.verticalScroll(rememberScrollState())
.horizontalScroll(rememberScrollState())
)
멀티터치(꼬집기) 제스처
핀치줌 같은 기능을 만들때 유용할 것 같다.
modifier의 transformable 을 활용하여 rememberTransformableState를 주입해 scale, angle, offset을 컨트롤 가능하다.
예시는 이 state를 이용해 graphicsLayer로 조절한다.
var scale by remember { mutableStateOf(1f) }
var angle by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { scaleChange, offsetChange, rotationChange ->
scale *= scaleChange
angle += rotationChange
offset += offsetChange
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Box(
Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = angle,
translationX = offset.x,
translationY = offset.y
)
.transformable(state = state)
.background(Color.Blue)
.size(100.dp)
)
}
49. 스와이프 제스처 감지하기
swipeableAPI는 deprecated 되었고anchoredDraggableAPI를 쓰라고 한다.
스와이프 제스처란, 기기 화면에 접촉한 지점에서의 수직 / 수평 움직임을 의미한다.
예를 들어, "밀어서 잠금 해제" 같은 제스처이다.
modifier에 swipeable()을 호출한다. 이를 위한 필수 파라미터 코드이다.
Column 범위를 스와이프 가능한 컴포저블로 만든 예시이다.
val state = rememberSwipeableState("On")
val anchors = mapOf(0f to "On", 100f to "Off", 200f to "Locked")
Column(
modifier = Modifier.swipeable(
state = state,
anchors = anchors,
thresholds = {_, _ -> FractionalThreshold(0.5f)},
orientation = Orientation.Horizontal
)
)
먼저 state 에는 rememberSwipeableState이 사용되며, offset, currentValue, targetValue 등의 값이 포함된다. 초기값을 설정할 수 있는데, 이는 anchors Map의 value와 같은 타입이어야 한다.
anchors는 Map인데, key는 float 타입의 px 단위이며, value 는 state값이다. 스와이프 제스처를 몇 px만큼 하냐의 기준이 되는 지점이며, 그 지점에 해당하는 state가 매핑된다.
state에는 currentValue, targetValue가 있는데, currentValue에서 thresholds를 넘는다면 targetValue로 state이 변하게 된다. (anchor의 pixel 좌표와 state의 value를 잘 구분해야한다)
thresholds는 각 anchor 사이의 임계치를 설정한다. 두 가지 threshold가 있는데, 하나는 비율로 계산하는 FractionalThreshold이다. 위 코드 예시를 들면, 0px에서 100px이 되려면 둘 사이에서 50%가 넘게 스와이프 해야한다. 만약 스와이프를 49 px 만큼 한다면, 임계치를 못 넘었기에 state는 여전히 "On"이며 anchor value는 다시 0px로 돌아갈 것이다.
또 다른 threshold는 FixedThreshold로 임계치를 고정값으로 설정한다. 아래와 같이 설정할 수 있다.
thresholds = {_, _ -> FixedThreshold(10.dp)},
이는 현재 좌표에서 10.dp 만큼 스와이프 한다면 targetValue로 state이 바뀐다는 것이다.
마지막 orientation은 스와이프의 방향을 설정한다.
특이했던 점은, anchors로 설정한 값 범위의 바깥 양끝으로 20%만큼 추가로 스와이프 할 수 있다.
물론 anchors 범위 밖으로 스와이프는 가능하지만 state 변화 없이 다시 범위 내부로 들어온다.
아마도 스와이프의 자연스러운 인터랙션을 구현하기 위함이 아닌가 싶다.
(20%는 실험을 통해 확인한 값이다. 예를 들어 범위가 0f ~ 500f 면 양끝으로 +- 50px 더 스와이프 가능하다.)
anchoredDraggable 로 swipeable 대체하기
To be continue..
'Android' 카테고리의 다른 글
| [프론트엔드 개발자를 위한 Android] 전역상태 사용하기 (0) | 2025.11.29 |
|---|---|
| [프론트엔드 개발자를 위한 Android] 상태(State) 쓰기 (5) | 2025.09.30 |
| Android XR과 SDK 현황 살펴보기(with Google I/O 25) 2편 (8) | 2025.08.31 |
| Android XR과 SDK 현황 살펴보기(with Google I/O 25) 1편 (4) | 2025.08.31 |