หน้านี้แสดงตัวอย่างวิธีใช้ Haptics API ต่างๆ เพื่อสร้างเอฟเฟกต์ที่กำหนดเองนอกเหนือจากรูปแบบการสั่นมาตรฐานในแอป Android
หน้านี้มีตัวอย่างต่อไปนี้
- รูปแบบการสั่นที่กำหนดเอง
- รูปแบบการเพิ่ม: รูปแบบที่เริ่มต้นอย่างราบรื่น
- รูปแบบที่ซ้ำกัน: รูปแบบที่ไม่มีที่สิ้นสุด
- รูปแบบที่มีเส้นทางที่แสดงสำรอง: การสาธิตเส้นทางที่แสดงสำรอง
- องค์ประกอบการสั่น
- รูปแบบคลื่นการสั่นพร้อมรูปคลื่น
- สปริงเด้ง: เอฟเฟกต์การเด้งของสปริงโดยใช้เอฟเฟกต์อีนเวลอปพื้นฐาน
- การปล่อยจรวด: เอฟเฟกต์การปล่อยจรวดโดยใช้เอฟเฟกต์รูปคลื่น
ดูตัวอย่างเพิ่มเติมได้ที่เพิ่มการตอบกลับด้วยระบบสัมผัสให้กับเหตุการณ์ และปฏิบัติตามหลักการการออกแบบการสัมผัสเสมอ
ใช้เนื้อหาสำรองเพื่อจัดการความเข้ากันได้ของอุปกรณ์
เมื่อใช้เอฟเฟกต์ที่กำหนดเอง ให้พิจารณาสิ่งต่อไปนี้
- ความสามารถของอุปกรณ์ที่จำเป็นสำหรับเอฟเฟกต์
- สิ่งที่ต้องทำเมื่ออุปกรณ์เล่นเอฟเฟกต์ไม่ได้
ข้อมูลอ้างอิง Android Haptics API มีรายละเอียดเกี่ยวกับวิธีตรวจสอบการรองรับคอมโพเนนต์ที่เกี่ยวข้องกับการสัมผัสเพื่อให้แอปมอบประสบการณ์โดยรวมที่สอดคล้องกัน
คุณอาจต้องการปิดใช้เอฟเฟกต์ที่กำหนดเองหรือระบุเอฟเฟกต์ที่กำหนดเองทางเลือกโดยขึ้นอยู่กับกรณีการใช้งานของคุณ
วางแผนสำหรับความสามารถระดับสูงต่อไปนี้ของอุปกรณ์
หากคุณใช้องค์ประกอบพื้นฐานของการสัมผัส: อุปกรณ์ที่รองรับองค์ประกอบพื้นฐานเหล่านั้นซึ่งเอฟเฟกต์ที่กำหนดเองต้องใช้ (ดูรายละเอียดเกี่ยวกับพรอมิเตอได้ที่ส่วนถัดไป)
อุปกรณ์ที่มีการควบคุมแอมพลิจูด
อุปกรณ์ที่รองรับการสั่นขั้นพื้นฐาน (เปิด/ปิด) กล่าวคือ อุปกรณ์ที่ไม่มีการควบคุมแอมพลิจูด
หากตัวเลือกเอฟเฟกต์การสัมผัสของแอปของคุณคำนึงถึงหมวดหมู่เหล่านี้ ประสบการณ์การสัมผัสของผู้ใช้ก็ควรจะคาดการณ์ได้สำหรับอุปกรณ์แต่ละเครื่อง
การใช้องค์ประกอบพื้นฐานของการโต้ตอบการสัมผัส
Android มีองค์ประกอบพื้นฐานของการสัมผัสหลายรายการที่แตกต่างกันทั้งในด้านแอมพลิจูดและความถี่ คุณอาจใช้องค์ประกอบพื้นฐานรายการเดียวหรือหลายรายการร่วมกันเพื่อให้ได้เอฟเฟกต์การสัมผัสที่สมจริง
- ใช้การหน่วงเวลา 50 ms ขึ้นไปสำหรับช่องว่างที่สังเกตได้ระหว่างพรอมิตี 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 // Don't 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; // Don't 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);
ตัวอย่างองค์ประกอบการสั่น
ส่วนต่อไปนี้แสดงตัวอย่างการประกอบการสั่นหลายรายการ ซึ่งนำมาจากแอปตัวอย่างการสัมผัสใน GitHub
ต้านทาน (การคลิกต่ำ)
คุณควบคุมความกว้างของคลื่นการสั่นพื้นฐานเพื่อสื่อความคิดเห็นที่เป็นประโยชน์เกี่ยวกับการดำเนินการที่กำลังดำเนินอยู่ได้ คุณสามารถใช้ค่าสเกลที่เว้นระยะห่างกันไม่มากเพื่อสร้างเอฟเฟกต์การค่อยๆ ดังขึ้นอย่างราบรื่นของรูปเรขาคณิตพื้นฐาน นอกจากนี้ คุณยังตั้งค่าเวลาหน่วงระหว่างพรอมต์ติดต่อกันแบบไดนามิกตามการโต้ตอบของผู้ใช้ได้ด้วย ตัวอย่างต่อไปนี้แสดงภาพเคลื่อนไหวของมุมมองที่ควบคุมด้วยการลากและเสริมด้วยระบบสัมผัส

รูปที่ 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_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
Primitive นี้จะมีประสิทธิภาพสูงสุดเมื่อเรียกใช้มากกว่า 1 ครั้ง การต่อภาพการหมุนหลายภาพเข้าด้วยกันอาจสร้างเอฟเฟกต์ที่โยกเยกและไม่เสถียร ซึ่งสามารถปรับปรุงให้ดียิ่งขึ้นได้โดยใช้การปรับขนาดแบบสุ่มกับรูปเรขาคณิตพื้นฐานแต่ละรูป นอกจากนี้ คุณยังทดลองใช้ระยะห่างระหว่างพรอมต์การหมุนต่อเนื่องได้ด้วย การสปิน 2 ครั้งโดยไม่มีช่องว่าง (เว้นช่วง 0 มิลลิวินาที) จะทำให้เกิดความรู้สึกว่าภาพสั่น การเพิ่มระยะห่างระหว่างการหมุนจาก 10 เป็น 50 มิลลิวินาทีจะทำให้การหมุนดูหลวมขึ้น และสามารถใช้เพื่อจับคู่กับระยะเวลาของวิดีโอหรือภาพเคลื่อนไหว
อย่าใช้การเว้นระยะเวลานานกว่า 100 ms เนื่องจากภาพสั่นต่อเนื่องจะผสานกันไม่ดีและเริ่มดูเหมือนเอฟเฟกต์แต่ละรายการ
ต่อไปนี้คือตัวอย่างของรูปร่างยืดหยุ่นที่เด้งกลับหลังจากลากลงแล้วปล่อย ภาพเคลื่อนไหวได้รับการปรับปรุงด้วยเอฟเฟกต์การหมุน 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 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)
}
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 the range [-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;
}
}
});
}
}
รูปแบบการสั่นพร้อมด้วยอีเวนต์
กระบวนการสร้างรูปแบบการสั่นที่กำหนดเองช่วยให้คุณควบคุมแอมพลิจูดการสั่นเพื่อสร้างเอฟเฟกต์ที่ราบรื่นของการค่อยๆ เพิ่มและลดการสั่น ส่วนนี้จะอธิบายวิธีสร้างเอฟเฟกต์การสัมผัสแบบไดนามิกโดยใช้รูปคลื่นของอีนVELOP ซึ่งช่วยให้ควบคุมความกว้างและความถี่ของการสั่นได้อย่างแม่นยำเมื่อเวลาผ่านไป ซึ่งจะช่วยให้คุณสร้างประสบการณ์การสัมผัสที่สมบูรณ์และละเอียดยิ่งขึ้น
ตั้งแต่ Android 16 (API ระดับ 36) เป็นต้นไป ระบบจะมี API ต่อไปนี้เพื่อสร้างรูปคลื่นการสั่นโดยกำหนดลําดับของจุดควบคุม
BasicEnvelopeBuilder
: แนวทางที่เข้าถึงได้ในการสร้างเอฟเฟกต์การสัมผัสที่ไม่ขึ้นอยู่กับฮาร์ดแวร์WaveformEnvelopeBuilder
: แนวทางขั้นสูงในการสร้างเอฟเฟกต์การสัมผัส ต้องใช้ความคุ้นเคยกับฮาร์ดแวร์การสัมผัส
Android ไม่มีการแสดงผลสำรองสำหรับเอฟเฟกต์ของโฟลเดอร์ หากต้องการการสนับสนุนนี้ ให้ทำตามขั้นตอนต่อไปนี้
- ตรวจสอบว่าอุปกรณ์รองรับเอฟเฟกต์เสียงห่อหุ้มโดยใช้
Vibrator.areEnvelopeEffectsSupported()
- ปิดใช้ชุดประสบการณ์การใช้งานที่สอดคล้องกันซึ่งระบบไม่รองรับ หรือใช้รูปแบบการสั่นที่กำหนดเองหรือการผสมเป็นทางเลือกสำรอง
หากต้องการสร้างเอฟเฟกต์อีนVELOP พื้นฐานมากขึ้น ให้ใช้ 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 ms โดยเริ่มจากการเพิ่มระดับแอมพลิจูดจาก 0 เป็น 100% ในช่วง 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
เพื่อจําลองการโต้ตอบจากการตีกลับ Envelope API พื้นฐานให้การควบคุมที่ละเอียดยิ่งขึ้นอย่างมาก ซึ่งช่วยให้คุณปรับความแรงและความคมชัดของการสั่นได้อย่างแม่นยำ
ส่งผลให้การตอบสนองด้วยการสัมผัสเป็นไปตามเหตุการณ์ภาพเคลื่อนไหวอย่างแม่นยำมากขึ้น
ต่อไปนี้คือตัวอย่างสปริงที่ตกลงมาโดยอิสระพร้อมภาพเคลื่อนไหวที่ปรับปรุงด้วยเอฟเฟกต์อีนเวลอปพื้นฐานที่เล่นทุกครั้งที่สปริงเด้งออกจากด้านล่างของหน้าจอ
@Composable
fun BouncingSpringAnimation() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
vibrator.vibrate(
VibrationEffect.BasicEnvelopeBuilder()
// Starting from zero sharpness here, will simulate a smoother
// "boing" effect.
.setInitialSharpness(0f)
// Add a control point to reach the desired intensity and
// sharpness very quickly.
.addControlPoint(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
การปล่อยจรวด
ตัวอย่างก่อนหน้านี้แสดงวิธีใช้ Envelope API พื้นฐานเพื่อจำลองการตอบสนองของสปริงที่เด้ง WaveformEnvelopeBuilder
ช่วยให้คุณควบคุมช่วงความถี่ทั้งหมดของอุปกรณ์ได้อย่างแม่นยำ ซึ่งช่วยให้คุณปรับแต่งเอฟเฟกต์การสัมผัสได้อย่างมาก เมื่อรวมข้อมูลนี้เข้ากับข้อมูล FOAM คุณจะปรับแต่งการสั่นสะเทือนให้เหมาะกับความสามารถของย่านความถี่ที่เฉพาะเจาะจงได้
ต่อไปนี้เป็นตัวอย่างการจำลองการปล่อยจรวดโดยใช้รูปแบบการสั่นแบบไดนามิก ผลจะเริ่มจากความถี่ที่รองรับต่ำสุดซึ่งก็คือเอาต์พุตความเร่ง 0.1 G ไปจนถึงความถี่เรโซแนนซ์ โดยรักษาอินพุตแอมพลิจูดไว้ที่ 10% เสมอ วิธีนี้ช่วยให้เอฟเฟกต์เริ่มต้นด้วยเอาต์พุตที่ค่อนข้างแรงและเพิ่มความเข้มและความคมชัดที่รับรู้ แม้ว่าแอมพลิจูดในการขับเคลื่อนจะเท่าเดิม เมื่อถึงจุดเรโซแนนซ์ ความถี่ของเอฟเฟกต์จะลดลงกลับไปยังค่าต่ำสุด ซึ่งจะรับรู้ได้ว่าความเข้มและความคมชัดลดลง ซึ่งจะทำให้เกิดความรู้สึกถึงการต้านทานครั้งแรกตามด้วยการปล่อยตัว คล้ายกับการปล่อยจรวดขึ้นสู่อวกาศ
เอฟเฟกต์นี้ใช้ไม่ได้กับ Envelope API พื้นฐาน เนื่องจากจะแยกข้อมูลเฉพาะอุปกรณ์เกี่ยวกับความถี่เรโซแนนซ์และเส้นโค้งความเร่งเอาต์พุตออก การเพิ่มความคมอาจทำให้ความถี่ที่เทียบเท่าสูงกว่าระดับการสั่นพ้อง ซึ่งอาจทำให้การเร่งความเร็วลดลงโดยไม่ตั้งใจ
@Composable
fun RocketLaunchAnimation() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
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)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
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, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
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 - rampUpDuration - minDurationMs
vibrator.vibrate(
VibrationEffect.WaveformEnvelopeBuilder()
// Quickly reach the desired 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()
)
}