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

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

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

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

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

เมื่อใช้เอฟเฟกต์ที่กำหนดเอง ให้พิจารณาสิ่งต่อไปนี้

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

เพิ่มระยะห่างระหว่างรูปแบบการสั่น

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