คอมโพเนนต์อินเทอร์เฟซผู้ใช้จะแสดงความคิดเห็นแก่ผู้ใช้อุปกรณ์ตามลักษณะ ตอบสนองต่อการโต้ตอบของผู้ใช้ คอมโพเนนต์ทั้งหมดมีวิธีตอบสนองต่อ ซึ่งช่วยให้ผู้ใช้ทราบว่าการโต้ตอบของตนกำลังทำอะไร สำหรับ ตัวอย่างเช่น หากผู้ใช้แตะปุ่มบนหน้าจอสัมผัสของอุปกรณ์ ปุ่มดังกล่าว อาจเปลี่ยนไปในทางใดทางหนึ่ง เช่น เพิ่มสีไฮไลต์ การเปลี่ยนแปลงนี้ แจ้งให้ผู้ใช้ทราบว่าได้แตะปุ่มดังกล่าว หากผู้ใช้ไม่ต้องการดำเนินการ พวกเขาจะได้สามารถลากนิ้วออกจากปุ่ม การปล่อย -- มิเช่นนั้นปุ่มจะเปิดใช้งาน
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-always-enabled.gif?authuser=0000&hl=th)
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-reflects-state.gif?authuser=0000&hl=th)
เอกสารประกอบเกี่ยวกับท่าทางสัมผัสในการเขียน ครอบคลุมวิธี เขียนคอมโพเนนต์จะจัดการเหตุการณ์ตัวชี้ระดับต่ำ เช่น การย้ายตัวชี้และ คลิก รวมการเขียนเหตุการณ์ระดับล่างลงใน การโต้ตอบในระดับที่สูงกว่า เช่น ชุดเหตุการณ์ตัวชี้อาจรวมกันเป็น เพื่อกดปุ่มแล้วปล่อย การทำความเข้าใจนามธรรมระดับสูงเหล่านั้นสามารถ ช่วยปรับแต่งการตอบสนองของ UI ต่อผู้ใช้ได้ ตัวอย่างเช่น คุณอาจต้องการ เพื่อปรับแต่งลักษณะที่ลักษณะที่ปรากฏของคอมโพเนนต์จะเปลี่ยนแปลงเมื่อผู้ใช้โต้ตอบกับ หรือบางทีคุณอาจแค่ต้องการเก็บบันทึกการดำเนินการของผู้ใช้เหล่านั้นไว้ ช่วงเวลานี้ เอกสารจะให้ข้อมูลที่จำเป็นในการแก้ไของค์ประกอบ UI มาตรฐาน หรือออกแบบเองก็ได้
การโต้ตอบ
ในหลายกรณี คุณไม่จำเป็นต้องทราบว่าคอมโพเนนต์ Compose ของคุณเป็นอย่างไร
การตีความการโต้ตอบของผู้ใช้ ตัวอย่างเช่น Button
อาศัย
Modifier.clickable
เพื่อดูว่าผู้ใช้คลิกปุ่มนั้นหรือไม่ หากคุณกำลังเพิ่ม
ลงในแอปของคุณ คุณสามารถกำหนดโค้ด onClick
ของปุ่มนั้น และ
Modifier.clickable
จะเรียกใช้โค้ดดังกล่าวตามความเหมาะสม ซึ่งหมายความว่าคุณไม่ต้อง
เพื่อให้ทราบว่าผู้ใช้แตะหน้าจอหรือเลือกปุ่มที่มี
แป้นพิมพ์; Modifier.clickable
ทราบว่าผู้ใช้คลิก และ
ตอบสนองด้วยการเรียกใช้โค้ด onClick
แต่หากต้องการปรับแต่งการตอบสนองของคอมโพเนนต์ UI ต่อพฤติกรรมของผู้ใช้ คุณอาจต้องรู้เพิ่มเติมว่ามีอะไรเกิดขึ้นบ้าง ส่วนนี้ให้ ข้อมูลดังกล่าว
เมื่อผู้ใช้โต้ตอบกับคอมโพเนนต์ UI ระบบจะแสดงลักษณะการทำงานของคอมโพเนนต์
ด้วยการสร้างจำนวน
Interaction
กิจกรรม ตัวอย่างเช่น หากผู้ใช้แตะปุ่ม ปุ่มจะสร้าง
PressInteraction.Press
หากผู้ใช้ยกนิ้วขึ้นภายในปุ่ม ปุ่มจะสร้าง
PressInteraction.Release
เพื่อให้ปุ่มทราบว่าคลิกเสร็จแล้ว ในทางกลับกัน หาก
ผู้ใช้ลากนิ้วออกจากปุ่ม แล้วยกนิ้วขึ้น ปุ่ม
สร้าง
PressInteraction.Cancel
เพื่อแสดงให้เห็นว่าการกดปุ่มถูกยกเลิกนั้นยังไม่เสร็จสมบูรณ์
การโต้ตอบเหล่านี้ไม่มีผลใดๆ ซึ่งก็คือการโต้ตอบระดับต่ำเหล่านี้ เหตุการณ์ต่างๆ ไม่ได้ตั้งใจที่จะตีความความหมายของการกระทำของผู้ใช้ หรือ ตามลำดับ และไม่ได้ตีความว่าการดำเนินการใดของผู้ใช้อาจมีความสำคัญมากกว่า การดำเนินการอื่นๆ
โดยทั่วไปการโต้ตอบเหล่านี้จะเป็นคู่กันโดยมีจุดเริ่มต้นและจุดสิ้นสุด องค์ประกอบที่ 2
การโต้ตอบมีการอ้างอิงถึงรายการแรก เช่น หากผู้ใช้
แตะปุ่มแล้วยกนิ้วขึ้น การแตะจะสร้าง
PressInteraction.Press
และผลงานจะสร้าง
PressInteraction.Release
;
Release
มีพร็อพเพอร์ตี้ press
ที่ระบุชื่อย่อ
PressInteraction.Press
คุณสามารถดูการโต้ตอบของคอมโพเนนต์หนึ่งๆ ได้โดยสังเกต
InteractionSource
InteractionSource
สร้างขึ้นอยู่บน Kotlin
เพื่อให้คุณสามารถรวบรวมการโต้ตอบด้วยวิธีเดียวกันนี้
คุณต้องทำงานร่วมกับขั้นตอนอื่นๆ ด้วย หากต้องการข้อมูลเพิ่มเติมเกี่ยวกับการตัดสินใจออกแบบนี้
ดูบล็อกโพสต์ส่งเสริมการโต้ตอบ
สถานะการโต้ตอบ
คุณอาจต้องขยายฟังก์ชันการทำงานในตัวของคอมโพเนนต์โดย
การติดตามการโต้ตอบด้วยตัวเอง ตัวอย่างเช่น คุณอาจต้องการให้ปุ่ม
เปลี่ยนสีเมื่อกด วิธีที่ง่ายที่สุดในการติดตามการโต้ตอบคือ
สังเกตสถานะการโต้ตอบที่เหมาะสม InteractionSource
มีหมายเลขโทรศัพท์
ที่แสดงสถานะการโต้ตอบต่างๆ เป็นสถานะ ตัวอย่างเช่น หาก
คุณต้องการดูว่าปุ่มใดถูกกดแล้วหรือไม่ คุณสามารถเรียกใช้
InteractionSource.collectIsPressedAsState()
วิธีการ:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
นอกเหนือจาก collectIsPressedAsState()
Compose ยังมี
collectIsFocusedAsState()
, collectIsDraggedAsState()
และ
collectIsHoveredAsState()
จริงๆ แล้ววิธีการเหล่านี้เป็นวิธีการอำนวยความสะดวก
สร้างขึ้นจาก API ระดับล่าง InteractionSource
ในบางกรณี คุณอาจ
ต้องการใช้ฟังก์ชันระดับต่ำลงมาโดยตรง
ตัวอย่างเช่น สมมติว่าคุณต้องการทราบว่ากำลังกดปุ่มอยู่หรือไม่ และ
ด้วยว่ามีการลากหรือไม่ หากคุณใช้ทั้ง collectIsPressedAsState()
และ collectIsDraggedAsState()
Compose จะทำงานที่ซ้ำกันจำนวนมาก และ
ไม่มีอะไรรับประกันว่าคุณจะได้รับการโต้ตอบทั้งหมดตามลำดับที่ถูกต้อง สำหรับ
สถานการณ์เช่นนี้ คุณอาจต้องทำงานโดยตรงกับ
InteractionSource
ดูข้อมูลเพิ่มเติมเกี่ยวกับการติดตามการโต้ตอบ
ตัวคุณเองกับ InteractionSource
โปรดดูทำงานกับ InteractionSource
ส่วนต่อไปนี้จะอธิบายวิธีใช้และส่งการโต้ตอบกับ
InteractionSource
และ MutableInteractionSource
ตามลำดับ
ใช้งานและปล่อย Interaction
InteractionSource
แสดงสตรีมแบบอ่านอย่างเดียวของ Interactions
ไม่ใช่สตรีมแบบอ่านอย่างเดียว
ในการปล่อย Interaction
ไปยัง InteractionSource
เพื่อปล่อย
Interaction
คุณต้องใช้ MutableInteractionSource
ซึ่งขยายตั้งแต่
InteractionSource
ตัวปรับแต่งและคอมโพเนนต์สามารถใช้ ปล่อย หรือใช้และปล่อย Interactions
ได้
ส่วนต่อไปนี้อธิบายวิธีใช้และสร้างการโต้ตอบจากทั้ง
แป้นกดร่วม และคอมโพเนนต์
การใช้ตัวอย่างตัวแก้ไข
สำหรับตัวแก้ไขที่วาดเส้นขอบสำหรับสถานะที่โฟกัส คุณเพียงต้องสังเกตการณ์
Interactions
เพื่อให้คุณยอมรับ InteractionSource
ได้:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
จากลายเซ็นฟังก์ชันนั้นชัดเจนว่าตัวแก้ไขนี้เป็นผู้บริโภค
กินเวลา Interaction
ได้ แต่ปล่อยไม่ได้
ตัวอย่างการสร้างตัวปรับแต่ง
สําหรับตัวแก้ไขที่จัดการเหตุการณ์วางเมาส์เหนือ เช่น Modifier.hoverable
ต้องปล่อย Interactions
และยอมรับ MutableInteractionSource
เป็น
แทน:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
ตัวแก้ไขนี้คือผู้ผลิต ซึ่งสามารถใช้
MutableInteractionSource
เพื่อปล่อย HoverInteractions
เมื่อวางเมาส์เหนือรายการ หรือ
ไม่ถูกโฮเวอร์
สร้างคอมโพเนนต์ที่ใช้และผลิต
คอมโพเนนต์ระดับสูงอย่าง Material Button
ทำหน้าที่เป็นทั้งผู้ผลิตและ
ผู้บริโภค พวกเขาจัดการอินพุตและเหตุการณ์โฟกัส รวมถึงเปลี่ยนลักษณะที่ปรากฏ
เพื่อตอบสนองต่อเหตุการณ์เหล่านี้ เช่น แสดงคลื่นหรือภาพเคลื่อนไหว
ระดับความสูง ด้วยเหตุนี้ บริษัทจึงแสดง MutableInteractionSource
โดยตรงว่าเป็น
เพื่อที่ว่าคุณจะสามารถระบุอินสแตนซ์ที่จำของคุณเอง
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
วิธีนี้ช่วยให้การยก
MutableInteractionSource
จากคอมโพเนนต์และสังเกตการณ์
Interaction
ที่คอมโพเนนต์ผลิต คุณสามารถใช้สิ่งนี้เพื่อควบคุม
ของคอมโพเนนต์นั้น
หรือคอมโพเนนต์อื่นๆ ใน UI ของคุณ
หากคุณกำลังสร้างคอมโพเนนต์ระดับสูงแบบอินเทอร์แอกทีฟของคุณเอง เราขอแนะนำ
ที่คุณแสดง MutableInteractionSource
เป็นพารามิเตอร์ด้วยวิธีนี้ นอกเหนือจาก
ต่อไปนี้จะช่วยให้อ่านง่ายและ
ควบคุมสถานะภาพของคอมโพเนนต์ในลักษณะเดียวกับที่
สถานะ (เช่น สถานะเปิดใช้งาน) สามารถอ่านและควบคุมได้
การเขียนเป็นไปตามแนวทางสถาปัตยกรรมแบบเลเยอร์
ดังนั้น คอมโพเนนต์ Material ระดับสูงจึงสร้างขึ้นที่ด้านบนของอาคารพื้นฐาน
บล็อกที่สร้าง Interaction
ที่ต้องใช้เพื่อควบคุม Ripples และ
เอฟเฟกต์ภาพ ไลบรารีพื้นฐานมีตัวแก้ไขการโต้ตอบระดับสูง
เช่น Modifier.hoverable
, Modifier.focusable
และ
Modifier.draggable
หากต้องการสร้างคอมโพเนนต์ที่ตอบสนองต่อเหตุการณ์แบบวางเหนือเหตุการณ์ คุณเพียงแค่ใช้
Modifier.hoverable
แล้วส่ง MutableInteractionSource
เป็นพารามิเตอร์
เมื่อใดก็ตามที่วางเมาส์เหนือคอมโพเนนต์ คอมโพเนนต์จะปล่อยเมาส์ HoverInteraction
และคุณใช้
เพื่อเปลี่ยนลักษณะการแสดงคอมโพเนนต์
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
หากต้องการให้คอมโพเนนต์นี้โฟกัสได้ด้วย ให้เพิ่ม Modifier.focusable
และ "ข้าม"
MutableInteractionSource
เดียวกันกับพารามิเตอร์ ตอนนี้ทั้ง 2 อย่าง
ปล่อย HoverInteraction.Enter/Exit
และ FocusInteraction.Focus/Unfocus
แล้ว
ผ่านMutableInteractionSource
เดียวกัน และคุณสามารถปรับแต่ง
ของการโต้ตอบทั้ง 2 ประเภทในตำแหน่งเดียวกัน
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable
ที่สูงกว่า
ระดับ Abstraction ระดับ hoverable
และ focusable
เพื่อให้คอมโพเนนต์
คลิกได้ องค์ประกอบนี้สามารถคลิกได้โดยนัย และองค์ประกอบที่สามารถคลิกได้ควร
ยังสามารถโฟกัสได้ คุณใช้ Modifier.clickable
เพื่อสร้างคอมโพเนนต์ที่
จัดการการโต้ตอบเมื่อวางเมาส์ การโฟกัส และกด โดยไม่ต้องรวมการโต้ตอบน้อยลง
API ระดับ หากต้องการให้คอมโพเนนต์เป็นแบบคลิกได้ ให้ทำดังนี้
แทนที่ hoverable
และ focusable
ด้วย clickable
:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
ทำงานกับ InteractionSource
หากต้องการข้อมูลแบบละเอียดเกี่ยวกับการโต้ตอบกับคอมโพเนนต์ คุณจะทำสิ่งต่อไปนี้ได้
ใช้ flow API มาตรฐานสำหรับ InteractionSource
ของคอมโพเนนต์นั้น
ตัวอย่างเช่น สมมติว่าคุณต้องการรักษารายการของการกดแล้วลาก
การโต้ตอบของ InteractionSource
โค้ดนี้ทำงานได้ครึ่งหนึ่งคือ
สื่อใหม่ๆ ที่จะได้รับเมื่อมีคำถามนั้นเข้ามา ได้แก่
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
นอกจากการเพิ่มการโต้ตอบใหม่แล้ว คุณยังต้องนำการโต้ตอบออกด้วย เมื่อสิ้นสุด (ตัวอย่างเช่น เมื่อผู้ใช้ยกนิ้วขึ้น คอมโพเนนต์) ทำได้ง่าย เนื่องจากการโต้ตอบปลายทางมักจะ การอ้างอิงไปยังการโต้ตอบเริ่มต้นที่เกี่ยวข้อง โค้ดนี้จะแสดงวิธีนำออก การโต้ตอบที่สิ้นสุดลงแล้ว ได้แก่
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
ทีนี้หากต้องการทราบว่ามีการกดหรือลากคอมโพเนนต์อยู่หรือไม่
เพียงแค่ตรวจสอบว่า interactions
ยังว่างอยู่หรือไม่
val isPressedOrDragged = interactions.isNotEmpty()
หากต้องการดูการโต้ตอบล่าสุด ให้ดูข้อมูลจาก ในรายการ ตัวอย่างเช่น นี่คือวิธีการใช้งานระลอกคลื่น Compose หาการวางซ้อนสถานะที่เหมาะสมที่จะใช้สำหรับการโต้ตอบล่าสุด:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
เนื่องจาก Interaction
ทั้งหมดมีโครงสร้างเดียวกัน จึงไม่มี
ความแตกต่างของโค้ดเมื่อใช้กับการโต้ตอบของผู้ใช้ประเภทต่างๆ เช่น
รูปแบบโดยรวมจะเหมือนกัน
โปรดทราบว่าตัวอย่างก่อนหน้านี้ในส่วนนี้แสดงถึง Flow
ของ
การโต้ตอบโดยใช้ State
— ทำให้เห็นค่าที่อัปเดตได้โดยง่าย
เพราะการอ่านค่าสถานะจะทำให้ระบบจัดองค์ประกอบใหม่โดยอัตโนมัติ อย่างไรก็ตาม
การจัดองค์ประกอบภาพถูกกำหนดเป็นกลุ่มล่วงหน้า ซึ่งหมายความว่าหากรัฐเปลี่ยนแปลง และ
จะเปลี่ยนกลับไปภายในเฟรมเดียวกัน คอมโพเนนต์ที่สังเกตสถานะจะไม่
ก็จะเห็นการเปลี่ยนแปลง
การตั้งค่านี้สำคัญสำหรับการโต้ตอบ เนื่องจากการโต้ตอบจะเริ่มต้นและสิ้นสุดเป็นประจำ
ภายในเฟรมเดียวกัน ตัวอย่างเช่น หากใช้ตัวอย่างก่อนหน้านี้กับ Button
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
หากการกดเริ่มและสิ้นสุดภายในเฟรมเดียวกัน ข้อความจะไม่แสดงเป็น
"กดแล้ว!" ในกรณีส่วนใหญ่ นี่ไม่ใช่ปัญหา เนื่องจากการแสดงเอฟเฟกต์แบบภาพสำหรับ
การเว้นระยะสั้นๆ เช่นนี้
จะทำให้วิดีโอกะพริบ และจะไม่
ผู้ใช้สังเกตเห็นได้ง่าย ในบางกรณี เช่น การแสดงเอฟเฟกต์ระลอกคลื่นหรือ
ภาพเคลื่อนไหวคล้ายกัน คุณอาจต้อง แสดงเอฟเฟ็กต์ สำหรับจำนวนเงินขั้นต่ำ
แทนที่จะหยุดทันทีถ้าไม่ได้กดปุ่มอีกต่อไป ถึง
คุณจะสามารถเริ่มและหยุดภาพเคลื่อนไหวจากภายในคอลเล็กชันได้โดยตรง
แลมบ์ดา แทนที่จะเขียนถึงรัฐ มีตัวอย่างของรูปแบบนี้ใน
ส่วนสร้าง Indication
ขั้นสูงที่มีเส้นขอบแบบเคลื่อนไหว
ตัวอย่าง: สร้างคอมโพเนนต์ที่มีการจัดการการโต้ตอบที่กำหนดเอง
หากต้องการดูว่าคุณสามารถสร้างคอมโพเนนต์ที่มีการตอบสนองแบบกำหนดเองต่ออินพุตได้ที่นี่ ตัวอย่างของปุ่มที่มีการแก้ไข ในกรณีนี้ สมมติว่าคุณต้องการปุ่ม ตอบสนองต่อการกดโดยการเปลี่ยนลักษณะที่ปรากฏ ดังนี้
![ภาพเคลื่อนไหวของปุ่มที่เพิ่มไอคอนรถเข็นช็อปปิ้งแบบไดนามิกเมื่อคลิก](https://developer.android.google.cn/static/develop/ui/compose/images/handling-interactions-iconbutton.gif?authuser=0000&hl=th)
หากต้องการทำเช่นนี้ ให้สร้าง Composable ที่กำหนดเองโดยอิงตาม Button
และ
พารามิเตอร์ icon
เพิ่มเติมเพื่อวาดไอคอน (ในกรณีนี้คือรถเข็นช็อปปิ้ง) คุณ
เรียกใช้ collectIsPressedAsState()
เพื่อติดตามว่าผู้ใช้วางเมาส์เหนือ
คุณจะเพิ่มไอคอนได้ โค้ดมีลักษณะดังนี้
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
และนี่คือลักษณะของการใช้ Composable ใหม่นั้น:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
เนื่องจาก PressIconButton
ใหม่นี้สร้างขึ้นจาก Material ที่มีอยู่
Button
โต้ตอบกับการโต้ตอบของผู้ใช้ตามปกติ เมื่อผู้ใช้
เมื่อกดปุ่ม ปุ่มก็จะเปลี่ยนความทึบแสงเล็กน้อย เหมือนปุ่มทั่วไป
เนื้อหา Button
สร้างและใช้เอฟเฟกต์ที่กำหนดเองแบบใช้ซ้ำได้ด้วย Indication
ในส่วนก่อนหน้านี้ คุณได้เรียนรู้วิธีเปลี่ยนบางส่วนของคอมโพเนนต์ในการตอบกลับแล้ว
ไปยังInteraction
ต่างๆ เช่น แสดงไอคอนเมื่อกด เดียวกันนี้
สามารถใช้วิธีการเปลี่ยนค่าของพารามิเตอร์ที่คุณระบุเป็น
หรือเปลี่ยนเนื้อหาที่แสดงภายในคอมโพเนนต์
ใช้ได้แบบรายคอมโพเนนต์เท่านั้น บ่อยครั้งที่แอปพลิเคชันหรือระบบการออกแบบ
จะมีระบบทั่วไปของเอฟเฟกต์ภาพแบบเก็บสถานะ ซึ่งก็คือเอฟเฟกต์ที่ควร
กับคอมโพเนนต์ทั้งหมดอย่างสอดคล้องกัน
หากคุณกำลังสร้างระบบการออกแบบประเภทนี้ ให้ปรับแต่งส่วนประกอบหนึ่งและ การนำการปรับแต่งนี้ไปใช้ซ้ำสำหรับคอมโพเนนต์อื่นๆ อาจเป็นเรื่องยากสำหรับ เหตุผลต่อไปนี้
- คอมโพเนนต์ทั้งหมดในระบบการออกแบบต้องมีต้นแบบเดียวกัน
- เป็นเรื่องง่ายที่จะลืมใช้เอฟเฟ็กต์นี้กับคอมโพเนนต์ที่สร้างใหม่และ คอมโพเนนต์ที่คลิกได้
- การรวมเอฟเฟกต์ที่กำหนดเองกับเอฟเฟกต์อื่นๆ อาจเป็นเรื่องยาก
เพื่อหลีกเลี่ยงปัญหาเหล่านี้และปรับขนาดคอมโพเนนต์ที่กำหนดเองในระบบได้อย่างง่ายดาย
คุณสามารถใช้ Indication
Indication
แสดงถึงเอฟเฟกต์ภาพที่นํามาใช้ใหม่ได้ซึ่งใช้ได้กับ
องค์ประกอบในแอปพลิเคชันหรือระบบการออกแบบ Indication
แบ่งออกเป็น 2 ส่วน
ชิ้นส่วน:
IndicationNodeFactory
: โรงงานที่สร้างอินสแตนซ์Modifier.Node
ซึ่ง แสดงผลเอฟเฟกต์ภาพสำหรับคอมโพเนนต์ สำหรับการใช้งานที่ง่ายขึ้นซึ่งไม่ ที่เปลี่ยนไปในคอมโพเนนต์ต่างๆ ซึ่งอาจเป็นแบบเดี่ยว (ออบเจ็กต์) และนำมาใช้ซ้ำใน แอปพลิเคชันทั้งหมดอินสแตนซ์เหล่านี้จะเป็นแบบเก็บสถานะหรือไม่เก็บสถานะก็ได้ เนื่องจากสร้างตาม คอมโพเนนต์จะดึงค่าจาก
CompositionLocal
เพื่อเปลี่ยนวิธี แสดงหรือทำงานภายในคอมโพเนนต์ใดคอมโพเนนต์หนึ่งModifier.Node
Modifier.indication
: แป้นกดร่วมที่วาดเป็นIndication
สำหรับ คอมโพเนนต์Modifier.clickable
และตัวแก้ไขการโต้ตอบระดับสูงอื่นๆ ยอมรับพารามิเตอร์การบ่งชี้โดยตรง จึงไม่เพียงแค่แสดงInteraction
แต่วาดเอฟเฟกต์ภาพสำหรับInteraction
ได้ด้วย ปล่อยออกมา ดังนั้นสำหรับกรณีง่ายๆ คุณสามารถใช้Modifier.clickable
ได้โดยไม่ต้อง ต้องใช้Modifier.indication
แทนที่เอฟเฟกต์ด้วย Indication
หัวข้อนี้จะอธิบายวิธีแทนที่ผลกระทบการปรับขนาดด้วยตนเองที่ใช้กับ ปุ่มที่มีสัญลักษณ์เทียบเท่าซึ่งสามารถนำไปใช้ซ้ำได้ในหลายๆ ปุ่ม คอมโพเนนต์
โค้ดต่อไปนี้จะสร้างปุ่มที่ปรับขนาดลงเมื่อกด:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
หากต้องการแปลงเอฟเฟกต์มาตราส่วนในตัวอย่างด้านบนเป็น Indication
ให้ทำตาม
ขั้นตอนเหล่านี้:
สร้าง
Modifier.Node
มีหน้าที่ใช้เอฟเฟกต์การปรับขนาด เมื่อแนบแล้ว โหนดจะสังเกตแหล่งที่มาของการโต้ตอบ คล้ายกับลักษณะก่อนหน้า ตัวอย่าง ความแตกต่างเพียงอย่างเดียวตรงนี้คือ การเปิดใช้ภาพเคลื่อนไหวโดยตรง แทนการแปลงการโต้ตอบขาเข้าเป็นสถานะโหนดต้องใช้
DrawModifierNode
เพื่อให้ลบล้างได้ContentDrawScope#draw()
และแสดงเอฟเฟ็กต์มาตราส่วนโดยใช้ภาพวาดเดียวกัน เช่นเดียวกับกราฟิก API อื่นๆ ใน Composeการโทรหา
drawContent()
ว่างจากเครื่องรับContentDrawScope
จะจับรางวัล คอมโพเนนต์จริงที่ควรใช้Indication
ดังนั้นคุณจึงเพียง ต้องเรียกใช้ฟังก์ชันนี้ภายในการแปลงขนาด ตรวจสอบว่า การติดตั้งใช้งานIndication
จะเรียกใช้drawContent()
เสมอเมื่อใดก็ตาม มิฉะนั้นจะไม่มีการวาดคอมโพเนนต์ที่คุณใช้Indication
private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
สร้าง
IndicationNodeFactory
หน้าที่ของเราคือการสร้าง อินสแตนซ์ของโหนดใหม่สำหรับแหล่งที่มาของการโต้ตอบที่ระบุ เนื่องจากไม่มี พารามิเตอร์มากำหนดค่าตัวบ่งชี้ โรงงานสามารถเป็นออบเจ็กต์ได้ ดังนี้object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
ใช้Modifier.indication
เป็นการภายใน ดังนั้นเพื่อสร้าง คอมโพเนนต์ที่คลิกได้ด้วยScaleIndication
คุณเพียงแค่ระบุIndication
เป็นพารามิเตอร์ไปยังclickable
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
วิธีนี้ยังช่วยให้สร้างคอมโพเนนต์ระดับสูงที่นำมาใช้ใหม่ได้ โดยใช้
Indication
— ปุ่มอาจมีลักษณะดังนี้@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
จากนั้นคุณสามารถใช้ปุ่มในลักษณะต่อไปนี้
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
![ภาพเคลื่อนไหวของปุ่มที่มีไอคอนรถเข็นของชำจะเล็กลงเมื่อกด](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-custom-indication.gif?authuser=0000&hl=th)
Indication
ที่กำหนดเองสร้าง Indication
ขั้นสูงพร้อมเส้นขอบแบบภาพเคลื่อนไหว
Indication
ไม่ได้จำกัดอยู่เพียงผลของการเปลี่ยนรูปแบบเท่านั้น เช่น การปรับขนาด
คอมโพเนนต์ เนื่องจาก IndicationNodeFactory
แสดง Modifier.Node
คุณจึงวาด
ผลกระทบใดๆ ก็ตามที่อยู่เหนือหรือใต้เนื้อหาเช่นเดียวกับ API สำหรับภาพวาดอื่นๆ สำหรับ
เช่น คุณสามารถวาดเส้นขอบแบบเคลื่อนไหวรอบๆ คอมโพเนนต์และการวางซ้อนบน
ด้านบนของคอมโพเนนต์เมื่อกด
![ปุ่มที่มีเอฟเฟกต์สายรุ้งสวยงามเมื่อกด](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/rainbow-effect.gif?authuser=0000&hl=th)
Indication
การใช้งาน Indication
ที่นี่คล้ายกับตัวอย่างก่อนหน้านี้มาก —
โมเดลนั้นก็จะสร้างโหนดที่มีพารามิเตอร์บางอย่าง เนื่องจากขอบที่เคลื่อนไหวขึ้นอยู่กับ
บนรูปร่างและขอบของคอมโพเนนต์ที่ใช้ Indication
การใช้งาน Indication
จะต้องระบุรูปร่างและความกว้างของขอบด้วย
เป็นพารามิเตอร์
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
การติดตั้งใช้งาน Modifier.Node
ก็มีแนวคิดเหมือนกัน แม้ว่า
โค้ดที่เขียนได้จะซับซ้อนกว่า และเช่นเคย สังเกตการณ์ InteractionSource
เมื่อแนบ เปิดภาพเคลื่อนไหว และใช้ DrawModifierNode
เพื่อวาด
ผลกระทบที่มีต่อเนื้อหา ได้แก่
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
ความแตกต่างที่สำคัญก็คือ ในขณะนี้ มีระยะเวลาขั้นต่ำสำหรับฟิลด์
ภาพเคลื่อนไหวด้วยฟังก์ชัน animateToResting()
ดังนั้นแม้ว่าการกด
ทันที ภาพเคลื่อนไหวของสื่อจะยังดำเนินต่อไป นอกจากนี้ยังมีการจัดการ
สำหรับการกดเร็วๆ หลายครั้งเมื่อเริ่มต้น animateToPressed
— หากการกด
จะเกิดขึ้นระหว่างที่มีการกด หรือ
ภาพเคลื่อนไหวขณะพักอยู่
และภาพเคลื่อนไหวของสื่อจะเริ่มขึ้นตั้งแต่ต้น เพื่อรองรับ
เอฟเฟ็กต์ที่เกิดขึ้นพร้อมกัน (เช่น คลื่นที่จะมีภาพเคลื่อนไหวระลอกคลื่น
ที่ด้านบนของระลอกคลื่นอื่นๆ) คุณสามารถติดตามภาพเคลื่อนไหวได้ในรายการ แทนที่จะต้อง
การยกเลิกภาพเคลื่อนไหวที่มีอยู่และเริ่มอันใหม่
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ทำความเข้าใจท่าทางสัมผัส
- Kotlin สำหรับ Jetpack Compose
- คอมโพเนนต์และเลย์เอาต์ของวัสดุ