แตะและกด

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

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

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

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

คำอธิบาย

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

ตัวชี้ลงแล้วขึ้น

แตะสองครั้ง

เคอร์เซอร์เลื่อนลง ขึ้น ลง ขึ้น

กดค้าง

ตัวชี้ลงและค้างไว้นานขึ้น

สื่อ

เคอร์เซอร์เลื่อนลง

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

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

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

คุณสามารถเพิ่มตัวแก้ไข 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 หรือตรงกลางของปุ่มบังคับทิศทางเพื่อโต้ตอบ
  • ทำให้องค์ประกอบวางเมาส์เหนือได้ เพื่อให้ตอบสนองต่อเมาส์หรือสไตลัสที่วางเหนือองค์ประกอบ

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

combinedClickable ช่วยให้คุณเพิ่มลักษณะการทํางานของการแตะสองครั้งหรือการกดค้างไว้นอกเหนือจากลักษณะการทํางานของการคลิกปกติ คุณสามารถใช้ 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การเรียกใช้

ปิดคอมโพสิเบิลโดยแตะสกรีม

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

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

ในกรณีนี้ พื้นหลังไม่ควรมีตัวบ่งชี้ภาพเกี่ยวกับการโต้ตอบ ไม่ควรตอบสนองต่อการวางเมาส์เหนือ ไม่ควรโฟกัสได้ และการตอบสนองต่อแป้นพิมพ์และเหตุการณ์การช่วยเหลือพิเศษแตกต่างจากการคอมโพสิชันทั่วไป แทนที่จะพยายามปรับลักษณะการทํางานของ 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 อีกครั้งโดยอัตโนมัติเพื่อให้แน่ใจว่ามีการเรียกใช้การเรียกกลับที่ถูกต้องเมื่อผู้ใช้แตะภาพหน้าจอ

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

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

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

ดังที่คุณเห็นในวิดีโอ การซูมเข้าจะเกิดขึ้นบริเวณตำแหน่งของเหตุการณ์การแตะ ผลลัพธ์จะแตกต่างกันเมื่อเราซูมเข้าที่ส่วนซ้ายของรูปภาพเทียบกับส่วนขวา เราสามารถใช้ตัวปรับเปลี่ยน 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
        }
)