Bỏ qua để đến nội dung

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.

@Composable
fun ClickableBox() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable {
println("Clicked!")
}
)
}
@Composable
fun AccessibleButton() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable(
enabled = true,
onClick = { },
role = Role.Button,
onClickLabel = "Perform action" // For accessibility
)
)
}
@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") }
)
)
}

@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")
}
}
)
}
)
}

@Composable
fun ScrollableContent() {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
repeat(50) { index ->
Text("Item $index", modifier = Modifier.padding(16.dp))
}
}
}
@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
}
}
}
@Composable
fun ScrollObserver() {
val scrollState = rememberScrollState()
LaunchedEffect(scrollState) {
snapshotFlow { scrollState.value }
.collect { scrollPosition ->
println("Scroll position: $scrollPosition")
}
}
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content
}
}

@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
}
)
)
}
@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
}
}
)
}
@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
}
}
)
}

@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))
}
}
}
@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)
}
}

@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()
)
}
@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)
)
}

@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))
}
}
}
}

@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 */ }
)
}
)
}

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")
}
}
)
}

Gesture API
Tap clickable, detectTapGestures
Long Press combinedClickable, detectTapGestures
Scroll verticalScroll, horizontalScroll
Drag draggable, detectDragGestures
Swipe SwipeToDismissBox, custom
Transform transformable, detectTransformGestures
Cần đơn giản?
├── Click → clickable
├── Multiple click types → combinedClickable
└── Scroll → verticalScroll/horizontalScroll
Cần customize?
└── pointerInput + detect*Gestures