หน้านี้แสดงตัวอย่างวิธีใช้ Haptics API ต่างๆ เพื่อสร้างเอฟเฟกต์ที่กำหนดเองนอกเหนือจากรูปแบบคลื่นการสั่นมาตรฐานในแอป Android
หน้านี้ประกอบด้วยตัวอย่างต่อไปนี้
- องค์ประกอบที่ยึดตามไทม์ไลน์ด้วย
VibrationEffect.Builder- เขียนด้วยค่าที่กำหนดล่วงหน้า: จัดลำดับความรู้สึกจากการสัมผัสที่กำหนดไว้ล่วงหน้า
- แต่งเพลงด้วย Envelope และค่าที่กำหนดล่วงหน้า: รวม Envelope และค่าที่กำหนดล่วงหน้าตามไทม์ไลน์
- ใช้ซ้ำและเปลี่ยนเหตุการณ์: เปลี่ยนและใช้เหตุการณ์การจัดองค์ประกอบที่มีอยู่ซ้ำ
- องค์ประกอบที่เกิดซ้ำ: สร้างเอฟเฟกต์ไทม์ไลน์ที่เกิดซ้ำ
- รูปแบบการสั่นที่กำหนดเอง
- รูปแบบการเพิ่มขึ้น: รูปแบบที่เริ่มต้นอย่างราบรื่น
- รูปแบบที่เกิดซ้ำ: รูปแบบที่ไม่มีจุดสิ้นสุด
- รูปแบบที่มีรายการสำรอง: การสาธิตรายการสำรอง
- องค์ประกอบพื้นฐานของการสั่น
- รูปแบบคลื่นการสั่นพร้อมเอนเวโลป
- สปริงเด้งดึ๋ง: เอฟเฟกต์เด้งดึ๋งแบบสปริงที่ใช้เอฟเฟกต์ซองจดหมายพื้นฐาน
- การปล่อยจรวด: เอฟเฟกต์การปล่อยจรวดโดยใช้เอฟเฟกต์ Waveform Envelope
ดูตัวอย่างเพิ่มเติมได้ที่เพิ่มการตอบสนองแบบรู้สึกได้ลงในเหตุการณ์ และปฏิบัติตามหลักการออกแบบการโต้ตอบการสัมผัสเสมอ
ใช้โฆษณาสำรองเพื่อจัดการความเข้ากันได้ของอุปกรณ์
เมื่อใช้เอฟเฟกต์การสัมผัสที่กำหนดเอง ความเข้ากันได้ของอุปกรณ์และลักษณะการทำงานสำรองจะขึ้นอยู่กับแพลตฟอร์ม API ที่คุณเลือก ดังนี้
VibrationEffect.Builder(แนะนำ): ตั้งแต่ Android 16 (26Q4) เป็นต้นไป เอฟเฟกต์ที่สร้างโดยใช้VibrationEffect.Builderจะมีการทำงานสำรองระดับเฟรมเวิร์กอัตโนมัติ หากอุปกรณ์ไม่รองรับPresetที่ขอหรือEnvelopeพื้นฐานโดยค่าเริ่มต้น เฟรมเวิร์กจะแปลเป็นทางเลือกที่เหมาะสมโดยอัตโนมัติในเวลาเล่นอย่างสุดความสามารถ คุณไม่จำเป็นต้องตรวจสอบความสามารถของอุปกรณ์ต่อพรีมิติฟด้วยตนเองก่อนเล่นเอฟเฟกต์ที่ประกอบด้วยVibrationEffect.Builder- ข้อยกเว้น: รูปคลื่นขั้นสูงที่สร้างด้วย
WaveformEnvelopeBuilderไม่รองรับการสำรองอัตโนมัติเนื่องจากขึ้นอยู่กับการแมปความถี่ของฮาร์ดแวร์ที่เฉพาะเจาะจง (FOAM) หากไม่รองรับ ระบบจะไม่เล่นไฟล์ดังกล่าว
- ข้อยกเว้น: รูปคลื่นขั้นสูงที่สร้างด้วย
VibrationEffect.Composition: องค์ประกอบที่สร้างด้วยstartComposition()API จะไม่มีการสำรองอัตโนมัติ หากองค์ประกอบมีองค์ประกอบพื้นฐานที่ไม่รองรับแม้เพียงรายการเดียว การสั่นทั้งหมดจะเล่นไม่สำเร็จ คุณต้องตรวจสอบความสามารถด้วยตนเองโดยใช้vibrator.arePrimitivesSupported()- รูปคลื่นที่มีการควบคุมแอมพลิจูด: แอมพลิจูดที่ไม่ใช่ 0 จะปัดขึ้นเป็น 100% ในอุปกรณ์ที่ไม่มีการควบคุมแอมพลิจูด ตรวจสอบ
vibrator.hasAmplitudeControl()และกลับไปใช้รูปแบบเปิด/ปิดที่ออกแบบไว้อย่างชัดเจนหากจำเป็น
การใช้การโต้ตอบการสัมผัสพื้นฐาน
Android มีองค์ประกอบพื้นฐานของการสัมผัสหลายอย่างซึ่งแตกต่างกันทั้งในด้านแอมพลิจูดและความถี่ คุณอาจใช้พรีมิติฟเพียงรายการเดียวหรือหลายรายการร่วมกันเพื่อให้ได้เอฟเฟกต์การสั่นที่สมบูรณ์
- ใช้การหน่วงเวลา 50 มิลลิวินาทีขึ้นไปเพื่อให้มีช่องว่างที่สังเกตเห็นได้ระหว่างองค์ประกอบพื้นฐาน 2 รายการ และคำนึงถึงระยะเวลาขององค์ประกอบพื้นฐานด้วยหากเป็นไปได้
- ใช้สเกลที่แตกต่างกันในอัตราส่วน 1.4 ขึ้นไปเพื่อให้รับรู้ความแตกต่างของความหนักได้ดีขึ้น
ใช้สเกล 0.5, 0.7 และ 1.0 เพื่อสร้างเวอร์ชันความเข้มต่ำ ปานกลาง และสูงขององค์ประกอบพื้นฐาน
สร้างรูปแบบการสั่นที่กำหนดเอง
รูปแบบการสั่นมักใช้ในการโต้ตอบการสัมผัสเพื่อดึงดูดความสนใจ เช่น การแจ้งเตือนและเสียงเรียกเข้า บริการ Vibrator สามารถเล่นรูปแบบการสั่นแบบยาวที่เปลี่ยนแอมพลิจูดของการสั่นเมื่อเวลาผ่านไป เราเรียกผลกระทบเหล่านี้ว่ารูปคลื่น
โดยปกติแล้วผู้ใช้จะรับรู้ถึงเอฟเฟกต์รูปคลื่นได้ แต่การสั่นเป็นเวลานานอย่างกะทันหันอาจทำให้ผู้ใช้ตกใจได้หากเล่นในสภาพแวดล้อมที่เงียบสงบ การเพิ่มแอมพลิจูดเป้าหมายเร็วเกินไปอาจทำให้เกิดเสียงหึ่งๆ ที่ได้ยินด้วย ออกแบบรูปแบบคลื่นเพื่อปรับการเปลี่ยนแอมพลิจูดให้ราบรื่นเพื่อสร้างเอฟเฟกต์การเพิ่มและลดระดับเสียง
ตัวอย่างรูปแบบการสั่น
ส่วนต่อไปนี้แสดงตัวอย่างรูปแบบการสั่นหลายรูปแบบ
รูปแบบการเพิ่มจำนวน
รูปคลื่นจะแสดงเป็น VibrationEffect โดยมีพารามิเตอร์ 3 รายการ ดังนี้
- Timings: อาร์เรย์ของระยะเวลาเป็นมิลลิวินาทีสำหรับแต่ละส่วนของรูปแบบคลื่น
- แอมพลิจูด: แอมพลิจูดการสั่นที่ต้องการสำหรับแต่ละระยะเวลาที่ระบุในอาร์กิวเมนต์แรก ซึ่งแสดงด้วยค่าจำนวนเต็มตั้งแต่ 0 ถึง 255 โดย 0 หมายถึง "สถานะปิด" ของเครื่องสั่น และ 255 คือแอมพลิจูดสูงสุดของอุปกรณ์
- ดัชนีการทำซ้ำ: ดัชนีในอาร์เรย์ที่ระบุในอาร์กิวเมนต์แรกเพื่อเริ่มทำซ้ำรูปแบบคลื่น หรือ -1 หากควรเล่นรูปแบบเพียงครั้งเดียว
ต่อไปนี้คือตัวอย่างรูปแบบคลื่นที่สั่น 2 ครั้งโดยหยุดชั่วคราว 350 มิลลิวินาทีระหว่างการสั่นแต่ละครั้ง โดยการสั่นครั้งแรกจะค่อยๆ เพิ่มขึ้นจนถึงแอมพลิจูดสูงสุด และการสั่นครั้งที่ 2 จะเพิ่มขึ้นอย่างรวดเร็วเพื่อคงแอมพลิจูดสูงสุดไว้ การหยุดที่จุดสิ้นสุดจะกำหนดโดยค่าดัชนีการทำซ้ำที่เป็นลบ
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 // Don't repeat. vibrator.vibrate( VibrationEffect.createWaveform( timings, amplitudes, repeatIndex ) )
รูปแบบที่เกิดซ้ำ
นอกจากนี้ยังเล่นรูปคลื่นซ้ำๆ ได้จนกว่าจะยกเลิก วิธีสร้างรูปคลื่นที่เกิดซ้ำคือการตั้งค่าพารามิเตอร์ repeat ที่ไม่เป็นค่าลบ เมื่อคุณเล่นรูปคลื่นแบบซ้ำ การสั่นจะดำเนินต่อไปจนกว่าจะมีการยกเลิกอย่างชัดแจ้งในบริการ
fun 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. val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform( timings, amplitudes, repeat ) // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect) } fun stopVibrating() { vibrator.cancel() }
ซึ่งมีประโยชน์มากสำหรับเหตุการณ์ที่เกิดขึ้นเป็นระยะๆ ที่ต้องให้ผู้ใช้ดำเนินการเพื่อรับทราบ ตัวอย่างเหตุการณ์ดังกล่าว ได้แก่ สายโทรเข้าและการปลุกที่ตั้งไว้
รูปแบบที่มีโฆษณาสำรอง
การควบคุมแอมพลิจูดของการสั่นเป็นความสามารถที่ขึ้นอยู่กับฮาร์ดแวร์ การเล่นรูปแบบคลื่นในอุปกรณ์ระดับล่างที่ไม่มีความสามารถนี้จะทำให้อุปกรณ์สั่นที่แอมพลิจูดสูงสุดสำหรับแต่ละรายการที่เป็นบวกในอาร์เรย์แอมพลิจูด หากแอปของคุณจำเป็นต้องรองรับอุปกรณ์ดังกล่าว ให้ใช้รูปแบบที่ไม่สร้างเอฟเฟกต์เสียงหึ่งๆ เมื่อเล่นในสภาวะนั้น หรือออกแบบรูปแบบเปิด/ปิดที่เรียบง่ายกว่าซึ่งสามารถเล่นเป็นทางเลือกสำรองแทนได้
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate( VibrationEffect.createWaveform( smoothTimings, amplitudes, smoothRepeatIdx ) ) } else { vibrator.vibrate( VibrationEffect.createWaveform( onOffTimings, onOffRepeatIdx ) ) }
องค์ประกอบที่ยึดตามไทม์ไลน์พร้อม VibrationEffect.Builder
ตั้งแต่ Android 16 (26Q4) เป็นต้นไป VibrationEffect.Builder จะเป็น API ที่แนะนำสำหรับการสร้างเอฟเฟกต์และการจัดองค์ประกอบการสั่นที่ซับซ้อน ซึ่งช่วยให้คุณสร้างความรู้สึกจากการสัมผัสที่สื่ออารมณ์ได้โดยการจัดลำดับองค์ประกอบการสัมผัสที่แยกกันตามไทม์ไลน์แบบสัมบูรณ์ด้วย startTimeMillis
VibrationEffect.Builder รองรับการรวมการสั่นหลายประเภท ดังนี้
- ค่าที่กำหนดล่วงหน้า: การสั่นแบบรู้สึกได้ที่กำหนดไว้ล่วงหน้า (
VibrationEffect.Preset) เช่น การคลิกและการติ๊ก - Envelope: รูปคลื่นต่อเนื่องแบบไดนามิก ซึ่งรวมถึง Envelope พื้นฐานที่ไม่ขึ้นอยู่กับฮาร์ดแวร์ (
BasicEnvelopeBuilder) และ Envelope ที่มีการปรับความถี่ขั้นสูง (WaveformEnvelopeBuilder) - VibrationEffects ที่มีอยู่: องค์ประกอบพื้นฐาน (
VibrationEffect.Composition), รูปคลื่นแบบขั้นบันได (VibrationEffect.createWaveform), เอฟเฟกต์ที่กำหนดไว้ล่วงหน้า (VibrationEffect.createPredefined) และแบบครั้งเดียว (VibrationEffect.createOneShot) ที่นำเข้าโดยใช้addEvents()หรือการคัดลอกคอนสตรัคเตอร์ - ลำดับที่ซ้ำกัน: รูปแบบต่อเนื่องที่กำหนดค่าด้วย
setRepeatingEffect()
การสั่นที่สร้างด้วย VibrationEffect.Builder มีการทำงานสำรองอัตโนมัติในระดับเฟรมเวิร์กในตัวสำหรับองค์ประกอบต่างๆ (รวมถึงค่าที่กำหนดล่วงหน้า, เอนเวโลปพื้นฐาน และเอฟเฟกต์การสั่นแบบผสม) ซึ่งช่วยให้ผู้ใช้ได้รับประสบการณ์ที่สอดคล้องกันในอุปกรณ์ต่างๆ โดยไม่ต้องตรวจสอบความสามารถด้วยตนเอง (Advanced waveform envelopes created with
WaveformEnvelopeBuilder require hardware support and don't have automatic
fallback support.)
เขียนด้วยค่าที่กำหนดล่วงหน้า
ใช้ VibrationEffect.Preset เพื่อเพิ่มพัลส์การสัมผัสสั้นๆ ที่กำหนดไว้ล่วงหน้าและใช้กันทั่วไป (เช่น PRESET_CLICK, PRESET_TICK หรือ PRESET_LOW_TICK) ลงในองค์ประกอบ
ผู้ผลิตอุปกรณ์จะใช้ค่าที่กำหนดล่วงหน้าเพื่อสร้างการสั่นที่คมชัด สั้น และน่าพอใจ ซึ่งสอดคล้องกับหลักการระบบสัมผัสเพื่อให้ได้การสัมผัสที่ชัดเจน ดูรายละเอียดเพิ่มเติมเกี่ยวกับความสามารถเหล่านี้และวิธีการทำงานได้ที่ข้อมูลเบื้องต้นเกี่ยวกับแอคชูเอเตอร์การสั่น
ค่าที่กำหนดล่วงหน้าจะแทนที่ค่าพื้นฐานแบบสั้นจาก VibrationEffect.Composition API
สำหรับความรู้สึกการตอบสนองต่อการสัมผัสที่ยาวนานขึ้นหรือต่อเนื่อง เช่น การเพิ่มและลดระดับ ให้ใช้รูปแบบคลื่นเอนเวโลป (PWLE) แทน
ค่าที่กำหนดล่วงหน้าแต่ละค่าสามารถกำหนดสเกลที่ไม่บังคับระหว่าง 0.0f ถึง 1.0f และจะวางไว้ที่เวลาเริ่มต้นที่ชัดเจน (เป็นมิลลิวินาที) จากจุดเริ่มต้นขององค์ประกอบ
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
แต่งเพลงด้วย Envelope และค่าที่กำหนดล่วงหน้า
คุณสามารถรวมอินสแตนซ์ VibrationEffect.Envelope (สร้างโดยใช้ BasicEnvelopeBuilder หรือ WaveformEnvelopeBuilder) เข้ากับค่าที่กำหนดล่วงหน้าได้อย่างราบรื่นเพื่อสร้างรูปแบบการโต้ตอบด้วยการสัมผัสที่หลากหลายและมีหลายกลุ่ม
ต่อไปนี้คือตัวอย่างที่เล่นซองจดหมายแบบค่อยๆ เพิ่มและค่อยๆ จางลงอย่างราบรื่น ตามด้วยค่าที่กำหนดล่วงหน้าสำหรับการคลิกที่คมชัด
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
นำกิจกรรมที่มีอยู่มาใช้ซ้ำและเลื่อนเวลา
หากต้องการใช้ซ้ำหรือเชื่อมโยง VibrationEffect ที่มีอยู่ (รวมถึง VibrationEffect.Composition) ให้ดึงข้อมูลรายการออบเจ็กต์ VibrationEffect.Event โดยใช้ getEvents() และเพิ่มออบเจ็กต์เหล่านั้นด้วยออฟเซ็ตโดยใช้ addEvents(startTimeShiftMillis, events) (หรือส่งเอฟเฟกต์ไปยังคอนสตรักเตอร์ VibrationEffect.Builder(effect) โดยตรง) เมื่อนำเข้าอินสแตนซ์ VibrationEffect.Composition ด้วยวิธีนี้ เฟรมเวิร์กจะแปลงองค์ประกอบพื้นฐานเป็นค่าที่กำหนดล่วงหน้าโดยอัตโนมัติ ซึ่งจะเปิดใช้การรองรับการสำรองรันไทม์
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
สร้างองค์ประกอบที่เกิดซ้ำ
ใช้ setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis)
เพื่อเพิ่มรูปแบบที่ซ้ำกันลงในองค์ประกอบ
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
การจัดการเวลา การตรวจสอบความถูกต้อง และความคลาดเคลื่อน
เมื่อออกแบบองค์ประกอบด้วย VibrationEffect.Builder โปรดคำนึงถึงกฎด้านเวลาและการตรวจสอบต่อไปนี้
- เวลาเริ่มต้นที่เพิ่มขึ้นอย่างเคร่งครัด: ทุกองค์ประกอบที่เพิ่มลงในเครื่องมือสร้างต้องมี
startTimeMillisที่มากกว่าหรือเท่ากับเวลาเริ่มต้นขององค์ประกอบก่อนหน้าอย่างเคร่งครัด - การตรวจสอบเวลาบิลด์: เครื่องมือสร้างจะทำการตรวจสอบอย่างเต็มความสามารถ ณ เวลา
build()โดยใช้ระยะเวลาขององค์ประกอบที่ทราบ (หรืออย่างน้อย 1 มิลลิวินาทีสำหรับค่าที่กำหนดล่วงหน้า) หากตรวจพบการทับซ้อนที่เป็นไปไม่ได้ ระบบจะแสดงข้อผิดพลาดIllegalArgumentException - การเลื่อนการเล่นตามลำดับ: หากองค์ประกอบการสั่นก่อนหน้ายังคงเล่นอยู่เมื่อถึงเวลาเริ่มต้นขององค์ประกอบถัดไป เฟรมเวิร์กจะเลื่อนองค์ประกอบถัดไปไปยังช่วงเวลาที่พร้อมใช้งานเร็วที่สุดโดยอัตโนมัติ ซึ่งจะช่วยให้มั่นใจได้ว่าเหตุการณ์จะไม่ทับซ้อนกันและไม่มีการสั่นที่ขาดหายไป แต่อาจทำให้เวลาคลาดเคลื่อนเล็กน้อยหากกำหนดเวลาเหตุการณ์ไว้ใกล้กันเกินไป เพื่อลดการคลาดเคลื่อน ให้เว้นระยะเวลาที่เพียงพอ (เช่น 50 มิลลิวินาทีขึ้นไป) ระหว่างเหตุการณ์การสัมผัสที่ต่อเนื่องกัน
สร้างองค์ประกอบพื้นฐานของการสั่น
ส่วนนี้จะแสดงวิธีสร้างการสั่นโดยใช้ VibrationEffect.Composition องค์ประกอบพื้นฐานขององค์ประกอบที่อธิบายไว้ก่อนหน้านี้ในหน้านี้จะติดตั้งใช้งานโดยผู้ผลิตอุปกรณ์ โดยจะให้การสั่นที่สั้น กระชับ และน่าพอใจซึ่งสอดคล้องกับหลักการของระบบการสั่นเพื่อให้ได้การสั่นที่ชัดเจน ดูรายละเอียดเพิ่มเติมเกี่ยวกับความสามารถเหล่านี้และวิธีการทำงานได้ที่ข้อมูลเบื้องต้นเกี่ยวกับแอคชูเอเตอร์การสั่น
API VibrationEffect.Composition ต่างจาก VibrationEffect.Builder ตรงที่ไม่มีการสำรองอัตโนมัติสำหรับพริมิทีฟที่ไม่รองรับ ดังนั้น
ก่อนเปิดใช้งานการโต้ตอบการสัมผัสขั้นสูง ให้ตรวจสอบว่าอุปกรณ์ที่ระบุรองรับองค์ประกอบพื้นฐานทั้งหมดที่คุณใช้
ปิดใช้ชุดประสบการณ์ที่สอดคล้องกันซึ่งไม่รองรับ ไม่ใช่แค่เอฟเฟกต์ที่ไม่มีองค์ประกอบพื้นฐาน
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(หรือ.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(หรือ.addEvents(0L, compositionEffect.getEvents()))
เมื่อสร้างด้วย VibrationEffect.Builder เฟรมเวิร์กจะแปลงองค์ประกอบพื้นฐานขององค์ประกอบเป็นค่าที่กำหนดล่วงหน้า และให้ทางเลือกสำรองรันไทม์โดยอัตโนมัติหากอุปกรณ์ของผู้ใช้ไม่รองรับองค์ประกอบพื้นฐานใดๆ
เอฟเฟกต์การสั่นที่ประกอบขึ้น
คุณสร้างเอฟเฟกต์การสั่นที่ประกอบขึ้นได้ด้วย VibrationEffect.Composition ตัวอย่างเอฟเฟกต์เสียงที่ค่อยๆ ดังขึ้นตามด้วยเสียงคลิกที่ชัดเจน
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 รายการ ต่อไปนี้คือตัวอย่างขององค์ประกอบที่มีการหน่วงเวลา
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() )
ตรวจสอบว่าระบบรองรับองค์ประกอบพื้นฐานใดบ้าง
คุณใช้ API ต่อไปนี้เพื่อยืนยันการรองรับอุปกรณ์สำหรับองค์ประกอบพื้นฐานที่เฉพาะเจาะจงได้
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. }
นอกจากนี้ คุณยังตรวจสอบองค์ประกอบพื้นฐานหลายรายการ แล้วตัดสินใจว่าจะเขียนองค์ประกอบใดโดยอิงตามระดับการรองรับของอุปกรณ์ได้ด้วย
val primitives: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)
ตัวอย่างองค์ประกอบการสั่น
ส่วนต่อไปนี้แสดงตัวอย่างองค์ประกอบการสั่นหลายรายการที่นำมาจากแอปตัวอย่างระบบการสั่นใน GitHub
ต้านทาน (มีเห็บน้อย)
คุณสามารถควบคุมแอมพลิจูดของการสั่นพื้นฐานเพื่อสื่อสารการตอบสนองที่เป็นประโยชน์ต่อการดำเนินการที่กำลังดำเนินอยู่ ค่าสเกลที่อยู่ใกล้กันสามารถใช้เพื่อสร้างเอฟเฟกต์การค่อยๆ เพิ่มระดับเสียงของพรีมิติฟได้อย่างราบรื่น นอกจากนี้ยังตั้งค่าการหน่วงเวลาระหว่างองค์ประกอบพื้นฐานที่ต่อเนื่องกันแบบไดนามิกตามการโต้ตอบของผู้ใช้ได้ด้วย ตัวอย่างต่อไปนี้แสดงภาพเคลื่อนไหวของมุมมองที่ควบคุมโดยท่าทางการลากและเสริมด้วยการโต้ตอบการสัมผัส
รูปที่ 1 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์
@Composable fun ResistScreen() { // Control variables for the dragging of the indicator. var isDragging by remember { mutableStateOf(false) } var dragOffset by remember { mutableFloatStateOf(0f) } // Only vibrates while the user is dragging LaunchedEffect(isDragging) { // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. while (isDragging) { // 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() ) } } 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) } }
ขยาย (มีขึ้นและลง)
องค์ประกอบพื้นฐานสำหรับการเพิ่มความหนักของการสั่นที่รับรู้ได้มี 2 อย่าง ได้แก่
PRIMITIVE_QUICK_RISE และ PRIMITIVE_SLOW_RISE ทั้ง 2 ตัวเลือกเข้าถึงเป้าหมายเดียวกัน แต่มีระยะเวลาต่างกัน มีพรีมิติฟเพียงรายการเดียวสำหรับการลดระดับ ซึ่งก็คือ PRIMITIVE_QUICK_FALL องค์ประกอบพื้นฐานเหล่านี้ทำงานร่วมกันได้ดีขึ้นเพื่อสร้างส่วนรูปแบบคลื่นที่มีความหนักเพิ่มขึ้นแล้วค่อยๆ ลดลง คุณสามารถจัดตำแหน่งองค์ประกอบพื้นฐานที่ปรับขนาดเพื่อป้องกันไม่ให้แอมพลิจูดระหว่างองค์ประกอบพื้นฐานกระโดดขึ้นอย่างกะทันหัน ซึ่งยังช่วยยืดระยะเวลาของเอฟเฟกต์โดยรวมได้ดีอีกด้วย
ในแง่ของการรับรู้ ผู้คนมักจะสังเกตเห็นส่วนที่เพิ่มขึ้นมากกว่าส่วนที่ลดลง ดังนั้นการทำให้ส่วนที่เพิ่มขึ้นสั้นกว่าส่วนที่ลดลงจึงสามารถใช้เพื่อเปลี่ยนการเน้นไปยังส่วนที่ลดลงได้
ต่อไปนี้เป็นตัวอย่างการใช้ Composition นี้เพื่อขยายและยุบวงกลม เอฟเฟกต์การลอยขึ้นจะช่วยเพิ่มความรู้สึกของการขยายตัวในระหว่างการเคลื่อนไหวได้ การผสมผสานระหว่างเอฟเฟกต์การเพิ่มขึ้นและการลดลงช่วยเน้นการยุบตัวที่ส่วนท้ายของภาพเคลื่อนไหว
รูปที่ 2 รูปแบบคลื่นนี้แสดงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์
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) Column( Modifier .clickable( onClick = { 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) } }
โยก (พร้อมหมุน)
หลักการเกี่ยวกับระบบการสั่นที่สำคัญอย่างหนึ่งคือการสร้างความพึงพอใจให้แก่ผู้ใช้ วิธีสนุกๆ ในการสร้างเอฟเฟกต์การสั่นที่น่าพึงพอใจและไม่คาดคิดคือการใช้ PRIMITIVE_SPIN พรีมิติฟนี้จะมีประสิทธิภาพสูงสุดเมื่อมีการเรียกใช้มากกว่า 1 ครั้ง การหมุนหลายๆ ครั้งต่อกันอาจทำให้เกิดเอฟเฟกต์การสั่นไหวและไม่เสถียร ซึ่งสามารถปรับปรุงเพิ่มเติมได้โดยใช้การปรับขนาดแบบสุ่มในแต่ละองค์ประกอบพื้นฐาน นอกจากนี้ คุณยังทดลองใช้ช่องว่างระหว่างองค์ประกอบพื้นฐานของการหมุนที่ต่อเนื่องกันได้ด้วย การหมุน 2 ครั้งโดยไม่มีช่องว่าง (0 มิลลิวินาทีระหว่างการหมุน) จะสร้างความรู้สึกหมุนที่แน่นหนา การเพิ่มช่องว่างระหว่างการหมุนจาก 10 เป็น 50 มิลลิวินาทีจะทำให้รู้สึกถึงการหมุนที่หลวมขึ้น และสามารถใช้เพื่อจับคู่ระยะเวลาของวิดีโอหรือภาพเคลื่อนไหวได้
อย่าใช้ช่องว่างที่นานกว่า 100 มิลลิวินาที เนื่องจากเสียงหมุนที่ต่อเนื่องกันจะไม่ผสานรวมกันได้ดีอีกต่อไป และจะเริ่มรู้สึกเหมือนเป็นเอฟเฟกต์เดี่ยวๆ
นี่คือตัวอย่างของรูปร่างที่ยืดหยุ่นซึ่งจะเด้งกลับหลังจากถูกลากลงแล้วปล่อย ภาพเคลื่อนไหวได้รับการปรับปรุงด้วยเอฟเฟกต์การหมุน 2 แบบ ซึ่งเล่นด้วยความหนักที่แตกต่างกันซึ่งเป็นสัดส่วนกับการกระจัดของการเด้ง
รูปที่ 3 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์
@Composable fun WobbleScreen() { // Control variables for the dragging and animating state of the elastic. var dragDistance by remember { mutableFloatStateOf(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 ), // The spring ends on its target. An end value of 0f means that the // shape is back at rest, so the wobble is over. finishedListener = { endValue -> if (endValue == 0f) isWobbling = false }, ) LaunchedEffect(isWobbling) { while (isWobbling) { 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 = { // Wobble only if the shape is far enough from rest to vibrate. // It then springs back to 0f, and the finishedListener ends // the wobble. val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE isWobbling = displacement > SPIN_MIN_DISPLACEMENT dragDistance = 0f }, orientation = Orientation.Vertical, state = rememberDraggableState { delta -> isWobbling = false dragDistance += delta } ) ) { // Draw the wobbling shape using the animated spring-like value. The // lambda defers the read to the draw phase, so the animation doesn't // recompose this screen on every frame. WobbleShape { dragDistanceAnimated } } } // Calculate a random scale for each spin to vary the full effect. fun nextSpinScale(displacement: Float): Float { // Generate a random offset in the range [-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) }
เด้ง (มีเสียงตุบ)
การใช้งานเอฟเฟกต์การสั่นขั้นสูงอีกอย่างหนึ่งคือการจำลองการโต้ตอบทางกายภาพ PRIMITIVE_THUD สามารถสร้างเอฟเฟกต์ที่หนักแน่นและก้องกังวาน ซึ่งสามารถจับคู่กับการแสดงภาพของแรงกระแทก เช่น ในวิดีโอหรือภาพเคลื่อนไหว เพื่อเพิ่มประสบการณ์โดยรวม
ต่อไปนี้คือตัวอย่างภาพเคลื่อนไหวลูกบอลตกที่ปรับปรุงด้วยเอฟเฟกต์เสียงตุบซึ่งจะเล่นทุกครั้งที่ลูกบอลกระเด้งออกจากด้านล่างของหน้าจอ
รูปที่ 4 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์
enum class BallPosition { Start, End } @Composable fun BounceScreen() { // Control variable for the state of the ball. var ballPosition by remember { mutableStateOf(BallPosition.Start) } // Plain holder for the bounce count. It's read only in the effect and in // the click handler, never in composition, so it needs no snapshot state. val bounceCount = remember { intArrayOf(0) } // Animation for the bouncing ball. val transitionData = updateTransitionData(ballPosition) val collisionData = updateCollisionData(transitionData) // Ball is about to contact floor. The effect runs only when the collision // state changes, so it vibrates once per collision. Nothing here suspends, // so a keyed SideEffect does the job without launching a coroutine. SideEffect(collisionData.collisionWithFloor) { if (collisionData.collisionWithFloor) { val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat() vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale ).compose() ) } } Box( Modifier .fillMaxSize() .clickable { if (transitionData.isAtStart) { ballPosition = BallPosition.End } else { ballPosition = BallPosition.Start bounceCount[0] = 0 } }, ) { // Build the ball UI based on the current state. BouncingBall(transitionData) } }
รูปคลื่นการสั่นพร้อมเอนเวโลป
กระบวนการสร้างรูปแบบการสั่นที่กำหนดเองช่วยให้คุณควบคุมแอมพลิจูดของการสั่นเพื่อสร้างเอฟเฟกต์ที่ราบรื่นของการเพิ่มและลดระดับได้ ส่วนนี้จะอธิบายวิธีสร้างเอฟเฟกต์การโต้ตอบการสัมผัสแบบไดนามิกโดยใช้ซองรูปแบบคลื่นที่ช่วยให้ควบคุมแอมพลิจูดและความถี่ของการสั่นได้อย่างแม่นยำเมื่อเวลาผ่านไป ซึ่งจะช่วยให้คุณสร้างประสบการณ์การโต้ตอบด้วยการสัมผัสที่สมบูรณ์และละเอียดยิ่งขึ้น
ตั้งแต่ Android 16 (API ระดับ 36) เป็นต้นไป ระบบจะมี API ต่อไปนี้เพื่อสร้างซองรูปคลื่นการสั่นโดยกำหนดลำดับของจุดควบคุม
BasicEnvelopeBuilder: วิธีการที่เข้าถึงได้ในการสร้างเอฟเฟกต์การโต้ตอบการสัมผัสที่ไม่ขึ้นอยู่กับฮาร์ดแวร์WaveformEnvelopeBuilder: วิธีการสร้างเอฟเฟกต์การสั่นที่ซับซ้อนมากขึ้น ซึ่งต้องอาศัยความคุ้นเคยกับฮาร์ดแวร์การสั่น
Android ไม่มีตัวเลือกสำรองสำหรับเอฟเฟกต์ซองจดหมาย หากต้องการการสนับสนุนนี้ ให้ทำตามขั้นตอนต่อไปนี้
- ตรวจสอบว่าอุปกรณ์ที่ระบุรองรับเอฟเฟกต์ซองจดหมายโดยใช้
Vibrator.areEnvelopeEffectsSupported()หรือไม่ - ปิดใช้ชุดประสบการณ์ที่สอดคล้องกันซึ่งไม่รองรับ หรือใช้รูปแบบการสั่นที่กำหนดเองหรือองค์ประกอบเป็นทางเลือกสำรอง
หากต้องการสร้างเอฟเฟกต์ซองจดหมายพื้นฐานเพิ่มเติม ให้ใช้ BasicEnvelopeBuilder กับพารามิเตอร์ต่อไปนี้
- ค่าความหนักในช่วง \( [0, 1] \)ซึ่งแสดงถึงความแรงของการสั่นที่รับรู้ได้ ตัวอย่างเช่น ค่า \( 0.5 \)จะถือเป็นครึ่งหนึ่งของความสว่างสูงสุดที่อุปกรณ์ทำได้
ค่าความคมชัดในช่วง \( [0, 1] \)ซึ่งแสดงถึงความชัดเจนของการสั่น ค่าที่ต่ำลงจะทำให้การสั่นราบรื่นขึ้น ส่วนค่าที่สูงขึ้นจะทำให้รู้สึกสั่นแรงขึ้น
ค่าระยะเวลา ซึ่งแสดงเวลาเป็นมิลลิวินาทีที่ใช้ในการเปลี่ยนจากจุดควบคุมสุดท้าย (นั่นคือคู่ความเข้มและความคมชัด) ไปยังจุดควบคุมใหม่
ตัวอย่างรูปแบบคลื่นที่เพิ่มความหนักจากการสั่นระดับต่ำไปเป็นระดับสูงและแรงที่สุดในระยะเวลา 500 มิลลิวินาที จากนั้นลดระดับลงจน\( 0 \) (ปิด) ในระยะเวลา 100 มิลลิวินาที
vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() .setInitialSharpness(0.0f) .addControlPoint(1.0f, 1.0f, 500) .addControlPoint(0.0f, 1.0f, 100) .build() )
หากคุณมีความรู้ขั้นสูงเกี่ยวกับระบบการสั่น คุณสามารถกำหนดเอฟเฟกต์เอนเวโลปโดยใช้ WaveformEnvelopeBuilder เมื่อใช้ออบเจ็กต์นี้ คุณจะเข้าถึงการแมปความถี่กับการเร่งเอาต์พุต (FOAM) ได้ผ่าน VibratorFrequencyProfile
- ค่าแอมพลิจูดในช่วง \( [0, 1] \)ซึ่งแสดงถึงความแรงของการสั่นสะเทือนที่ทำได้ที่ความถี่ที่กำหนด ตามที่กำหนดโดย FOAM ของอุปกรณ์ ตัวอย่างเช่น ค่า \( 0.5 \) จะสร้างความเร่งเอาต์พุตสูงสุดครึ่งหนึ่งที่สามารถทำได้ที่ความถี่ที่กำหนด
ค่าความถี่ที่ระบุเป็นเฮิรตซ์
ค่าระยะเวลา ซึ่งแสดงเวลาเป็นมิลลิวินาทีที่ใช้ในการเปลี่ยนจากจุดควบคุมสุดท้ายไปยังจุดควบคุมใหม่
โค้ดต่อไปนี้แสดงตัวอย่างรูปแบบคลื่นที่กำหนดเอฟเฟกต์การสั่น 400 มิลลิวินาที โดยเริ่มด้วยการเพิ่มแอมพลิจูด 50 ms จากปิดเป็นเต็มที่ที่ความถี่คงที่ 60 Hz จากนั้นความถี่จะเพิ่มขึ้นเป็น 120 Hz ในช่วง 100 ms ถัดไป และคงอยู่ที่ระดับนั้นเป็นเวลา 200 ms สุดท้าย แอมพลิจูดจะลดลงเหลือ \( 0 \)และความถี่จะกลับไปที่ 60 Hz ในช่วง 50 ms สุดท้าย
vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() .addControlPoint(1.0f, 60f, 50) .addControlPoint(1.0f, 120f, 100) .addControlPoint(1.0f, 120f, 200) .addControlPoint(0.0f, 60f, 50) .build() )
ส่วนต่อไปนี้แสดงตัวอย่างรูปคลื่นการสั่นสะเทือนที่มีเอนเวโลปหลายตัวอย่าง
สปริงเด้งดึ๋ง
ตัวอย่างก่อนหน้านี้ใช้ PRIMITIVE_THUD เพื่อจำลองการโต้ตอบการเด้งทางกายภาพ API เอนเวโลปพื้นฐานช่วยให้ควบคุมได้ละเอียดยิ่งขึ้นอย่างมาก ซึ่งช่วยให้คุณปรับแต่งความหนักและความคมชัดของการสั่นได้อย่างแม่นยำ
ซึ่งจะส่งผลให้การตอบสนองแบบรู้สึกได้เป็นไปตามเหตุการณ์ภาพเคลื่อนไหวอย่างแม่นยำมากขึ้น
ต่อไปนี้คือตัวอย่างของสปริงที่ตกลงมาอย่างอิสระ โดยมีการปรับปรุงภาพเคลื่อนไหวด้วยเอฟเฟกต์เอนเวโลปพื้นฐานที่เล่นทุกครั้งที่สปริงกระเด้งออกจากด้านล่างของหน้าจอ
รูปที่ 5 กราฟรูปคลื่นความเร่งเอาต์พุตสำหรับการสั่นที่จำลองสปริงที่เด้ง
@Composable fun BouncingSpringAnimation() { // The spring's height above the floor: 1f is the drop height, 0f is the floor. val springHeight = remember { Animatable(1f) } var isAnimating by remember { mutableStateOf(true) } LaunchedEffect(isAnimating) { if (!isAnimating) return@LaunchedEffect springHeight.snapTo(1f) // Each bounce reaches a fraction of the previous height. var bounceHeight = 1f while (bounceHeight > MIN_BOUNCE_HEIGHT) { // Fall to the floor, speeding up like a falling object. A fall from // a greater height takes longer. val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad)) // The spring hits the floor: play a "boing" envelope effect that fades // out. A higher bounce gives a stronger and sharper vibration. vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() // Starting from zero sharpness here, will simulate a smoother // "boing" effect. .setInitialSharpness(0f) // Add a control point to reach the target intensity and // sharpness very quickly. .addControlPoint(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L) // Add a control point to fade out the vibration intensity while // maintaining sharpness. .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong()) .build() ) // Bounce back up to a lower height, slowing down like a thrown object. // When the next bounce is too small, the spring stays on the floor. bounceHeight *= BOUNCE_DAMPING if (bounceHeight > MIN_BOUNCE_HEIGHT) { val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad)) } } isAnimating = false } Box( modifier = Modifier .fillMaxSize() .clickable(enabled = !isAnimating) { isAnimating = true } .drawBehind { // Read the height in the draw phase, and scale it to the distance // between the top of the box and the floor. val floorTop = size.height - FLOOR_HEIGHT.toPx() val dropDistance = floorTop - SPRING_HEIGHT.toPx() drawSpring(topY = dropDistance * (1f - springHeight.value)) drawFloor(top = floorTop) } ) { if (!isAnimating) { Text("Tap to restart", Modifier.align(Alignment.Center)) } } }
การปล่อยจรวด
ตัวอย่างก่อนหน้านี้แสดงวิธีใช้ Envelope API พื้นฐานเพื่อจำลองปฏิกิริยาของสปริงที่เด้งได้ WaveformEnvelopeBuilder ช่วยให้ควบคุมช่วงความถี่ทั้งหมดของอุปกรณ์ได้อย่างแม่นยำ ทำให้สามารถปรับแต่งเอฟเฟกต์การสัมผัสได้ตามต้องการ เมื่อรวมข้อมูลนี้เข้ากับข้อมูล FOAM คุณจะปรับแต่งการสั่นให้เหมาะกับความสามารถด้านความถี่ที่เฉพาะเจาะจงได้
ตัวอย่างต่อไปนี้แสดงการจำลองการปล่อยจรวดโดยใช้รูปแบบการสั่นแบบไดนามิก เอฟเฟกต์จะเปลี่ยนจากเอาต์พุตการเร่งความถี่ขั้นต่ำที่รองรับ 0.1 G เป็นความถี่เรโซแนนซ์ โดยรักษาอินพุตแอมพลิจูด 10% ไว้เสมอ ซึ่งจะช่วยให้เอฟเฟกต์เริ่มต้นด้วยเอาต์พุตที่แรงพอสมควรและเพิ่มความหนักและความคมชัดที่รับรู้ได้ แม้ว่าแอมพลิจูดที่ขับเคลื่อนจะเท่ากันก็ตาม เมื่อถึงจุดเรโซแนนซ์ ความถี่ของเอฟเฟกต์จะลดลงกลับไปที่ค่าต่ำสุด ซึ่งจะรับรู้ได้ว่าเป็นความหนักและความคมชัดที่ลดลง ซึ่งจะสร้างความรู้สึกถึงแรงต้านในตอนแรกตามด้วยการปล่อยตัว ซึ่งเลียนแบบการปล่อยยานสู่อวกาศ
เอฟเฟกต์นี้ไม่สามารถทำได้ด้วย API เอนเวโลปพื้นฐาน เนื่องจากจะแยกข้อมูลเฉพาะอุปกรณ์เกี่ยวกับความถี่เรโซแนนซ์และเส้นโค้งความเร่งเอาต์พุตออกไป การเพิ่มความคมชัดอาจทำให้ความถี่ที่เทียบเท่าสูงกว่าความถี่เรโซแนนซ์ ซึ่งอาจทำให้เกิดการเร่งความเร็วที่ลดลงโดยไม่ตั้งใจ
รูปที่ 6 กราฟรูปคลื่นความเร่งเอาต์พุตสำหรับการสั่นที่จำลองการปล่อยจรวด
@Composable fun RocketLaunchAnimation() { var isLaunched by remember { mutableStateOf(false) } val animation = remember { Animatable(0f) } val animationDuration = 3000 LaunchedEffect(isLaunched) { if (isLaunched) { animation.animateTo( 1.2f, // Overshoot so that the rocket goes off the screen. animationSpec = tween( durationMillis = animationDuration, // Applies an easing curve with a slow start and rapid acceleration // towards the end. easing = CubicBezierEasing(1f, 0f, 0.75f, 1f) ) ) animation.snapTo(0f) isLaunched = false } } Canvas( modifier = Modifier .fillMaxSize() .clickable { if (!isLaunched) { // Play vibration with same duration as the animation, using 70% of // the time for the rise of the vibration, to match the easing curve // defined previously. playVibration(vibrator, animationDuration.toLong(), 0.7f) isLaunched = true } } .background(colorResource(R.color.background)) ) { // Read the animation in the draw phase, and scale it to the canvas height. drawRocket(size.height * animation.value) } } private fun playVibration( vibrator: Vibrator, totalDurationMs: Long, riseBias: Float, minOutputAccelerationGs: Float = 0.1f, ) { require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." } if (!vibrator.areEnvelopeEffectsSupported()) { return } val resonantFrequency = vibrator.resonantFrequency if (resonantFrequency.isNaN()) { // Device doesn't have or expose a resonant frequency. return } val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return if (startFrequency >= resonantFrequency) { // Vibrator can't generate the minimum required output at lower frequencies. return } val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs val rampDownDurationMs = totalDurationMs - rampUpDurationMs - minDurationMs vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() // Quickly reach the target output at the start frequency .addControlPoint(0.1f, startFrequency, minDurationMs) .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs) .addControlPoint(0.1f, startFrequency, rampDownDurationMs) // Controlled ramp down to zero to avoid ringing after the vibration. .addControlPoint(0.0f, startFrequency, minDurationMs) .build() ) }
LavaBeats
เช่นเดียวกับตัวอย่างการปล่อยจรวด API WaveformEnvelopeBuilder ช่วยให้สามารถออกแบบเอฟเฟกต์การสัมผัสที่ซับซ้อนได้หลายแบบโดยเปิดใช้การควบคุมแอมพลิจูดและส่วนความถี่ในการสั่น อีกตัวอย่างหนึ่งของการออกแบบดังกล่าวคือการจำลองความรู้สึกทางกายภาพที่เป็นนามธรรมมากขึ้น เช่น "ความมีชีวิตชีวา"
ซึ่งทำได้โดยการแสดงไบโอมาร์กเกอร์ของสัญญาณคลื่นไฟฟ้าหัวใจ (ECG) ทั่วไปที่มีส่วนการสั่นสะเทือนของแอมพลิจูดและความถี่บางอย่าง LavaBeats เป็นตัวอย่างที่แสดงลักษณะเฉพาะ 2 ส่วนของการบันทึก ECG เป็นพัลส์ 2 ครั้งที่คั่นด้วยการหน่วงเวลา พัลส์ลักษณะเฉพาะแรกคือ QRS Complex ซึ่งแสดงเป็นยอดแหลมที่มีแอมพลิจูดสูงและระยะเวลาสั้น พัลส์ที่ 2 คือคลื่น T ซึ่งมีแอมพลิจูดต่ำกว่า ระยะเวลานานกว่า และรูปร่างที่ราบรื่นกว่า (ดูรูปที่ 7)
ใช้ WaveformEnvelopeBuilder เพื่อสร้างการทำซ้ำต่างๆ ของพัลส์ 2 รายการนี้ซึ่งคั่นด้วยความล่าช้าของพัลส์แรกถึงพัลส์ที่ 2 ที่คงที่ พัลส์แรกอาจเป็นสัญญาณเสียงร้องที่เริ่มต้นที่ความถี่ต่ำและสิ้นสุดที่ความถี่สูงกว่าในระยะเวลาสั้นๆ พัลส์ที่ 2 สามารถแสดงเป็นคาบเดียวของไซนัสซอยด์ความถี่ต่ำ เราสามารถรวมพัลส์ 2 รายการเข้าเป็นจังหวะเดียว และทำซ้ำการรวมดังกล่าวหลายครั้งโดยมีระยะหน่วงระหว่างพัลส์ตามอัตราจังหวะต่อนาที (BPM) ทั่วไป ผลลัพธ์ที่ได้คือเอฟเฟกต์การโต้ตอบการสัมผัสที่คล้ายกับการเต้นของหัวใจ
คุณสามารถลองใช้ LavaBeats ในแอปตัวอย่างระบบการสั่นบน GitHub และสัมผัสเอฟเฟกต์ที่มาพร้อมกับการแสดงภาพโคมไฟลาวาที่เต้นเป็นจังหวะเดียวกับเอฟเฟกต์การสั่น นอกจากนี้ คุณยังเปลี่ยนการตั้งค่าของเอฟเฟกต์เพื่อสร้างความรู้สึกการเต้นที่แตกต่างกันได้โดยการปรับเปลี่ยนแอมพลิจูด ความถี่ ระยะเวลา และความล่าช้าของพัลส์ทั้ง 2 รายการ
รูปที่ 7 ส่วนหนึ่งของบันทึก ECG ที่มี QRS Complex และคลื่น T
private fun createEnvelopeEffect( beatParameters: List<BeatParameter> ): VibrationEffect = VibrationEffect.WaveformEnvelopeBuilder() .apply { repeat(beatParameters.getNumBeats()) { // First pulse chirp addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseStartFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstPulseDurationMillis().toLong(), ) addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) // Delay between first and second pulse addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstToSecondPulseDelayMillis().toLong(), ) // Second pulse addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(), ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), beatParameters.getBeatDelayMillis().toLong(), ) } } .build() /** A parameter of a haptic beat effect that represents an ECG signal parameter */ data class BeatParameter( val description: String = "", val value: Float = 0f, val range: ClosedFloatingPointRange<Float> = 0f..1f, val steps: Int = 0, val isFrequencyType: Boolean = false, )