หน้านี้แสดงตัวอย่างวิธีใช้ Haptics API ต่างๆ เพื่อสร้างเอฟเฟกต์ที่กำหนดเองในแอปพลิเคชัน Android เนื่องจากข้อมูลส่วนใหญ่ในหน้านี้อาศัยความรู้ที่ดีเกี่ยวกับวิธีการทํางานของตัวกระตุ้นการสั่นสะเทือน เราจึงขอแนะนําให้อ่านข้อมูลเบื้องต้นเกี่ยวกับตัวกระตุ้นการสั่นสะเทือน
หน้านี้มีตัวอย่างต่อไปนี้
- รูปแบบการสั่นที่กำหนดเอง
- รูปแบบการเพิ่ม: รูปแบบที่เริ่มต้นอย่างราบรื่น
- รูปแบบที่ซ้ำกัน: รูปแบบที่ไม่มีที่สิ้นสุด
- รูปแบบที่มีทางเลือก: การสาธิตทางเลือก
- องค์ประกอบการสั่น
ดูตัวอย่างเพิ่มเติมได้ที่เพิ่มการตอบกลับด้วยระบบสัมผัสให้กับเหตุการณ์ และปฏิบัติตามหลักการการออกแบบการสัมผัสเสมอ
ใช้การแสดงผลสำรองเพื่อจัดการความเข้ากันได้ของอุปกรณ์
เมื่อใช้เอฟเฟกต์ที่กำหนดเอง ให้พิจารณาสิ่งต่อไปนี้
- ความสามารถของอุปกรณ์ที่จำเป็นสำหรับเอฟเฟกต์
- สิ่งที่ต้องทำเมื่ออุปกรณ์เล่นเอฟเฟกต์ไม่ได้
ข้อมูลอ้างอิง Android Haptics API มีรายละเอียดเกี่ยวกับวิธีตรวจสอบการรองรับคอมโพเนนต์ที่เกี่ยวข้องกับการสัมผัสเพื่อให้แอปมอบประสบการณ์โดยรวมที่สอดคล้องกัน
คุณอาจต้องการปิดใช้เอฟเฟกต์ที่กำหนดเองหรือระบุเอฟเฟกต์ที่กำหนดเองทางเลือกโดยอิงตามความสามารถที่เป็นไปได้ต่างๆ ทั้งนี้ขึ้นอยู่กับกรณีการใช้งาน
วางแผนสำหรับความสามารถระดับสูงต่อไปนี้ของอุปกรณ์
หากคุณใช้องค์ประกอบพื้นฐานของการสัมผัส: อุปกรณ์ที่รองรับองค์ประกอบพื้นฐานเหล่านั้นซึ่งเอฟเฟกต์ที่กำหนดเองต้องใช้ (ดูรายละเอียดเกี่ยวกับพรอมิเตอได้ที่ส่วนถัดไป)
อุปกรณ์ที่มีการควบคุมแอมพลิจูด
อุปกรณ์ที่รองรับการสั่นขั้นพื้นฐาน (เปิด/ปิด) กล่าวคือ อุปกรณ์ที่ไม่มีการควบคุมแอมพลิจูด
หากตัวเลือกเอฟเฟกต์การสัมผัสของแอปของคุณคำนึงถึงหมวดหมู่เหล่านี้ ประสบการณ์การสัมผัสของผู้ใช้ควรยังคงคาดการณ์ได้สำหรับอุปกรณ์แต่ละเครื่อง
การใช้องค์ประกอบพื้นฐานของการโต้ตอบการสัมผัส
Android มีองค์ประกอบพื้นฐานของการสัมผัสหลายรายการที่แตกต่างกันทั้งในด้านแอมพลิจูดและความถี่ คุณอาจใช้องค์ประกอบพื้นฐานรายการเดียวหรือหลายรายการร่วมกันเพื่อให้ได้เอฟเฟกต์การสัมผัสที่สมจริง
- ใช้ความล่าช้า 50 มิลลิวินาทีขึ้นไปสำหรับช่องว่างที่สังเกตได้ระหว่างพรอมต์ 2 รายการ รวมถึงพิจารณาระยะเวลาของพรอมต์ด้วย หากเป็นไปได้
- ใช้มาตราส่วนที่มีอัตราส่วนต่างกันตั้งแต่ 1.4 ขึ้นไปเพื่อให้เห็นความแตกต่างของระดับความรุนแรงได้ดีขึ้น
ใช้ค่า 0.5, 0.7 และ 1.0 เพื่อสร้างองค์ประกอบพื้นฐานที่มีระดับความเข้มต่ำ ปานกลาง และสูง
สร้างรูปแบบการสั่นที่กำหนดเอง
รูปแบบการสั่นมักใช้ในการโต้ตอบการสัมผัสเพื่อดึงดูดความสนใจ เช่น การแจ้งเตือนและเสียงเรียกเข้า บริการ Vibrator
สามารถเล่นรูปแบบการสั่นแบบยาวซึ่งจะเปลี่ยนแปลงความกว้างของคลื่นการสั่นเมื่อเวลาผ่านไป เอฟเฟกต์ดังกล่าวเรียกว่ารูปแบบคลื่น
เอฟเฟกต์รูปแบบคลื่นเป็นสิ่งที่ผู้ใช้รับรู้ได้ง่าย แต่การสั่นที่ยาวนานอย่างฉับพลันอาจทำให้ผู้ใช้ตกใจหากเล่นในสภาพแวดล้อมที่เงียบ การเพิ่มระดับความกว้างของสัญญาณเป้าหมายเร็วเกินไปอาจทำให้เกิดเสียงสั่นที่ได้ยินได้ คําแนะนําสําหรับการออกแบบรูปแบบรูปแบบคลื่นคือทำให้การเปลี่ยนความกว้างของคลื่นเป็นไปอย่างราบรื่นเพื่อสร้างเอฟเฟกต์การเพิ่มขึ้นและลดลง
ตัวอย่าง: รูปแบบการเพิ่มจำนวน
รูปแบบคลื่นจะแสดงเป็น VibrationEffect
โดยมีพารามิเตอร์ 3 รายการดังนี้
- ช่วงเวลา: อาร์เรย์ของระยะเวลาเป็นมิลลิวินาทีสำหรับแต่ละส่วนของรูปแบบคลื่น
- แอมพลิจูด: แอมพลิจูดการสั่นที่ต้องการสำหรับระยะเวลาแต่ละครั้งที่ระบุไว้ในอาร์กิวเมนต์แรก ซึ่งแสดงด้วยค่าจำนวนเต็มตั้งแต่ 0 ถึง 255 โดยที่ 0 แสดงถึง "ปิด" ตัวสั่น และ 255 แสดงถึงแอมพลิจูดสูงสุดของอุปกรณ์
- Repeat index: ดัชนีในอาร์เรย์ที่ระบุไว้ในอาร์กิวเมนต์แรกเพื่อเริ่มทำซ้ำรูปแบบคลื่น หรือ -1 หากควรเล่นรูปแบบเพียงครั้งเดียว
ต่อไปนี้คือตัวอย่างรูปแบบคลื่นที่ส่งสัญญาณ 2 ครั้งโดยหยุดพัก 350 มิลลิวินาทีระหว่างการส่งสัญญาณ พัลส์แรกเป็นการเพิ่มค่าอย่างราบรื่นจนถึงระดับสูงสุด และพัลส์ที่ 2 เป็นการเพิ่มค่าอย่างรวดเร็วเพื่อคงระดับสูงสุดไว้ การหยุดที่จุดสิ้นสุดจะกำหนดโดยค่าดัชนีซ้ำเชิงลบ
Kotlin
val timings: LongArray = longArrayOf(50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200) val amplitudes: IntArray = intArrayOf(33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255) val repeatIndex = -1 // Do not repeat. vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] { 50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 }; int[] amplitudes = new int[] { 33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 }; int repeatIndex = -1; // Do not repeat. vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));
ตัวอย่าง: รูปแบบที่ซ้ำกัน
นอกจากนี้ คุณยังเล่นรูปแบบคลื่นซ้ำๆ ได้จนกว่าจะยกเลิก วิธีสร้างรูปแบบคลื่นซ้ำคือตั้งค่าพารามิเตอร์ "repeat" ที่ไม่ใช่ค่าลบ เมื่อเล่นรูปแบบคลื่นซ้ำ การสั่นจะยังคงดำเนินต่อไปจนกว่าจะมีการยกเลิกอย่างชัดเจนในบริการ โดยทำดังนี้
Kotlin
void startVibrating() { val timings: LongArray = longArrayOf(50, 50, 100, 50, 50) val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64) val repeat = 1 // Repeat from the second entry, index = 1. VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat) // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect) } void stopVibrating() { vibrator.cancel() }
Java
void startVibrating() { long[] timings = new long[] { 50, 50, 100, 50, 50 }; int[] amplitudes = new int[] { 64, 128, 255, 128, 64 }; int repeat = 1; // Repeat from the second entry, index = 1. VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat); // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect); } void stopVibrating() { vibrator.cancel(); }
ซึ่งมีประโยชน์อย่างยิ่งสําหรับเหตุการณ์ที่เกิดขึ้นเป็นระยะๆ ซึ่งจําเป็นต้องให้ผู้ใช้ดำเนินการเพื่อรับทราบ ตัวอย่างเหตุการณ์ดังกล่าว ได้แก่ การโทรเข้าและสัญญาณเตือนที่ทริกเกอร์
ตัวอย่าง: รูปแบบที่มีค่าสำรอง
การควบคุมแอมพลิจูดของการสั่นเป็นความสามารถที่ขึ้นอยู่กับฮาร์ดแวร์ การเล่นรูปแบบคลื่นในอุปกรณ์ระดับล่างที่ไม่มีความสามารถนี้จะทำให้อุปกรณ์สั่นที่ระดับความกว้างสูงสุดสำหรับรายการเชิงบวกแต่ละรายการในอาร์เรย์ความกว้าง หากแอปของคุณต้องรองรับอุปกรณ์ดังกล่าว เราขอแนะนำให้ตรวจสอบว่ารูปแบบของคุณไม่ทำให้เกิดเสียงสั่นเมื่อเล่นในสภาพนั้น หรือออกแบบรูปแบบเปิด/ปิดที่เล่นเป็นทางเลือกแทนได้
Kotlin
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx)) } else { vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx)) }
Java
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx)); } else { vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx)); }
สร้างองค์ประกอบการสั่น
ส่วนนี้จะแสดงวิธีใช้เอฟเฟกต์เหล่านี้เพื่อสร้างเอฟเฟกต์ที่กำหนดเองที่ยาวขึ้นและซับซ้อนขึ้น รวมถึงสำรวจการสัมผัสที่สมจริงโดยใช้ความสามารถของฮาร์ดแวร์ที่ล้ำสมัยมากขึ้น คุณสามารถใช้เอฟเฟกต์แบบผสมผสานที่ความกว้างและความถี่แตกต่างกันเพื่อสร้างเอฟเฟกต์การสัมผัสที่ซับซ้อนมากขึ้นในอุปกรณ์ที่มีตัวกระตุ้นการสัมผัสที่มีแบนด์วิดท์ความถี่กว้างขึ้น
กระบวนการสร้างรูปแบบการสั่นที่กำหนดเองที่อธิบายไว้ก่อนหน้านี้ในหน้านี้จะอธิบายวิธีควบคุมแอมพลิจูดการสั่นเพื่อสร้างเอฟเฟกต์ที่ราบรื่นของการค่อยๆ เพิ่มและลดการสั่น สัมผัสที่สมจริงจะปรับปรุงแนวคิดนี้ด้วยการสำรวจช่วงความถี่ที่กว้างขึ้นของมอเตอร์สั่นของอุปกรณ์เพื่อให้เอฟเฟกต์ราบรื่นยิ่งขึ้น รูปแบบคลื่นเหล่านี้มีประสิทธิภาพอย่างยิ่งในการสร้างเอฟเฟกต์การค่อยๆ ดังขึ้นหรือค่อยๆ เบาลง
องค์ประกอบพื้นฐานที่อธิบายไว้ก่อนหน้านี้ในหน้านี้จะใช้โดยผู้ผลิตอุปกรณ์ ฟีเจอร์นี้ให้การสั่นที่คมชัด สั้น และน่าพอใจซึ่งสอดคล้องกับหลักการของการสัมผัสเพื่อให้การสัมผัสที่ชัดเจน ดูรายละเอียดเพิ่มเติมเกี่ยวกับความสามารถเหล่านี้และวิธีการทํางานได้ที่ข้อมูลเบื้องต้นเกี่ยวกับตัวกระตุ้นการสั่นสะเทือน
Android ไม่มีการแสดงผลสำรองสำหรับองค์ประกอบที่มีองค์ประกอบพื้นฐานที่ไม่รองรับ เราขอแนะนําให้คุณทําตามขั้นตอนต่อไปนี้
ก่อนเปิดใช้งานการสัมผัสขั้นสูง ให้ตรวจสอบว่าอุปกรณ์รองรับองค์ประกอบพื้นฐานทั้งหมดที่คุณใช้อยู่
ปิดใช้ชุดประสบการณ์การใช้งานที่สอดคล้องกันซึ่งระบบไม่รองรับ ไม่ใช่แค่เอฟเฟกต์ที่ไม่มีองค์ประกอบพื้นฐาน ข้อมูลเพิ่มเติมเกี่ยวกับวิธีตรวจสอบการรองรับของอุปกรณ์แสดงอยู่ด้านล่าง
คุณสามารถสร้างเอฟเฟกต์การสั่นที่ประกอบกันด้วย VibrationEffect.Composition
ตัวอย่างเอฟเฟกต์ที่ค่อยๆ เพิ่มขึ้นตามด้วยเอฟเฟกต์คลิกที่คมชัด
Kotlin
vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK ).compose() )
Java
vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .compose());
การสร้างการประพันธ์เพลงทำได้โดยการเพิ่มองค์ประกอบพื้นฐานที่จะเล่นตามลำดับ แต่ละรายการยังปรับขนาดได้อีกด้วย คุณจึงควบคุมความกว้างของคลื่นการสั่นที่แต่ละรายการสร้างขึ้นได้ มาตราส่วนนี้กําหนดเป็นค่าระหว่าง 0 ถึง 1 โดยที่ 0 จะแมปกับแอมพลิจูดขั้นต่ำที่ผู้ใช้รับรู้ถึงองค์ประกอบพื้นฐานนี้ (แทบไม่รู้สึก)
หากต้องการสร้างองค์ประกอบพื้นฐานเดียวกันในเวอร์ชันอ่อนและเวอร์ชันแรง เราขอแนะนำให้ใช้สเกลที่มีสัดส่วนต่างกันตั้งแต่ 1.4 ขึ้นไปเพื่อให้เห็นความแตกต่างของระดับความรุนแรงได้อย่างชัดเจน อย่าสร้างระดับความเข้มขององค์ประกอบพื้นฐานเดียวกันมากกว่า 3 ระดับ เนื่องจากระดับความเข้มเหล่านี้จะแยกแยะได้ยาก เช่น ใช้สเกล 0.5, 0.7 และ 1.0 เพื่อสร้างองค์ประกอบพื้นฐานที่มีระดับความเข้มต่ำ ปานกลาง และสูง
องค์ประกอบยังระบุเวลาหน่วงที่จะเพิ่มระหว่างองค์ประกอบพื้นฐานที่ต่อเนื่องกันได้ด้วย ความล่าช้านี้แสดงเป็นมิลลิวินาทีนับจากจุดสิ้นสุดของพรอมิเทีฟก่อนหน้า โดยทั่วไปแล้ว ช่องว่าง 5-10 มิลลิวินาทีระหว่างพรอมิตทีฟ 2 รายการจะสั้นเกินกว่าจะตรวจจับได้ ลองใช้การเว้นวรรคประมาณ 50 มิลลิวินาทีขึ้นไปหากต้องการสร้างการเว้นวรรคที่สังเกตได้ระหว่างองค์ประกอบพื้นฐาน 2 รายการ ต่อไปนี้คือตัวอย่างการเรียบเรียงที่มีเวลาหน่วง
Kotlin
val delayMs = 100 vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs ).compose() )
Java
int delayMs = 100; vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs) .compose());
คุณสามารถใช้ API ต่อไปนี้เพื่อยืนยันการรองรับอุปกรณ์สำหรับพรอมต์ที่เฉพาะเจาะจง
Kotlin
val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK if (vibrator.areAllPrimitivesSupported(primitive)) { vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(primitive).compose()) } else { // Play a predefined effect or custom pattern as a fallback. }
Java
int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK; if (vibrator.areAllPrimitivesSupported(primitive)) { vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(primitive).compose()); } else { // Play a predefined effect or custom pattern as a fallback. }
นอกจากนี้ คุณยังตรวจสอบองค์ประกอบพื้นฐานหลายรายการแล้วเลือกองค์ประกอบที่จะคอมโพสิทตามระดับการรองรับของอุปกรณ์ได้ด้วย
Kotlin
val effects: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives);
Java
int[] primitives = new int[] { VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK }; boolean[] supported = vibrator.arePrimitivesSupported(effects);
ตัวอย่าง: ต้านทาน (การนับที่ต่ำ)
คุณควบคุมความกว้างของคลื่นการสั่นขั้นต้นเพื่อสื่อความคิดเห็นที่เป็นประโยชน์เกี่ยวกับการดำเนินการที่กำลังดำเนินอยู่ได้ คุณสามารถใช้ค่าสเกลที่เว้นระยะห่างกันไม่มากเพื่อสร้างเอฟเฟกต์การค่อยๆ ดังขึ้นอย่างราบรื่นของรูปเรขาคณิตพื้นฐาน นอกจากนี้ คุณยังตั้งค่าเวลาหน่วงระหว่าง Primitive ติดต่อกันแบบไดนามิกตามการโต้ตอบของผู้ใช้ได้ด้วย ตัวอย่างต่อไปนี้แสดงภาพเคลื่อนไหวของมุมมองที่ควบคุมด้วยการลากและเสริมด้วยระบบสัมผัส

รูปที่ 1 รูปแบบคลื่นนี้แสดงการเร่งความเร็วเอาต์พุตของการสั่นในอุปกรณ์
Kotlin
@Composable fun ResistScreen() { // Control variables for the dragging of the indicator. var isDragging by remember { mutableStateOf(false) } var dragOffset by remember { mutableStateOf(0f) } // Only vibrates while the user is dragging if (isDragging) { LaunchedEffect(Unit) { // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. while (true) { // Calculate the interval inversely proportional to the drag offset. val vibrationInterval = calculateVibrationInterval(dragOffset) // Calculate the scale directly proportional to the drag offset. val vibrationScale = calculateVibrationScale(dragOffset) delay(vibrationInterval) vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, vibrationScale ).compose() ) } } } Screen() { Column( Modifier .draggable( orientation = Orientation.Vertical, onDragStarted = { isDragging = true }, onDragStopped = { isDragging = false }, state = rememberDraggableState { delta -> dragOffset += delta } ) ) { // Build the indicator UI based on how much the user has dragged it. ResistIndicator(dragOffset) } } }
Java
class DragListener implements View.OnTouchListener { // Control variables for the dragging of the indicator. private int startY; private int vibrationInterval; private float vibrationScale; @Override public boolean onTouch(View view, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startY = event.getRawY(); vibrationInterval = calculateVibrationInterval(0); vibrationScale = calculateVibrationScale(0); startVibration(); break; case MotionEvent.ACTION_MOVE: float dragOffset = event.getRawY() - startY; // Calculate the interval inversely proportional to the drag offset. vibrationInterval = calculateVibrationInterval(dragOffset); // Calculate the scale directly proportional to the drag offset. vibrationScale = calculateVibrationScale(dragOffset); // Build the indicator UI based on how much the user has dragged it. updateIndicator(dragOffset); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // Only vibrates while the user is dragging cancelVibration(); break; } return true; } private void startVibration() { vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, vibrationScale) .compose()); // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. handler.postDelayed(this::startVibration, vibrationInterval); } private void cancelVibration() { handler.removeCallbacksAndMessages(null); } }
ตัวอย่าง: ขยาย (มีเสียงดังขึ้นและเบาลง)
พรอมต์สำหรับเพิ่มความแรงของการสั่นที่รับรู้มี 2 รายการ ได้แก่ PRIMITIVE_QUICK_RISE
และ PRIMITIVE_SLOW_RISE
แคมเปญทั้ง 2 แคมเปญเข้าถึงเป้าหมายเดียวกัน แต่มีระยะเวลาต่างกัน Primitive สำหรับการลดระดับมีเพียง 1 รายการเท่านั้น นั่นคือ PRIMITIVE_QUICK_FALL
องค์ประกอบพื้นฐานเหล่านี้ทำงานร่วมกันได้ดียิ่งขึ้นเพื่อสร้างส่วนของรูปแบบคลื่นที่เพิ่มความเข้มขึ้นแล้วค่อยๆ เบาลง คุณสามารถจัดแนวองค์ประกอบพื้นฐานที่ปรับขนาดแล้วเพื่อป้องกันไม่ให้ความกว้างของรูปคลื่นเพิ่มขึ้นอย่างฉับพลันระหว่างองค์ประกอบ ซึ่งยังช่วยยืดระยะเวลาของเอฟเฟกต์โดยรวมได้ด้วย ในแง่การรับรู้ ผู้คนมักจะสังเกตเห็นส่วนที่เป็นขาขึ้นมากกว่าส่วนที่เป็นขาลง ดังนั้นการทําให้ส่วนที่เป็นขาขึ้นสั้นกว่าส่วนที่เป็นขาลงจึงอาจใช้เพื่อเปลี่ยนการเน้นไปที่ส่วนที่เป็นขาลงได้
ต่อไปนี้เป็นตัวอย่างการใช้การคอมโพสนี้เพื่อขยายและยุบวงกลม เอฟเฟกต์การเพิ่มขึ้นจะช่วยเพิ่มความรู้สึกว่ามีการขยายตัวระหว่างภาพเคลื่อนไหว การรวมเอฟเฟกต์การเพิ่มขึ้นและการลดลงจะช่วยเน้นการยุบตัวในตอนท้ายของภาพเคลื่อนไหว

รูปที่ 2 รูปแบบคลื่นนี้แสดงการเร่งความเร็วเอาต์พุตของการสั่นในอุปกรณ์
Kotlin
enum class ExpandShapeState { Collapsed, Expanded } @Composable fun ExpandScreen() { // Control variable for the state of the indicator. var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) } // Animation between expanded and collapsed states. val transitionData = updateTransitionData(currentState) Screen() { Column( Modifier .clickable( { if (currentState == ExpandShapeState.Collapsed) { currentState = ExpandShapeState.Expanded vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f ).compose() ) } else { currentState = ExpandShapeState.Collapsed vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE ).compose() ) } ) ) { // Build the indicator UI based on the current state. ExpandIndicator(transitionData) } } }
Java
class ClickListener implements View.OnClickListener { private final Animation expandAnimation; private final Animation collapseAnimation; private boolean isExpanded; ClickListener(Context context) { expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand); expandAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f) .compose()); } }); collapseAnimation = AnimationUtils.loadAnimation(context, R.anim.collapse); collapseAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE) .compose()); } }); } @Override public void onClick(View view) { view.startAnimation(isExpanded ? collapseAnimation : expandAnimation); isExpanded = !isExpanded; } }
ตัวอย่าง: การสั่น (ด้วยการหมุน)
หลักการสําคัญของสัมผัสอย่างหนึ่งคือการสร้างความพึงพอใจให้แก่ผู้ใช้ วิธีสนุกๆ ในการแสดงผลเอฟเฟกต์การสั่นที่ไม่คาดคิดซึ่งน่าพอใจคือการใช้ PRIMITIVE_SPIN
พรอมต์นี้มีประสิทธิภาพสูงสุดเมื่อเรียกใช้มากกว่า 1 ครั้ง การต่อภาพการหมุนหลายภาพเข้าด้วยกันอาจสร้างเอฟเฟกต์ที่โยกเยกและไม่เสถียร ซึ่งสามารถปรับปรุงให้ดียิ่งขึ้นได้โดยใช้การปรับขนาดแบบสุ่มเล็กน้อยกับรูปเรขาคณิตพื้นฐานแต่ละรูป นอกจากนี้ คุณยังทดสอบระยะห่างระหว่าง Primitive การหมุนที่ต่อเนื่องกันได้ด้วย การบิด 2 ครั้งโดยไม่มีช่องว่าง (0 มิลลิวินาที) จะทำให้เกิดความรู้สึกว่าภาพบิดไปมา การเพิ่มช่วงเวลาระหว่างการหมุนจาก 10 เป็น 50 มิลลิวินาทีจะทำให้การหมุนดูหลวมขึ้น และสามารถใช้เพื่อจับคู่ระยะเวลาของวิดีโอหรือภาพเคลื่อนไหว
เราไม่แนะนำให้ใช้ช่วงเวลาที่นานกว่า 100 มิลลิวินาที เนื่องจากภาพสไลด์ที่แสดงต่อกันจะไม่ผสานรวมกันได้ดีและเริ่มดูเหมือนเป็นเอฟเฟกต์แต่ละรายการ
ต่อไปนี้เป็นตัวอย่างของรูปร่างยืดหยุ่นที่เด้งกลับหลังจากลากลงแล้วปล่อย ภาพเคลื่อนไหวได้รับการปรับปรุงด้วยเอฟเฟกต์การหมุน 2 รายการที่เล่นด้วยความรุนแรงที่แตกต่างกันตามการกระเด้ง

รูปที่ 3 รูปแบบคลื่นนี้แสดงการเร่งความเร็วเอาต์พุตของการสั่นในอุปกรณ์
Kotlin
@Composable fun WobbleScreen() { // Control variables for the dragging and animating state of the elastic. var dragDistance by remember { mutableStateOf(0f) } var isWobbling by remember { mutableStateOf(false) } // Use drag distance to create an animated float value behaving like a spring. val dragDistanceAnimated by animateFloatAsState( targetValue = if (dragDistance > 0f) dragDistance else 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessMedium ), ) if (isWobbling) { LaunchedEffect(Unit) { while (true) { val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE // Use some sort of minimum displacement so the final few frames // of animation don't generate a vibration. if (displacement > SPIN_MIN_DISPLACEMENT) { vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement) ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement) ).compose() ) } // Delay the next check for a sufficient duration until the current // composition finishes. Note that you can use // Vibrator.getPrimitiveDurations API to calculcate the delay. delay(VIBRATION_DURATION) } } } Box( Modifier .fillMaxSize() .draggable( onDragStopped = { isWobbling = true dragDistance = 0f }, orientation = Orientation.Vertical, state = rememberDraggableState { delta -> isWobbling = false dragDistance += delta } ) ) { // Draw the wobbling shape using the animated spring-like value. WobbleShape(dragDistanceAnimated) } } // Calculate a random scale for each spin to vary the full effect. fun nextSpinScale(displacement: Float): Float { // Generate a random offset in [-0.1,+0.1] to be added to the vibration // scale so the spin effects have slightly different values. val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f) }
Java
class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener { private final Random vibrationRandom = new Random(seed); private final long lastVibrationUptime; @Override public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { // Delay the next check for a sufficient duration until the current // composition finishes. Note that you can use // Vibrator.getPrimitiveDurations API to calculcate the delay. if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) { return; } float displacement = calculateRelativeDisplacement(value); // Use some sort of minimum displacement so the final few frames // of animation don't generate a vibration. if (displacement < SPIN_MIN_DISPLACEMENT) { return; } lastVibrationUptime = SystemClock.uptimeMillis(); vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement)) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement)) .compose()); } // Calculate a random scale for each spin to vary the full effect. float nextSpinScale(float displacement) { // Generate a random offset in [-0.1,+0.1] to be added to the vibration // scale so the spin effects have slightly different values. float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f return MathUtils.clamp(displacement + randomOffset, 0f, 1f) } }
ตัวอย่าง: เสียงกระโดด (พร้อมเสียงดังตุบ)
การใช้งานขั้นสูงอีกอย่างหนึ่งของเอฟเฟกต์การสั่นคือการจำลองการโต้ตอบทางกายภาพ PRIMITIVE_THUD
สามารถสร้างเอฟเฟกต์ที่ดังก้องกังวาน ซึ่งสามารถจับคู่กับภาพแสดงผลของการกระแทกในวิดีโอหรือภาพเคลื่อนไหวได้ เพื่อเพิ่มประสบการณ์โดยรวม
ต่อไปนี้คือตัวอย่างภาพเคลื่อนไหวการปล่อยบอลที่เรียบง่ายซึ่งเพิ่มเอฟเฟกต์เสียงดังทุกครั้งที่บอลเด้งออกจากด้านล่างของหน้าจอ

รูปที่ 4 รูปแบบคลื่นนี้แสดงการเร่งความเร็วเอาต์พุตของการสั่นในอุปกรณ์
Kotlin
enum class BallPosition { Start, End } @Composable fun BounceScreen() { // Control variable for the state of the ball. var ballPosition by remember { mutableStateOf(BallPosition.Start) } var bounceCount by remember { mutableStateOf(0) } // Animation for the bouncing ball. var transitionData = updateTransitionData(ballPosition) val collisionData = updateCollisionData(transitionData) // Ball is about to contact floor, only vibrating once per collision. var hasVibratedForBallContact by remember { mutableStateOf(false) } if (collisionData.collisionWithFloor) { if (!hasVibratedForBallContact) { val vibrationScale = 0.7.pow(bounceCount++).toFloat() vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale ).compose() ) hasVibratedForBallContact = true } } else { // Reset for next contact with floor. hasVibratedForBallContact = false } Screen() { Box( Modifier .fillMaxSize() .clickable { if (transitionData.isAtStart) { ballPosition = BallPosition.End } else { ballPosition = BallPosition.Start bounceCount = 0 } }, ) { // Build the ball UI based on the current state. BouncingBall(transitionData) } } }
Java
class ClickListener implements View.OnClickListener { @Override public void onClick(View view) { view.animate() .translationY(targetY) .setDuration(3000) .setInterpolator(new BounceInterpolator()) .setUpdateListener(new AnimatorUpdateListener() { boolean hasVibratedForBallContact = false; int bounceCount = 0; @Override public void onAnimationUpdate(ValueAnimator animator) { boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98; if (valueBeyondThreshold) { if (!hasVibratedForBallContact) { float vibrationScale = (float) Math.pow(0.7, bounceCount++); vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale) .compose()); hasVibratedForBallContact = true; } } else { // Reset for next contact with floor. hasVibratedForBallContact = false; } } }); } }