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

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

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

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

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

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

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

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

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

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

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

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

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

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

การใช้วัตถุพื้นฐานแบบรู้สึกได้

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

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

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

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

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

ตัวอย่าง: รูปแบบการเพิ่มจำนวน

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

  1. การจับเวลา: อาร์เรย์ของระยะเวลาเป็นมิลลิวินาทีสำหรับ Waveform แต่ละรูปแบบ กลุ่ม
  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 // Do not 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; // Do not repeat.

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

ตัวอย่าง: รูปแบบซ้ำ

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

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

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

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

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

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

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 จะแมปกับแอมพลิจูดขั้นต่ำที่ 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());

สามารถใช้ 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);

ตัวอย่าง: ต้านทาน (มีขีดต่ำ)

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

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

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 ทั้งคู่บรรลุเป้าหมายเดียวกัน แต่มีระยะเวลาต่างกัน มีเพียงรายการเดียวเท่านั้น แบบเดิมเพื่อลดจำนวนโฆษณา PRIMITIVE_QUICK_FALL ชนิดพื้นฐานเหล่านี้จะทำงานร่วมกันได้ดียิ่งขึ้นเพื่อสร้างส่วนของรูปแบบคลื่นที่เติบโตขึ้นเรื่อยๆ และดับไป คุณสามารถจัดเรียงค่าพื้นฐานที่ปรับขนาดแล้วเพื่อป้องกันการกะทันหัน ในช่วงแอมพลิจูด (แอมพลิจูด) ระหว่างตัว ซึ่งก็เหมาะกับการขยายขนาดโดยรวมด้วย ระยะเวลาของผลกระทบ โดยส่วนตัวแล้ว ผู้คนมักสังเกตเห็นส่วนที่เพิ่มขึ้นมากกว่า ส่วนที่ลดลง ดังนั้นการทำให้ส่วนที่มีการเพิ่มขึ้นนั้นสั้นกว่าช่วงที่ลดลง เพื่อเน้นไปยังส่วนที่ลดลง

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

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

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 ค่าพื้นฐานนี้จะมีประสิทธิภาพมากที่สุดเมื่อมีการเรียกมากกว่า 1 ครั้ง หลายสกุลเงิน การหมุนที่เชื่อมกันอาจทำให้เกิดการสั่นสะเทือนและไม่เสถียร ซึ่ง เพิ่มประสิทธิภาพเพิ่มเติมด้วยการใช้สเกลที่ค่อนข้างแบบสุ่มกับค่าพื้นฐานแต่ละรายการ คุณ นอกจากนี้ยังสามารถทดสอบกับช่องว่างระหว่าง Primitives ในการหมุนแบบต่อเนื่องได้อีกด้วย 2 รอบ โดยไม่มีช่องว่างใดๆ (ระหว่าง 0 มิลลิวินาที) จะทำให้เกิดการหมุนที่ตึง เพิ่ม ช่องว่างระหว่างการหมุนจาก 10 ถึง 50 มิลลิวินาที ทำให้ความรู้สึกในการหมุนเร็วขึ้น และ สามารถใช้เพื่อจับคู่ระยะเวลาของวิดีโอหรือภาพเคลื่อนไหว

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

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

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

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 [-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 [-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 สามารถสร้างเอฟเฟ็กต์ที่ดังกึกก้อง ซึ่งสามารถจับคู่กับ การแสดงภาพผลกระทบในวิดีโอหรือภาพเคลื่อนไหว เป็นต้น เพื่อเพิ่ม ประสบการณ์โดยรวม

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

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

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