สร้างเอฟเฟกต์การโต้ตอบการสัมผัสที่กำหนดเอง

หน้านี้ครอบคลุมตัวอย่างวิธีใช้ Haptics API ต่างๆ เพื่อสร้างเอฟเฟกต์ที่กำหนดเองนอกเหนือจากรูปแบบคลื่นการสั่นสะเทือนมาตรฐานในแอป Android

หน้านี้มีตัวอย่างต่อไปนี้

ดูตัวอย่างเพิ่มเติมได้ที่เพิ่มการตอบสนองแบบสั่นให้กับเหตุการณ์ และปฏิบัติตามหลักการออกแบบการตอบสนองแบบสั่นเสมอ

ใช้การสำรองเพื่อจัดการความเข้ากันได้ของอุปกรณ์

เมื่อติดตั้งใช้งานเอฟเฟกต์ที่กำหนดเอง โปรดคำนึงถึงสิ่งต่อไปนี้

  • ความสามารถของอุปกรณ์ที่จำเป็นสำหรับเอฟเฟกต์
  • สิ่งที่ต้องทำเมื่ออุปกรณ์เล่นเอฟเฟกต์ไม่ได้

ข้อมูลอ้างอิง API การสัมผัสของ Android มีรายละเอียดเกี่ยวกับวิธีตรวจสอบการรองรับส่วนประกอบที่เกี่ยวข้องกับการสัมผัส เพื่อให้แอปมอบประสบการณ์โดยรวมที่สอดคล้องกันได้

คุณอาจต้องการปิดใช้เอฟเฟกต์ที่กำหนดเองหรือ จัดหาเอฟเฟกต์ที่กำหนดเองทางเลือกตามความสามารถที่อาจแตกต่างกัน ทั้งนี้ขึ้นอยู่กับกรณีการใช้งาน

วางแผนสำหรับความสามารถของอุปกรณ์ในระดับสูงต่อไปนี้

  • หากคุณใช้ Primitive แบบสัมผัส: อุปกรณ์ที่รองรับ Primitive เหล่านั้น ซึ่งเอฟเฟกต์ที่กำหนดเองต้องใช้ (ดูรายละเอียดเกี่ยวกับ Primitive ได้ในส่วนถัดไป)

  • อุปกรณ์ที่มีการควบคุมแอมพลิจูด

  • อุปกรณ์ที่รองรับการสั่นพื้นฐาน (เปิด/ปิด) กล่าวคือ อุปกรณ์ที่ ไม่มีการควบคุมแอมพลิจูด

หากตัวเลือกเอฟเฟกต์การสัมผัสของแอปคำนึงถึงหมวดหมู่เหล่านี้ ประสบการณ์การใช้งานแบบสัมผัสของผู้ใช้ควรยังคงคาดการณ์ได้สำหรับอุปกรณ์แต่ละเครื่อง

การใช้องค์ประกอบพื้นฐานของการโต้ตอบการสัมผัส

Android มีองค์ประกอบพื้นฐานการสั่นหลายอย่างที่มีทั้งแอมพลิจูดและความถี่ที่แตกต่างกัน คุณอาจใช้รูปแบบพื้นฐานเพียงอย่างเดียวหรือใช้หลายรูปแบบร่วมกัน เพื่อให้ได้เอฟเฟกต์การสัมผัสที่สมบูรณ์

  • ใช้การหน่วงเวลา 50 มิลลิวินาทีขึ้นไปสำหรับช่องว่างที่สังเกตได้ระหว่างองค์ประกอบพื้นฐาน 2 รายการ และพิจารณาระยะเวลาขององค์ประกอบพื้นฐานด้วยหากเป็นไปได้
  • ใช้สเกลที่มีอัตราส่วนต่างกันตั้งแต่ 1.4 ขึ้นไปเพื่อให้เห็นความแตกต่างของ ความเข้มได้ชัดเจนยิ่งขึ้น
  • ใช้สเกล 0.5, 0.7 และ 1.0 เพื่อสร้างเวอร์ชันที่มีความเข้มต่ำ ปานกลาง และสูง ของรูปทรงเรขาคณิต

สร้างรูปแบบการสั่นที่กำหนดเอง

โดยทั่วไปแล้ว รูปแบบการสั่นมักใช้ในการโต้ตอบการสัมผัสที่กระตุ้นความสนใจ เช่น การแจ้งเตือน และเสียงเรียกเข้า บริการ Vibrator สามารถเล่นรูปแบบการสั่นที่ยาวนาน ซึ่งจะเปลี่ยนแอมพลิจูดการสั่นเมื่อเวลาผ่านไป เอฟเฟกต์ดังกล่าวเรียกว่า รูปคลื่น

โดยปกติแล้ว ผู้ใช้จะรับรู้ถึงเอฟเฟกต์รูปคลื่นได้ แต่การสั่นยาวๆ อย่างกะทันหันอาจทำให้ผู้ใช้ตกใจได้หากเล่นในสภาพแวดล้อมที่เงียบ การเพิ่มแอมพลิจูดเป้าหมายเร็วเกินไป อาจทำให้เกิดเสียงหึ่งที่ได้ยินด้วย ออกแบบรูปแบบรูปคลื่นเพื่อ เปลี่ยนผ่านแอมพลิจูดอย่างราบรื่นเพื่อสร้างเอฟเฟกต์เพิ่มและลดระดับ

ตัวอย่างรูปแบบการสั่น

ส่วนต่อไปนี้แสดงตัวอย่างรูปแบบการสั่นหลายแบบ

รูปแบบการเพิ่มจำนวน

โดยรูปคลื่นจะแสดงเป็น VibrationEffect พร้อมพารามิเตอร์ 3 รายการ ดังนี้

  1. Timings: อาร์เรย์ของระยะเวลาเป็นมิลลิวินาทีสำหรับแต่ละส่วนของรูปคลื่น
  2. แอมพลิจูด: แอมพลิจูดการสั่นที่ต้องการสำหรับระยะเวลาแต่ละรายการที่ระบุ ในอาร์กิวเมนต์แรก ซึ่งแสดงด้วยค่าจำนวนเต็มตั้งแต่ 0 ถึง 255 โดย 0 หมายถึง "สถานะปิด" ของไวเบรเตอร์ และ 255 คือแอมพลิจูดสูงสุดของอุปกรณ์
  3. ดัชนีการทำซ้ำ: ดัชนีในอาร์เรย์ที่ระบุในอาร์กิวเมนต์แรกเพื่อ เริ่มทำซ้ำรูปแบบคลื่น หรือ -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));
}

สร้างองค์ประกอบการสั่น

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

กระบวนการสร้างรูปแบบการสั่นที่กำหนดเองที่อธิบายไว้ก่อนหน้านี้ในหน้านี้จะอธิบายวิธีควบคุมแอมพลิจูดการสั่นเพื่อสร้างเอฟเฟกต์การเพิ่มและลดระดับที่ราบรื่น การสั่นที่สมบูรณ์ช่วยปรับปรุงแนวคิดนี้โดย สำรวจช่วงความถี่ที่กว้างขึ้นของมอเตอร์สั่นของอุปกรณ์เพื่อทำให้เอฟเฟกต์ นุ่มนวลยิ่งขึ้น โดยรูปคลื่นเหล่านี้มีประสิทธิภาพเป็นพิเศษในการสร้างเอฟเฟกต์เครสเชนโด หรือดีมินูเอนโด

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

Android ไม่มีฟอลแบ็กสำหรับองค์ประกอบที่มีองค์ประกอบพื้นฐานที่ไม่รองรับ ดังนั้น ให้ทำตามขั้นตอนต่อไปนี้

  1. ก่อนเปิดใช้งานการสั่นขั้นสูง โปรดตรวจสอบว่าอุปกรณ์ที่ต้องการรองรับ องค์ประกอบพื้นฐานทั้งหมดที่คุณใช้

  2. ปิดใช้ชุดประสบการณ์การใช้งานที่สอดคล้องกันซึ่งไม่รองรับ ไม่ใช่แค่เอฟเฟกต์ที่ไม่มีองค์ประกอบพื้นฐาน

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีตรวจสอบการรองรับของอุปกรณ์ได้ในส่วนต่อไปนี้

สร้างเอฟเฟกต์การสั่นที่ประกอบขึ้น

คุณสร้างเอฟเฟกต์การสั่นที่ประกอบขึ้นได้ด้วย 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 เพื่อสร้างเวอร์ชันที่มีความเข้มต่ำ ปานกลาง และสูงของ Primitive

เพิ่มช่องว่างระหว่างองค์ประกอบการสั่น

นอกจากนี้ องค์ประกอบยังระบุการหน่วงเวลาที่จะเพิ่มระหว่างองค์ประกอบพื้นฐานที่ต่อเนื่องกันได้ด้วย ความล่าช้านี้แสดงเป็นมิลลิวินาทีตั้งแต่สิ้นสุด องค์ประกอบพื้นฐานก่อนหน้า โดยทั่วไปแล้ว ช่องว่าง 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());

ตรวจสอบว่ารองรับ Primitive ใดบ้าง

คุณใช้ API ต่อไปนี้เพื่อยืนยันการรองรับอุปกรณ์สำหรับ Primitive ที่เฉพาะเจาะจงได้

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);
    }
}

ขยาย (พร้อมขึ้นและลง)

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

รูปแบบคลื่นการสั่นที่มีซองจดหมาย

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

ตั้งแต่ Android 16 (API ระดับ 36) เป็นต้นไป ระบบจะจัดเตรียม API ต่อไปนี้เพื่อสร้างซองจดหมายรูปคลื่นการสั่นโดยกำหนดลำดับของจุดควบคุม

  • BasicEnvelopeBuilder: วิธีที่เข้าถึงได้ในการสร้าง เอฟเฟกต์การสัมผัสที่ใช้ได้กับฮาร์ดแวร์ทุกประเภท
  • WaveformEnvelopeBuilder: วิธีที่ซับซ้อนกว่าในการสร้าง เอฟเฟกต์การสัมผัส ต้องมีความคุ้นเคยกับฮาร์ดแวร์การสัมผัส

Android ไม่มีฟอลแบ็กสำหรับเอฟเฟกต์ซองจดหมาย หากต้องการรับการสนับสนุนนี้ ให้ทำตามขั้นตอนต่อไปนี้

  1. ตรวจสอบว่าอุปกรณ์รองรับเอฟเฟกต์ซองจดหมายหรือไม่โดยใช้ Vibrator.areEnvelopeEffectsSupported()
  2. ปิดใช้ชุดประสบการณ์การใช้งานที่สอดคล้องกันซึ่งไม่รองรับ หรือใช้รูปแบบการสั่นที่กำหนดเองหรือองค์ประกอบเป็นทางเลือกสำรอง

หากต้องการสร้างเอฟเฟกต์ซองจดหมายพื้นฐานเพิ่มเติม ให้ใช้ 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] \)ซึ่งแสดงถึง ความแรงของการสั่นที่ทำได้ที่ความถี่ที่กำหนด ตามที่กำหนดโดย โฟมของอุปกรณ์ เช่น ค่า \( 0.5 \) จะสร้างการเร่งเอาต์พุตครึ่งหนึ่งของค่าสูงสุด ที่ทำได้ที่ความถี่ที่กำหนด
  • ค่าความถี่ที่ระบุในหน่วยเฮิรตซ์

  • ค่าระยะเวลา ซึ่งแสดงถึงเวลาเป็นมิลลิวินาทีที่ใช้ในการ เปลี่ยนจากจุดควบคุมสุดท้ายไปยังจุดควบคุมใหม่

โค้ดต่อไปนี้แสดงรูปคลื่นตัวอย่างที่กำหนดเอฟเฟกต์การสั่น 400 มิลลิวินาที โดยจะเริ่มด้วยการเพิ่มแอมพลิจูด 50 มิลลิวินาทีจากปิดเป็นเต็มที่ความถี่คงที่ 60 เฮิร์ตซ์ จากนั้นความถี่จะเพิ่มขึ้นเป็น 120 เฮิร์ตซ์ในช่วง 100 มิลลิวินาทีถัดไปและคงอยู่ที่ระดับนั้นเป็นเวลา 200 มิลลิวินาที สุดท้ายแอมพลิจูดจะลดลงเป็น \( 0 \)และความถี่จะกลับไปเป็น 60 เฮิร์ตซ์ในช่วง 50 มิลลิวินาทีสุดท้าย ดังนี้

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() {
  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")
    }
  }
}

การปล่อยจรวด

ตัวอย่างก่อนหน้าแสดงวิธีใช้ API ซองจดหมายพื้นฐานเพื่อ จำลองปฏิกิริยาของสปริงที่เด้ง WaveformEnvelopeBuilder ช่วยให้ควบคุมช่วงความถี่ทั้งหมดของอุปกรณ์ได้อย่างแม่นยำ จึงสร้างเอฟเฟกต์การสัมผัสที่ปรับแต่งได้อย่างละเอียด การรวมข้อมูลนี้กับข้อมูล FOAM จะช่วยให้คุณปรับแต่ง การสั่นให้ตรงกับความสามารถด้านความถี่ที่เฉพาะเจาะจงได้

ตัวอย่างต่อไปนี้แสดงการจำลองการปล่อยจรวดโดยใช้รูปแบบการสั่นแบบไดนามิก โดยเอฟเฟกต์จะเริ่มจากความถี่ต่ำสุดที่รองรับ เอาต์พุตการเร่งความเร็ว 0.1 G ไปจนถึงความถี่เรโซแนนซ์ โดยจะรักษา อินพุตแอมพลิจูด 10% ไว้เสมอ ซึ่งจะช่วยให้เอฟเฟกต์เริ่มต้นด้วยเอาต์พุตที่ค่อนข้างแรงและ เพิ่มความเข้มและความคมชัดที่รับรู้ได้ แม้ว่าแอมพลิจูดที่ขับเคลื่อนจะเท่ากันก็ตาม เมื่อถึงจุดเรโซแนนซ์ ความถี่ของเอฟเฟกต์จะลดลง กลับไปที่ค่าต่ำสุด ซึ่งรับรู้ได้ว่าความเข้มและความคมชัดลดลง ซึ่งจะสร้างความรู้สึกถึงแรงต้านเริ่มต้นตามด้วยการปล่อยตัว ซึ่งจำลอง การปล่อยตัวสู่อวกาศ

เอฟเฟกต์นี้ไม่สามารถใช้กับ Basic Envelope API เนื่องจากจะแยกข้อมูลเฉพาะของอุปกรณ์เกี่ยวกับความถี่เรโซแนนซ์และเส้นโค้งความเร่งเอาต์พุต การเพิ่มความคมชัดอาจทำให้ความถี่เทียบเท่าเกินกว่าเรโซแนนซ์ ซึ่งอาจทำให้เกิดการลดความเร็วโดยไม่ตั้งใจ

ภาพเคลื่อนไหวของยานอวกาศที่ทะยานขึ้นจากด้านล่างของหน้าจอ
พล็อตของรูปคลื่นการสั่นสะเทือนของอินพุต

รูปที่ 6 กราฟรูปคลื่นการเร่งเอาต์พุตสำหรับการสั่นที่ จำลองการปล่อยจรวด

@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()
  )
}