Gestures
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
Phần tiêu đề “1. Click và Tap cơ bản”clickable modifier
Phần tiêu đề “clickable modifier”@Composablefun ClickableBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable { println("Clicked!") } )}clickable với parameters
Phần tiêu đề “clickable với parameters”@Composablefun AccessibleButton() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable( enabled = true, onClick = { }, role = Role.Button, onClickLabel = "Perform action" // For accessibility ) )}combinedClickable
Phần tiêu đề “combinedClickable”@Composablefun 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
Phần tiêu đề “2. Pointer Input - Low-level API”detectTapGestures
Phần tiêu đề “detectTapGestures”@Composablefun 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
Phần tiêu đề “3. Scroll”verticalScroll / horizontalScroll
Phần tiêu đề “verticalScroll / horizontalScroll”@Composablefun ScrollableContent() { val scrollState = rememberScrollState()
Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) ) { repeat(50) { index -> Text("Item $index", modifier = Modifier.padding(16.dp)) } }}Programmatic scrolling
Phần tiêu đề “Programmatic scrolling”@Composablefun 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
Phần tiêu đề “Observe scroll position”@Composablefun ScrollObserver() { val scrollState = rememberScrollState()
LaunchedEffect(scrollState) { snapshotFlow { scrollState.value } .collect { scrollPosition -> println("Scroll position: $scrollPosition") } }
Column(modifier = Modifier.verticalScroll(scrollState)) { // Content }}4. Drag
Phần tiêu đề “4. Drag”draggable modifier
Phần tiêu đề “draggable modifier”@Composablefun 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
Phần tiêu đề “detectDragGestures - 2D drag”@Composablefun 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
Phần tiêu đề “Drag với animation”@Composablefun 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
Phần tiêu đề “5. Swipe”Swipe to Dismiss
Phần tiêu đề “Swipe to Dismiss”@OptIn(ExperimentalMaterial3Api::class)@Composablefun 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
Phần tiêu đề “Custom Swipe Detection”@Composablefun 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
Phần tiêu đề “6. Transform Gestures - Zoom, Rotate, Pan”@Composablefun 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)
Phần tiêu đề “detectTransformGestures (low-level)”@Composablefun 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
Phần tiêu đề “7. Nested Scrolling”nestedScroll modifier
Phần tiêu đề “nestedScroll modifier”@OptIn(ExperimentalMaterial3Api::class)@Composablefun 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
Phần tiêu đề “8. Accessibility và Gestures”@Composablefun 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
Phần tiêu đề “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) } }}
@Composablefun TripleTapBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .pointerInput(Unit) { detectTripleTap { offset -> println("Triple tap at $offset") } } )}📝 Tóm tắt
Phần tiêu đề “📝 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?
Phần tiêu đề “Chọn API nào?”Cần đơn giản?├── Click → clickable├── Multiple click types → combinedClickable└── Scroll → verticalScroll/horizontalScroll
Cần customize?└── pointerInput + detect*Gestures