แตะและกด

Composable จำนวนมากรองรับการแตะหรือการคลิกในตัว และมีแท็ก onClick แลมบ์ดา ตัวอย่างเช่น คุณสามารถสร้าง Surface ที่คลิกได้ซึ่ง มีลักษณะการทำงานของดีไซน์ Material ทั้งหมดที่เหมาะสำหรับการโต้ตอบกับพื้นผิว:

Surface(onClick = { /* handle click */ }) {
    Text("Click me!", Modifier.padding(24.dp))
}

แต่การคลิกไม่ได้เป็นเพียงวิธีเดียวที่ผู้ใช้สามารถโต้ตอบกับ Composable ได้ หน้านี้ เน้นที่ท่าทางสัมผัสที่เกี่ยวข้องกับตัวชี้อันเดียว โดยตำแหน่งของ ตัวชี้ดังกล่าวไม่มีนัยสำคัญในการจัดการเหตุการณ์นั้น ดังต่อไปนี้ ตารางจะแสดงประเภทของท่าทางสัมผัสต่อไปนี้

ท่าทางสัมผัส

คำอธิบาย

แตะ (หรือคลิก)

ตัวชี้เลื่อนลงแล้วขึ้น

แตะสองครั้ง

ตัวชี้เลื่อนลง ขึ้น ลง ขึ้น

กดค้าง

ตัวชี้ไม่ทำงานและถือไว้เป็นเวลานานขึ้น

สื่อ

ตัวชี้ไม่ทำงาน

ตอบสนองต่อการแตะหรือคลิก

clickable คือตัวแก้ไขที่ใช้กันโดยทั่วไปซึ่งทำให้ Composable ตอบสนองต่อ แตะหรือคลิก ตัวแก้ไขนี้ยังเพิ่มคุณลักษณะอื่นๆ เช่น การสนับสนุนสำหรับ โฟกัส เมาส์และสไตลัสลอยอยู่ และสัญญาณบอกสถานะแบบปรับแต่งได้เมื่อ กดแล้ว เครื่องปรับจะตอบสนองต่อ "การคลิก" ในความหมายกว้างที่สุดของคำนั้น ไม่ใช่ โดยใช้เมาส์หรือนิ้วเท่านั้น แต่ยังคลิกเหตุการณ์ผ่านการป้อนข้อมูลด้วยแป้นพิมพ์หรือเมื่อ โดยใช้บริการการช่วยเหลือพิเศษ

ให้นึกถึงตารางกริดรูปภาพซึ่งมีรูปภาพแสดงแบบเต็มหน้าจอเมื่อผู้ใช้ คลิกที่คำนั้น:

คุณเพิ่มตัวแก้ไข clickable ให้กับแต่ละรายการในตารางกริดเพื่อนำฟีเจอร์นี้ไปใช้ได้ พฤติกรรม:

@Composable
private fun ImageGrid(photos: List<Photo>) {
    var activePhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
    LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
        items(photos, { it.id }) { photo ->
            ImageItem(
                photo,
                Modifier.clickable { activePhotoId = photo.id }
            )
        }
    }
    if (activePhotoId != null) {
        FullScreenImage(
            photo = photos.first { it.id == activePhotoId },
            onDismiss = { activePhotoId = null }
        )
    }
}

ตัวแก้ไข clickable ยังเพิ่มการทำงานเพิ่มเติมดังนี้

  • interactionSource และ indication ซึ่งจะวาดระลอกคลื่นโดยค่าเริ่มต้นเมื่อ ผู้ใช้แตะ Composable ดูวิธีปรับแต่งการตั้งค่าเหล่านี้ได้ในการจัดการผู้ใช้ การโต้ตอบ
  • อนุญาตให้บริการการเข้าถึงโต้ตอบกับองค์ประกอบโดยการตั้งค่า อรรถศาสตร์
  • รองรับการโต้ตอบกับแป้นพิมพ์หรือจอยสติ๊กโดยอนุญาตให้โฟกัสและการกด Enter หรือกึ่งกลางของ D-pad เพื่อโต้ตอบ
  • ทำให้องค์ประกอบวางเมาส์เหนือองค์ประกอบได้ เพื่อให้ตอบสนองต่อเมาส์หรือสไตลัสที่ลอยอยู่ เหนือสิ่งนั้น

กดค้างไว้เพื่อแสดงเมนูตามบริบทตามบริบท

combinedClickable ช่วยให้คุณเพิ่มลักษณะการทำงานของการแตะ 2 ครั้งหรือกดค้างได้ใน นอกเหนือจากพฤติกรรมการคลิกปกติ คุณสามารถใช้ combinedClickable เพื่อแสดง เมนูตามบริบทเมื่อผู้ใช้แตะรูปภาพตารางกริดค้างไว้

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

แนวทางปฏิบัติแนะนำคือคุณควรระบุการตอบสนองแบบรู้สึกได้เมื่อผู้ใช้ กดองค์ประกอบค้างไว้ ซึ่งเป็นเหตุผลที่ข้อมูลโค้ดมี การเรียกใช้ performHapticFeedback

ปิด Composable ด้วยการแตะ Scrim

ในตัวอย่างด้านบน clickable และ combinedClickable เพิ่ม กับ Composable โดยจะแสดงสัญญาณบอกสถานะการโต้ตอบ ตอบสนองต่อการวางเมาส์เหนือ รวมถึงการสนับสนุนการโฟกัส แป้นพิมพ์ และการช่วยเหลือพิเศษ แต่ พฤติกรรมพิเศษนี้อาจไม่เป็นที่ต้องการเสมอไป

ลองดูที่หน้าจอรายละเอียดรูปภาพ พื้นหลังควรเป็นแบบกึ่งโปร่งใส และผู้ใช้ควรสามารถแตะพื้นหลังนั้นเพื่อปิดหน้าจอรายละเอียดได้

ในกรณีนี้ พื้นหลังดังกล่าวไม่ควรมีภาพบ่งชี้ การโต้ตอบ ไม่ควรตอบสนองต่อการวางเมาส์ ไม่ควรมุ่งเน้น และ การตอบสนองต่อเหตุการณ์เกี่ยวกับแป้นพิมพ์และการช่วยเหลือพิเศษจะแตกต่างจาก Composable คุณยกเลิกตัวเลือกดังกล่าวแทนการพยายามปรับลักษณะการทำงานของ clickable ได้ ให้อยู่ในระดับการแอบสแตรกต์ต่ำลงและใช้ตัวปรับแต่ง pointerInput โดยตรง ร่วมกับเมธอด detectTapGestures:

@Composable
private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) {
    val strClose = stringResource(R.string.close)
    Box(
        modifier
            // handle pointer input
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            // handle accessibility services
            .semantics(mergeDescendants = true) {
                contentDescription = strClose
                onClick {
                    onClose()
                    true
                }
            }
            // handle physical keyboard input
            .onKeyEvent {
                if (it.key == Key.Escape) {
                    onClose()
                    true
                } else {
                    false
                }
            }
            // draw scrim
            .background(Color.DarkGray.copy(alpha = 0.75f))
    )
}

ในฐานะคีย์ของตัวปรับแต่ง pointerInput คุณจะส่ง lambda ได้ onClose ช่วงเวลานี้ เรียกใช้ lambda อีกครั้งโดยอัตโนมัติ เพื่อให้แน่ใจว่ามีการเรียก Callback ที่ถูกต้อง เมื่อผู้ใช้แตะ Scrim

แตะสองครั้งเพื่อซูม

บางครั้ง clickable และ combinedClickable มีข้อมูลไม่เพียงพอ โต้ตอบกับการโต้ตอบด้วยวิธีที่ถูกต้อง ตัวอย่างเช่น Composable อาจ ต้องการสิทธิ์เข้าถึงตำแหน่งภายในขอบเขตของ Composable ซึ่งการโต้ตอบ ก็เกิดขึ้น

ลองดูที่หน้าจอรายละเอียดของภาพอีกครั้ง แนวทางปฏิบัติที่ดีที่สุดคือ สามารถซูมรูปภาพได้โดยแตะ 2 ครั้ง:

ตามที่คุณเห็นในวิดีโอ การซูมเข้าจะเกิดขึ้นรอบๆ ตำแหน่งการแตะ กิจกรรม ผลที่ได้จะต่างออกไปเมื่อเราซูมเข้าไปทางด้านซ้ายของรูปภาพ เทียบกับส่วนที่เหมาะสม เราสามารถใช้ตัวปรับแต่ง pointerInput ร่วมกันได้ ด้วย detectTapGestures เพื่อรวมตำแหน่งการแตะเข้ากับ การคำนวณ:

var zoomed by remember { mutableStateOf(false) }
var zoomOffset by remember { mutableStateOf(Offset.Zero) }
Image(
    painter = rememberAsyncImagePainter(model = photo.highResUrl),
    contentDescription = null,
    modifier = modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { tapOffset ->
                    zoomOffset = if (zoomed) Offset.Zero else
                        calculateOffset(tapOffset, size)
                    zoomed = !zoomed
                }
            )
        }
        .graphicsLayer {
            scaleX = if (zoomed) 2f else 1f
            scaleY = if (zoomed) 2f else 1f
            translationX = zoomOffset.x
            translationY = zoomOffset.y
        }
)