Gestures trong Jetpack Compose
Xử lý touch input và gestures là một phần quan trọng của mobile UI. Compose cung cấp APIs trực quan để detect và phản hồi các gestures.
1. Click và Tap cơ bản
clickable modifier
@Composable
fun ClickableBox() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable {
println("Clicked!")
}
)
}clickable với parameters
@Composable
fun AccessibleButton() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable(
enabled = true,
onClick = { },
role = Role.Button,
onClickLabel = "Perform action" // For accessibility
)
)
}combinedClickable
@Composable
fun MultiClickBox() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.combinedClickable(
onClick = { println("Single click") },
onDoubleClick = { println("Double click") },
onLongClick = { println("Long press") }
)
)
}2. Pointer Input - Low-level API
detectTapGestures
@Composable
fun TapGesturesBox() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectTapGestures(
onTap = { offset ->
println("Tap at $offset")
},
onDoubleTap = { offset ->
println("Double tap at $offset")
},
onLongPress = { offset ->
println("Long press at $offset")
},
onPress = { offset ->
// Called immediately when pressed
val released = tryAwaitRelease()
if (released) {
println("Released")
} else {
println("Cancelled")
}
}
)
}
)
}3. Scroll
verticalScroll / horizontalScroll
@Composable
fun ScrollableContent() {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
repeat(50) { index ->
Text("Item $index", modifier = Modifier.padding(16.dp))
}
}
}Programmatic scrolling
@Composable
fun ControlledScroll() {
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
Column {
Button(onClick = {
scope.launch {
scrollState.animateScrollTo(0) // Scroll to top
}
}) {
Text("Scroll to Top")
}
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content
}
}
}Observe scroll position
@Composable
fun ScrollObserver() {
val scrollState = rememberScrollState()
LaunchedEffect(scrollState) {
snapshotFlow { scrollState.value }
.collect { scrollPosition ->
println("Scroll position: $scrollPosition")
}
}
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content
}
}4. Drag
draggable modifier
@Composable
fun DraggableBox() {
var offsetX by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.size(100.dp)
.background(Color.Blue)
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
)
)
}detectDragGestures - 2D drag
@Composable
fun FreeDraggableBox() {
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offset += dragAmount
}
}
)
}Drag với animation
@Composable
fun SnapBackDraggable() {
var offset by remember { mutableStateOf(Offset.Zero) }
val animatedOffset by animateOffsetAsState(
targetValue = offset,
animationSpec = spring(stiffness = Spring.StiffnessLow),
label = "offset"
)
Box(
modifier = Modifier
.offset { IntOffset(animatedOffset.x.roundToInt(), animatedOffset.y.roundToInt()) }
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
offset = Offset.Zero // Snap back
}
) { change, dragAmount ->
change.consume()
offset += dragAmount
}
}
)
}5. Swipe
Swipe to Dismiss
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeItem(onDismiss: () -> Unit) {
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
onDismiss()
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.CenterEnd
) {
Icon(
Icons.Default.Delete, null,
modifier = Modifier.padding(16.dp),
tint = Color.White
)
}
}
) {
Card {
Text("Swipe to delete", modifier = Modifier.padding(16.dp))
}
}
}Custom Swipe Detection
@Composable
fun CustomSwipe() {
var offsetX by remember { mutableStateOf(0f) }
var swiped by remember { mutableStateOf(false) }
val animatedOffset by animateFloatAsState(
targetValue = if (swiped) 500f else offsetX,
label = "swipe"
)
Box(
modifier = Modifier
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
.fillMaxWidth()
.height(60.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
if (abs(offsetX) > 200) {
swiped = true
} else {
offsetX = 0f
}
}
) { change, dragAmount ->
change.consume()
offsetX += dragAmount
}
}
) {
Text("Swipe me", color = Color.White)
}
}6. Transform Gestures - Zoom, Rotate, Pan
@Composable
fun TransformableImage() {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Image(
painter = painterResource(R.drawable.image),
contentDescription = null,
modifier = Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
.transformable(state = state)
.fillMaxSize()
)
}detectTransformGestures (low-level)
@Composable
fun ManualTransform() {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotationDelta ->
scale *= zoom
rotation += rotationDelta
offset += pan
}
}
.size(200.dp)
.background(Color.Blue)
)
}7. Nested Scrolling
nestedScroll modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CollapsibleToolbar() {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("Title") },
scrollBehavior = scrollBehavior
)
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding)) {
items(100) { index ->
Text("Item $index", modifier = Modifier.padding(16.dp))
}
}
}
}8. Accessibility và Gestures
@Composable
fun AccessibleGestures() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.semantics {
onClick(label = "Perform main action") {
// Handle click for accessibility
true
}
onLongClick(label = "Show menu") {
// Handle long click
true
}
}
.pointerInput(Unit) {
detectTapGestures(
onTap = { /* Regular tap */ },
onLongPress = { /* Regular long press */ }
)
}
)
}9. Custom Gesture Detector
suspend fun PointerInputScope.detectTripleTap(
onTripleTap: (Offset) -> Unit
) {
awaitEachGesture {
val firstDown = awaitFirstDown()
val firstUp = waitForUpOrCancellation()
if (firstUp == null || (firstUp.uptimeMillis - firstDown.uptimeMillis) > 200) return@awaitEachGesture
val secondDown = withTimeoutOrNull(300) { awaitFirstDown() }
if (secondDown == null) return@awaitEachGesture
val secondUp = waitForUpOrCancellation()
if (secondUp == null) return@awaitEachGesture
val thirdDown = withTimeoutOrNull(300) { awaitFirstDown() }
if (thirdDown == null) return@awaitEachGesture
val thirdUp = waitForUpOrCancellation()
if (thirdUp != null) {
onTripleTap(thirdUp.position)
}
}
}
@Composable
fun TripleTapBox() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectTripleTap { offset ->
println("Triple tap at $offset")
}
}
)
}📝 Tóm tắt
| Gesture | API |
|---|---|
| Tap | clickable, detectTapGestures |
| Long Press | combinedClickable, detectTapGestures |
| Scroll | verticalScroll, horizontalScroll |
| Drag | draggable, detectDragGestures |
| Swipe | SwipeToDismissBox, custom |
| Transform | transformable, detectTransformGestures |
Chọn API nào?
Cần đơn giản?
├── Click → clickable
├── Multiple click types → combinedClickable
└── Scroll → verticalScroll/horizontalScroll
Cần customize?
└── pointerInput + detect*GesturesLast updated on