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

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

หน้านี้ประกอบด้วยตัวอย่างต่อไปนี้

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

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

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

  • VibrationEffect.Builder (แนะนำ): ตั้งแต่ Android 16 (26Q4) เป็นต้นไป เอฟเฟกต์ที่สร้างโดยใช้ VibrationEffect.Builder จะมีการทำงานสำรองระดับเฟรมเวิร์กอัตโนมัติ หากอุปกรณ์ไม่รองรับ Preset ที่ขอหรือ Envelope พื้นฐานโดยค่าเริ่มต้น เฟรมเวิร์กจะแปลเป็นทางเลือกที่เหมาะสมโดยอัตโนมัติในเวลาเล่นอย่างสุดความสามารถ คุณไม่จำเป็นต้องตรวจสอบความสามารถของอุปกรณ์ต่อพรีมิติฟด้วยตนเองก่อนเล่นเอฟเฟกต์ที่ประกอบด้วย VibrationEffect.Builder
    • ข้อยกเว้น: รูปคลื่นขั้นสูงที่สร้างด้วย WaveformEnvelopeBuilder ไม่รองรับการสำรองอัตโนมัติเนื่องจากขึ้นอยู่กับการแมปความถี่ของฮาร์ดแวร์ที่เฉพาะเจาะจง (FOAM) หากไม่รองรับ ระบบจะไม่เล่นไฟล์ดังกล่าว
  • VibrationEffect.Composition: องค์ประกอบที่สร้างด้วย startComposition() API จะไม่มีการสำรองอัตโนมัติ หากองค์ประกอบมีองค์ประกอบพื้นฐานที่ไม่รองรับแม้เพียงรายการเดียว การสั่นทั้งหมดจะเล่นไม่สำเร็จ คุณต้องตรวจสอบความสามารถด้วยตนเองโดยใช้ vibrator.arePrimitivesSupported()
  • รูปคลื่นที่มีการควบคุมแอมพลิจูด: แอมพลิจูดที่ไม่ใช่ 0 จะปัดขึ้นเป็น 100% ในอุปกรณ์ที่ไม่มีการควบคุมแอมพลิจูด ตรวจสอบ vibrator.hasAmplitudeControl() และกลับไปใช้รูปแบบเปิด/ปิดที่ออกแบบไว้อย่างชัดเจนหากจำเป็น

การใช้การโต้ตอบการสัมผัสพื้นฐาน

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

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

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

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

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

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

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

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

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

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

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

val timings: LongArray = longArrayOf(
    50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200
)
val amplitudes: IntArray = intArrayOf(
    33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255
)
val repeatIndex = -1 // Don't repeat.

vibrator.vibrate(
    VibrationEffect.createWaveform(
        timings, amplitudes, repeatIndex
    )
)

รูปแบบที่เกิดซ้ำ

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

fun startVibrating() {
    val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
    val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
    val repeat = 1 // Repeat from the second entry, index = 1.
    val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform(
        timings, amplitudes, repeat
    )
    // repeatingEffect can be used in multiple places.

    vibrator.vibrate(repeatingEffect)
}

fun stopVibrating() {
    vibrator.cancel()
}

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

รูปแบบที่มีโฆษณาสำรอง

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

if (vibrator.hasAmplitudeControl()) {
    vibrator.vibrate(
        VibrationEffect.createWaveform(
            smoothTimings, amplitudes, smoothRepeatIdx
        )
    )
} else {
    vibrator.vibrate(
        VibrationEffect.createWaveform(
            onOffTimings, onOffRepeatIdx
        )
    )
}

องค์ประกอบที่ยึดตามไทม์ไลน์พร้อม VibrationEffect.Builder

ตั้งแต่ Android 16 (26Q4) เป็นต้นไป VibrationEffect.Builder จะเป็น API ที่แนะนำสำหรับการสร้างเอฟเฟกต์และการจัดองค์ประกอบการสั่นที่ซับซ้อน ซึ่งช่วยให้คุณสร้างความรู้สึกจากการสัมผัสที่สื่ออารมณ์ได้โดยการจัดลำดับองค์ประกอบการสัมผัสที่แยกกันตามไทม์ไลน์แบบสัมบูรณ์ด้วย startTimeMillis

VibrationEffect.Builder รองรับการรวมการสั่นหลายประเภท ดังนี้

  • ค่าที่กำหนดล่วงหน้า: การสั่นแบบรู้สึกได้ที่กำหนดไว้ล่วงหน้า (VibrationEffect.Preset) เช่น การคลิกและการติ๊ก
  • Envelope: รูปคลื่นต่อเนื่องแบบไดนามิก ซึ่งรวมถึง Envelope พื้นฐานที่ไม่ขึ้นอยู่กับฮาร์ดแวร์ (BasicEnvelopeBuilder) และ Envelope ที่มีการปรับความถี่ขั้นสูง (WaveformEnvelopeBuilder)
  • VibrationEffects ที่มีอยู่: องค์ประกอบพื้นฐาน (VibrationEffect.Composition), รูปคลื่นแบบขั้นบันได (VibrationEffect.createWaveform), เอฟเฟกต์ที่กำหนดไว้ล่วงหน้า (VibrationEffect.createPredefined) และแบบครั้งเดียว (VibrationEffect.createOneShot) ที่นำเข้าโดยใช้ addEvents() หรือการคัดลอกคอนสตรัคเตอร์
  • ลำดับที่ซ้ำกัน: รูปแบบต่อเนื่องที่กำหนดค่าด้วย setRepeatingEffect()

การสั่นที่สร้างด้วย VibrationEffect.Builder มีการทำงานสำรองอัตโนมัติในระดับเฟรมเวิร์กในตัวสำหรับองค์ประกอบต่างๆ (รวมถึงค่าที่กำหนดล่วงหน้า, เอนเวโลปพื้นฐาน และเอฟเฟกต์การสั่นแบบผสม) ซึ่งช่วยให้ผู้ใช้ได้รับประสบการณ์ที่สอดคล้องกันในอุปกรณ์ต่างๆ โดยไม่ต้องตรวจสอบความสามารถด้วยตนเอง (Advanced waveform envelopes created with WaveformEnvelopeBuilder require hardware support and don't have automatic fallback support.)

เขียนด้วยค่าที่กำหนดล่วงหน้า

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

ค่าที่กำหนดล่วงหน้าจะแทนที่ค่าพื้นฐานแบบสั้นจาก VibrationEffect.Composition API สำหรับความรู้สึกการตอบสนองต่อการสัมผัสที่ยาวนานขึ้นหรือต่อเนื่อง เช่น การเพิ่มและลดระดับ ให้ใช้รูปแบบคลื่นเอนเวโลป (PWLE) แทน

ค่าที่กำหนดล่วงหน้าแต่ละค่าสามารถกำหนดสเกลที่ไม่บังคับระหว่าง 0.0f ถึง 1.0f และจะวางไว้ที่เวลาเริ่มต้นที่ชัดเจน (เป็นมิลลิวินาที) จากจุดเริ่มต้นขององค์ประกอบ

Kotlin

val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)

val effect = VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build()

vibrator.vibrate(effect)

Java

VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);

VibrationEffect effect = new VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build();

vibrator.vibrate(effect);

แต่งเพลงด้วย Envelope และค่าที่กำหนดล่วงหน้า

คุณสามารถรวมอินสแตนซ์ VibrationEffect.Envelope (สร้างโดยใช้ BasicEnvelopeBuilder หรือ WaveformEnvelopeBuilder) เข้ากับค่าที่กำหนดล่วงหน้าได้อย่างราบรื่นเพื่อสร้างรูปแบบการโต้ตอบด้วยการสัมผัสที่หลากหลายและมีหลายกลุ่ม

ต่อไปนี้คือตัวอย่างที่เล่นซองจดหมายแบบค่อยๆ เพิ่มและค่อยๆ จางลงอย่างราบรื่น ตามด้วยค่าที่กำหนดล่วงหน้าสำหรับการคลิกที่คมชัด

Kotlin

val basicEnvelope = VibrationEffect.Envelope.create(
    VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
)

val mixedEffect = VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build()

vibrator.vibrate(mixedEffect)

Java

VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
    new VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
);

VibrationEffect mixedEffect = new VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build();

vibrator.vibrate(mixedEffect);

นำกิจกรรมที่มีอยู่มาใช้ซ้ำและเลื่อนเวลา

หากต้องการใช้ซ้ำหรือเชื่อมโยง VibrationEffect ที่มีอยู่ (รวมถึง VibrationEffect.Composition) ให้ดึงข้อมูลรายการออบเจ็กต์ VibrationEffect.Event โดยใช้ getEvents() และเพิ่มออบเจ็กต์เหล่านั้นด้วยออฟเซ็ตโดยใช้ addEvents(startTimeShiftMillis, events) (หรือส่งเอฟเฟกต์ไปยังคอนสตรักเตอร์ VibrationEffect.Builder(effect) โดยตรง) เมื่อนำเข้าอินสแตนซ์ VibrationEffect.Composition ด้วยวิธีนี้ เฟรมเวิร์กจะแปลงองค์ประกอบพื้นฐานเป็นค่าที่กำหนดล่วงหน้าโดยอัตโนมัติ ซึ่งจะเปิดใช้การรองรับการสำรองรันไทม์

Kotlin

val existingEffect = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build()

// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
    .build()

vibrator.vibrate(combinedEffect)

Java

VibrationEffect existingEffect = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build();

// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
    .build();

vibrator.vibrate(combinedEffect);

สร้างองค์ประกอบที่เกิดซ้ำ

ใช้ setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) เพื่อเพิ่มรูปแบบที่ซ้ำกันลงในองค์ประกอบ

Kotlin

val repeatingPattern = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build()

val repeatingEffect = VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build()

vibrator.vibrate(repeatingEffect)

Java

VibrationEffect repeatingPattern = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build();

VibrationEffect repeatingEffect = new VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build();

vibrator.vibrate(repeatingEffect);

การจัดการเวลา การตรวจสอบความถูกต้อง และความคลาดเคลื่อน

เมื่อออกแบบองค์ประกอบด้วย VibrationEffect.Builder โปรดคำนึงถึงกฎด้านเวลาและการตรวจสอบต่อไปนี้

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

สร้างองค์ประกอบพื้นฐานของการสั่น

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

API VibrationEffect.Composition ต่างจาก VibrationEffect.Builder ตรงที่ไม่มีการสำรองอัตโนมัติสำหรับพริมิทีฟที่ไม่รองรับ ดังนั้น

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

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

  • Kotlin: val fallbackEffect = VibrationEffect.Builder(compositionEffect).build() (หรือ .addEvents(0L, compositionEffect.events))
  • Java: VibrationEffect fallbackEffect = new VibrationEffect.Builder(compositionEffect).build(); (หรือ .addEvents(0L, compositionEffect.getEvents()))

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

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

คุณสร้างเอฟเฟกต์การสั่นที่ประกอบขึ้นได้ด้วย VibrationEffect.Composition ตัวอย่างเอฟเฟกต์เสียงที่ค่อยๆ ดังขึ้นตามด้วยเสียงคลิกที่ชัดเจน

vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
        VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
    ).addPrimitive(
        VibrationEffect.Composition.PRIMITIVE_CLICK
    ).compose()
)

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

สร้างตัวแปรในรูปแบบการสั่นพื้นฐาน

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

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

นอกจากนี้ องค์ประกอบยังระบุความล่าช้าที่จะเพิ่มระหว่างองค์ประกอบพื้นฐานที่ต่อเนื่องกันได้ด้วย ความล่าช้านี้จะแสดงเป็นมิลลิวินาทีนับตั้งแต่สิ้นสุดพรีมิทิฟก่อนหน้า โดยทั่วไปแล้ว ช่องว่าง 5 ถึง 10 มิลลิวินาทีระหว่างองค์ประกอบพื้นฐาน 2 รายการนั้นสั้นเกินกว่าที่จะตรวจจับได้ ใช้ช่องว่างตามลำดับ 50 มิลลิวินาทีขึ้นไปหากต้องการสร้างช่องว่างที่สังเกตเห็นได้ระหว่างองค์ประกอบพื้นฐาน 2 รายการ ต่อไปนี้คือตัวอย่างขององค์ประกอบที่มีการหน่วงเวลา

val delayMs = 100
vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
        VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
    ).addPrimitive(
        VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
    ).addPrimitive(
        VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
    ).compose()
)

ตรวจสอบว่าระบบรองรับองค์ประกอบพื้นฐานใดบ้าง

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

val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK

if (vibrator.areAllPrimitivesSupported(primitive)) {
    vibrator.vibrate(
        VibrationEffect.startComposition()
            .addPrimitive(primitive).compose()
    )
} else {
    // Play a predefined effect or custom pattern as a fallback.
}

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

val primitives: IntArray = intArrayOf(
    VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
    VibrationEffect.Composition.PRIMITIVE_TICK,
    VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)

ตัวอย่างองค์ประกอบการสั่น

ส่วนต่อไปนี้แสดงตัวอย่างองค์ประกอบการสั่นหลายรายการที่นำมาจากแอปตัวอย่างระบบการสั่นใน GitHub

ต้านทาน (มีเห็บน้อย)

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

ภาพเคลื่อนไหวของวงกลมที่ถูกลากลง
กราฟของรูปคลื่นการสั่นอินพุต

รูปที่ 1 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์

@Composable
fun ResistScreen() {
    // Control variables for the dragging of the indicator.
    var isDragging by remember { mutableStateOf(false) }
    var dragOffset by remember { mutableFloatStateOf(0f) }

    // Only vibrates while the user is dragging
    LaunchedEffect(isDragging) {
        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        while (isDragging) {
            // Calculate the interval inversely proportional to the drag offset.
            val vibrationInterval = calculateVibrationInterval(dragOffset)
            // Calculate the scale directly proportional to the drag offset.
            val vibrationScale = calculateVibrationScale(dragOffset)

            delay(vibrationInterval)
            vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                    vibrationScale
                ).compose()
            )
        }
    }

    Column(
        Modifier
            .draggable(
                orientation = Orientation.Vertical,
                onDragStarted = {
                    isDragging = true
                },
                onDragStopped = {
                    isDragging = false
                },
                state = rememberDraggableState { delta ->
                    dragOffset += delta
                }
            )
    ) {
        // Build the indicator UI based on how much the user has dragged it.
        ResistIndicator(dragOffset)
    }
}

ขยาย (มีขึ้นและลง)

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

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

ภาพเคลื่อนไหวของวงกลมที่ขยายออก
กราฟของรูปแบบคลื่นการสั่นสะเทือนอินพุต

รูปที่ 2 รูปแบบคลื่นนี้แสดงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์

enum class ExpandShapeState {
    Collapsed,
    Expanded
}

@Composable
fun ExpandScreen() {
    // Control variable for the state of the indicator.
    var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }

    // Animation between expanded and collapsed states.
    val transitionData = updateTransitionData(currentState)

    Column(
        Modifier
            .clickable(
                onClick = {
                    if (currentState == ExpandShapeState.Collapsed) {
                        currentState = ExpandShapeState.Expanded
                        vibrator.vibrate(
                            VibrationEffect.startComposition().addPrimitive(
                                VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
                                0.3f
                            ).addPrimitive(
                                VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                                0.3f
                            ).compose()
                        )
                    } else {
                        currentState = ExpandShapeState.Collapsed
                        vibrator.vibrate(
                            VibrationEffect.startComposition().addPrimitive(
                                VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
                            ).compose()
                        )
                    }
                }
            )
    ) {
        // Build the indicator UI based on the current state.
        ExpandIndicator(transitionData)
    }
}

โยก (พร้อมหมุน)

หลักการเกี่ยวกับระบบการสั่นที่สำคัญอย่างหนึ่งคือการสร้างความพึงพอใจให้แก่ผู้ใช้ วิธีสนุกๆ ในการสร้างเอฟเฟกต์การสั่นที่น่าพึงพอใจและไม่คาดคิดคือการใช้ PRIMITIVE_SPIN พรีมิติฟนี้จะมีประสิทธิภาพสูงสุดเมื่อมีการเรียกใช้มากกว่า 1 ครั้ง การหมุนหลายๆ ครั้งต่อกันอาจทำให้เกิดเอฟเฟกต์การสั่นไหวและไม่เสถียร ซึ่งสามารถปรับปรุงเพิ่มเติมได้โดยใช้การปรับขนาดแบบสุ่มในแต่ละองค์ประกอบพื้นฐาน นอกจากนี้ คุณยังทดลองใช้ช่องว่างระหว่างองค์ประกอบพื้นฐานของการหมุนที่ต่อเนื่องกันได้ด้วย การหมุน 2 ครั้งโดยไม่มีช่องว่าง (0 มิลลิวินาทีระหว่างการหมุน) จะสร้างความรู้สึกหมุนที่แน่นหนา การเพิ่มช่องว่างระหว่างการหมุนจาก 10 เป็น 50 มิลลิวินาทีจะทำให้รู้สึกถึงการหมุนที่หลวมขึ้น และสามารถใช้เพื่อจับคู่ระยะเวลาของวิดีโอหรือภาพเคลื่อนไหวได้

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

นี่คือตัวอย่างของรูปร่างที่ยืดหยุ่นซึ่งจะเด้งกลับหลังจากถูกลากลงแล้วปล่อย ภาพเคลื่อนไหวได้รับการปรับปรุงด้วยเอฟเฟกต์การหมุน 2 แบบ ซึ่งเล่นด้วยความหนักที่แตกต่างกันซึ่งเป็นสัดส่วนกับการกระจัดของการเด้ง

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

รูปที่ 3 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableFloatStateOf(0f) }
    var isWobbling by remember { mutableStateOf(false) }

    // Use drag distance to create an animated float value behaving like a spring.
    val dragDistanceAnimated by animateFloatAsState(
        targetValue = if (dragDistance > 0f) dragDistance else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioHighBouncy,
            stiffness = Spring.StiffnessMedium
        ),
        // The spring ends on its target. An end value of 0f means that the
        // shape is back at rest, so the wobble is over.
        finishedListener = { endValue -> if (endValue == 0f) isWobbling = false },
    )

    LaunchedEffect(isWobbling) {
        while (isWobbling) {
            val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
            // Use some sort of minimum displacement so the final few frames
            // of animation don't generate a vibration.
            if (displacement > SPIN_MIN_DISPLACEMENT) {
                vibrator.vibrate(
                    VibrationEffect.startComposition().addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SPIN,
                        nextSpinScale(displacement)
                    ).addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SPIN,
                        nextSpinScale(displacement)
                    ).compose()
                )
            }
            // Delay the next check for a sufficient duration until the
            // current composition finishes. Note that you can use
            // Vibrator.getPrimitiveDurations API to calculcate the delay.
            delay(VIBRATION_DURATION)
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .draggable(
                onDragStopped = {
                    // Wobble only if the shape is far enough from rest to vibrate.
                    // It then springs back to 0f, and the finishedListener ends
                    // the wobble.
                    val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                    isWobbling = displacement > SPIN_MIN_DISPLACEMENT
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value. The
        // lambda defers the read to the draw phase, so the animation doesn't
        // recompose this screen on every frame.
        WobbleShape { dragDistanceAnimated }
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
    // Generate a random offset in the range [-0.1, +0.1] to be added to the
    // vibration scale so the spin effects have slightly different values.
    val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
    return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}

เด้ง (มีเสียงตุบ)

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

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

ภาพเคลื่อนไหวของลูกบอลที่ตกลงมาและเด้งออกจากด้านล่างของหน้าจอ
กราฟของรูปแบบคลื่นการสั่นที่ป้อน

รูปที่ 4 รูปคลื่นนี้แสดงถึงความเร่งเอาต์พุตของการสั่นบนอุปกรณ์

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
    // Control variable for the state of the ball.
    var ballPosition by remember { mutableStateOf(BallPosition.Start) }
    // Plain holder for the bounce count. It's read only in the effect and in
    // the click handler, never in composition, so it needs no snapshot state.
    val bounceCount = remember { intArrayOf(0) }

    // Animation for the bouncing ball.
    val transitionData = updateTransitionData(ballPosition)
    val collisionData = updateCollisionData(transitionData)

    // Ball is about to contact floor. The effect runs only when the collision
    // state changes, so it vibrates once per collision. Nothing here suspends,
    // so a keyed SideEffect does the job without launching a coroutine.
    SideEffect(collisionData.collisionWithFloor) {
        if (collisionData.collisionWithFloor) {
            val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat()
            vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_THUD,
                    vibrationScale
                ).compose()
            )
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .clickable {
                if (transitionData.isAtStart) {
                    ballPosition = BallPosition.End
                } else {
                    ballPosition = BallPosition.Start
                    bounceCount[0] = 0
                }
            },
    ) {
        // Build the ball UI based on the current state.
        BouncingBall(transitionData)
    }
}

รูปคลื่นการสั่นพร้อมเอนเวโลป

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

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

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

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

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

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

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

vibrator.vibrate(
    VibrationEffect.WaveformEnvelopeBuilder()
        .addControlPoint(1.0f, 60f, 50)
        .addControlPoint(1.0f, 120f, 100)
        .addControlPoint(1.0f, 120f, 200)
        .addControlPoint(0.0f, 60f, 50)
        .build()
)

ส่วนต่อไปนี้แสดงตัวอย่างรูปคลื่นการสั่นสะเทือนที่มีเอนเวโลปหลายตัวอย่าง

สปริงเด้งดึ๋ง

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

ต่อไปนี้คือตัวอย่างของสปริงที่ตกลงมาอย่างอิสระ โดยมีการปรับปรุงภาพเคลื่อนไหวด้วยเอฟเฟกต์เอนเวโลปพื้นฐานที่เล่นทุกครั้งที่สปริงกระเด้งออกจากด้านล่างของหน้าจอ

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

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

@Composable
fun BouncingSpringAnimation() {
    // The spring's height above the floor: 1f is the drop height, 0f is the floor.
    val springHeight = remember { Animatable(1f) }
    var isAnimating by remember { mutableStateOf(true) }

    LaunchedEffect(isAnimating) {
        if (!isAnimating) return@LaunchedEffect
        springHeight.snapTo(1f)

        // Each bounce reaches a fraction of the previous height.
        var bounceHeight = 1f
        while (bounceHeight > MIN_BOUNCE_HEIGHT) {
            // Fall to the floor, speeding up like a falling object. A fall from
            // a greater height takes longer.
            val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
            springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad))

            // The spring hits the floor: play a "boing" envelope effect that fades
            // out. A higher bounce gives a stronger and sharper vibration.
            vibrator.vibrate(
                VibrationEffect.BasicEnvelopeBuilder()
                    // Starting from zero sharpness here, will simulate a smoother
                    // "boing" effect.
                    .setInitialSharpness(0f)
                    // Add a control point to reach the target intensity and
                    // sharpness very quickly.
                    .addControlPoint(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L)
                    // Add a control point to fade out the vibration intensity while
                    // maintaining sharpness.
                    .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong())
                    .build()
            )

            // Bounce back up to a lower height, slowing down like a thrown object.
            // When the next bounce is too small, the spring stays on the floor.
            bounceHeight *= BOUNCE_DAMPING
            if (bounceHeight > MIN_BOUNCE_HEIGHT) {
                val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
                springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad))
            }
        }
        isAnimating = false
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = !isAnimating) { isAnimating = true }
            .drawBehind {
                // Read the height in the draw phase, and scale it to the distance
                // between the top of the box and the floor.
                val floorTop = size.height - FLOOR_HEIGHT.toPx()
                val dropDistance = floorTop - SPRING_HEIGHT.toPx()
                drawSpring(topY = dropDistance * (1f - springHeight.value))
                drawFloor(top = floorTop)
            }
    ) {
        if (!isAnimating) {
            Text("Tap to restart", Modifier.align(Alignment.Center))
        }
    }
}

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

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

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

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

ภาพเคลื่อนไหวของจรวดที่บินขึ้นจากด้านล่างของหน้าจอ
กราฟของรูปแบบคลื่นการสั่นที่ป้อน

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

@Composable
fun RocketLaunchAnimation() {
    var isLaunched by remember { mutableStateOf(false) }
    val animation = remember { Animatable(0f) }

    val animationDuration = 3000
    LaunchedEffect(isLaunched) {
        if (isLaunched) {
            animation.animateTo(
                1.2f, // Overshoot so that the rocket goes off the screen.
                animationSpec = tween(
                    durationMillis = animationDuration,
                    // Applies an easing curve with a slow start and rapid acceleration
                    // towards the end.
                    easing = CubicBezierEasing(1f, 0f, 0.75f, 1f)
                )
            )
            animation.snapTo(0f)
            isLaunched = false
        }
    }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .clickable {
                if (!isLaunched) {
                    // Play vibration with same duration as the animation, using 70% of
                    // the time for the rise of the vibration, to match the easing curve
                    // defined previously.
                    playVibration(vibrator, animationDuration.toLong(), 0.7f)
                    isLaunched = true
                }
            }
            .background(colorResource(R.color.background))
    ) {
        // Read the animation in the draw phase, and scale it to the canvas height.
        drawRocket(size.height * animation.value)
    }
}

private fun playVibration(
    vibrator: Vibrator,
    totalDurationMs: Long,
    riseBias: Float,
    minOutputAccelerationGs: Float = 0.1f,
) {
    require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." }

    if (!vibrator.areEnvelopeEffectsSupported()) {
        return
    }

    val resonantFrequency = vibrator.resonantFrequency
    if (resonantFrequency.isNaN()) {
        // Device doesn't have or expose a resonant frequency.
        return
    }

    val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return

    if (startFrequency >= resonantFrequency) {
        // Vibrator can't generate the minimum required output at lower frequencies.
        return
    }

    val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis
    val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs
    val rampDownDurationMs = totalDurationMs - rampUpDurationMs - minDurationMs

    vibrator.vibrate(
        VibrationEffect.WaveformEnvelopeBuilder()
            // Quickly reach the target output at the start frequency
            .addControlPoint(0.1f, startFrequency, minDurationMs)
            .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs)
            .addControlPoint(0.1f, startFrequency, rampDownDurationMs)
            // Controlled ramp down to zero to avoid ringing after the vibration.
            .addControlPoint(0.0f, startFrequency, minDurationMs)
            .build()
    )
}

LavaBeats

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

ซึ่งทำได้โดยการแสดงไบโอมาร์กเกอร์ของสัญญาณคลื่นไฟฟ้าหัวใจ (ECG) ทั่วไปที่มีส่วนการสั่นสะเทือนของแอมพลิจูดและความถี่บางอย่าง LavaBeats เป็นตัวอย่างที่แสดงลักษณะเฉพาะ 2 ส่วนของการบันทึก ECG เป็นพัลส์ 2 ครั้งที่คั่นด้วยการหน่วงเวลา พัลส์ลักษณะเฉพาะแรกคือ QRS Complex ซึ่งแสดงเป็นยอดแหลมที่มีแอมพลิจูดสูงและระยะเวลาสั้น พัลส์ที่ 2 คือคลื่น T ซึ่งมีแอมพลิจูดต่ำกว่า ระยะเวลานานกว่า และรูปร่างที่ราบรื่นกว่า (ดูรูปที่ 7)

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

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

ภาพเคลื่อนไหวของโคมไฟลาวาที่เต้นเป็นจังหวะตามรูปแบบการสั่นของหัวใจ
แผนภาพของส่วน ECG และรูปคลื่นลักษณะเฉพาะ

รูปที่ 7 ส่วนหนึ่งของบันทึก ECG ที่มี QRS Complex และคลื่น T

private fun createEnvelopeEffect(
    beatParameters: List<BeatParameter>
): VibrationEffect =
    VibrationEffect.WaveformEnvelopeBuilder()
        .apply {
            repeat(beatParameters.getNumBeats()) {
                // First pulse chirp
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseStartFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstPulseDurationMillis().toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )

                // Delay between first and second pulse
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstToSecondPulseDelayMillis().toLong(),
                )

                // Second pulse
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    beatParameters.getBeatDelayMillis().toLong(),
                )
            }
        }
        .build()

/** A parameter of a haptic beat effect that represents an ECG signal parameter */
data class BeatParameter(
    val description: String = "",
    val value: Float = 0f,
    val range: ClosedFloatingPointRange<Float> = 0f..1f,
    val steps: Int = 0,
    val isFrequencyType: Boolean = false,
)