แตะและกด

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

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

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

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

บางครั้ง 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
        }
)