Membuat efek haptic kustom

Halaman ini membahas contoh cara menggunakan haptics API yang berbeda untuk membuat efek kustom dalam aplikasi Android. Karena sebagian besar informasi di halaman ini bergantung pada pengetahuan yang baik tentang cara kerja aktuator getaran, sebaiknya baca Primer aktuator getaran.

Halaman ini menyertakan contoh berikut.

Untuk contoh lainnya, lihat Menambahkan respons haptic ke peristiwa, dan selalu ikuti prinsip desain haptic.

Menggunakan penggantian untuk menangani kompatibilitas perangkat

Saat menerapkan efek kustom, pertimbangkan hal berikut:

  • Kemampuan perangkat mana yang diperlukan untuk memberikan efek
  • Apa yang harus dilakukan saat perangkat tidak dapat memutar efek

Referensi Android haptics API memberikan detail cara memeriksa dukungan untuk komponen yang terlibat dalam haptic Anda, sehingga aplikasi Anda dapat memberikan pengalaman keseluruhan yang konsisten.

Bergantung pada kasus penggunaan, Anda dapat menonaktifkan efek kustom atau memberikan efek kustom alternatif berdasarkan potensi kemampuan yang berbeda.

Rencanakan kelas tingkat tinggi untuk kemampuan perangkat berikut:

  • Jika Anda menggunakan primitif haptic: perangkat yang mendukung primitif tersebut yang diperlukan oleh efek kustom. (Lihat bagian berikutnya untuk mengetahui detail tentang primitif.)

  • Perangkat dengan kontrol amplitudo.

  • Perangkat dengan dukungan getaran dasar (aktif/nonaktif)—dengan kata lain, perangkat yang tidak memiliki kontrol amplitudo.

Jika pilihan efek sentuhan aplikasi Anda memperhitungkan kategori ini, pengalaman pengguna sentuhan harus tetap dapat diprediksi untuk setiap perangkat.

Penggunaan primitif haptic

Android menyertakan beberapa primitif haptic yang bervariasi dalam amplitudo dan frekuensi. Anda dapat menggunakan satu primitif saja atau beberapa primitif dalam kombinasi untuk mendapatkan efek haptik yang kaya.

  • Gunakan penundaan selama 50 milidetik atau lebih untuk kesenjangan yang terlihat antara dua primitif, dengan juga memperhitungkan durasi primitif jika memungkinkan.
  • Gunakan skala yang berbeda dengan rasio 1,4 atau lebih agar perbedaan intensitas lebih terlihat.
  • Gunakan skala 0,5, 0,7, dan 1,0 untuk membuat versi primitif dengan intensitas rendah, sedang, dan tinggi.

Membuat pola getaran kustom

Pola getaran sering digunakan dalam haptic atensi, seperti notifikasi dan nada dering. Layanan Vibrator dapat memutar pola getaran panjang yang mengubah amplitudo getaran dari waktu ke waktu. Efek tersebut dinamakan bentuk gelombang.

Efek bentuk gelombang dapat dengan mudah terlihat, tetapi getaran lama yang tiba-tiba dapat mengagetkan pengguna jika diputar di lingkungan yang tenang. Dorongan ke amplitudo target terlalu cepat juga dapat menghasilkan bunyi dengungan yang terdengar. Rekomendasi untuk mendesain pola bentuk gelombang adalah menghaluskan transisi amplitudo untuk menciptakan efek naik dan turun.

Sampel: Pola peningkatan

Bentuk gelombang direpresentasikan sebagai VibrationEffect dengan tiga parameter:

  1. Waktu: array durasi, dalam milidetik, untuk setiap segmen bentuk gelombang.
  2. Amplitudo: amplitudo getaran yang diinginkan untuk setiap durasi yang ditentukan dalam argumen pertama, diwakili oleh nilai bilangan bulat dari 0 hingga 255, dengan 0 mewakili vibrator "nonaktif" dan 255 sebagai amplitudo maksimum perangkat.
  3. Ulangi indeks: indeks dalam array yang ditentukan dalam argumen pertama untuk mulai mengulangi bentuk gelombang, atau -1 jika pola hanya diputar sekali.

Berikut adalah contoh bentuk gelombang yang berkedip dua kali dengan jeda 350 md di antara pulsa. Denyut pertama adalah peningkatan halus ke amplitudo maksimum, dan yang kedua adalah tanjakan cepat untuk menyimpan amplitudo maksimum. Berhenti di akhir ditentukan oleh nilai indeks pengulangan negatif.

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

Sampel: Pola berulang

Bentuk gelombang juga dapat diputar berulang kali hingga dibatalkan. Cara untuk membuat bentuk gelombang berulang adalah dengan menetapkan parameter 'repeat' non-negatif. Saat Anda memutar bentuk gelombang berulang, getaran akan terus berlanjut hingga dibatalkan secara eksplisit dalam layanan:

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

Hal ini sangat berguna untuk peristiwa yang terjadi sesekali yang memerlukan tindakan pengguna untuk mengonfirmasinya. Contoh peristiwa tersebut mencakup panggilan telepon masuk dan alarm yang dipicu.

Sampel: Pola dengan penggantian

Mengontrol amplitudo getaran adalah kemampuan yang bergantung pada hardware. Memutar bentuk gelombang pada perangkat kelas bawah tanpa kemampuan ini akan menyebabkannya bergetar pada amplitudo maksimum untuk setiap entri positif dalam array amplitudo. Jika aplikasi Anda perlu mengakomodasi perangkat tersebut, sebaiknya pastikan pola Anda tidak menghasilkan efek berdengung saat diputar dalam kondisi tersebut, atau untuk mendesain pola AKTIF/NONAKTIF yang lebih sederhana, yang dapat diputar sebagai penggantian.

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

Membuat komposisi getaran

Bagian ini menampilkan cara untuk menyusunnya menjadi efek kustom yang lebih panjang dan lebih kompleks, dan lebih kompleks lagi untuk mempelajari haptic yang beragam menggunakan kemampuan hardware yang lebih canggih. Anda dapat menggunakan kombinasi efek yang memvariasikan amplitudo dan frekuensi untuk menciptakan efek haptik yang lebih kompleks pada perangkat dengan aktuator haptik yang memiliki bandwidth frekuensi lebih lebar.

Proses untuk membuat pola getaran kustom, yang dijelaskan sebelumnya di halaman ini, menjelaskan cara mengontrol amplitudo getaran untuk menciptakan efek halus dari naik dan turun. Rich haptic meningkatkan konsep ini dengan menjelajahi rentang frekuensi vibrator perangkat yang lebih luas untuk membuat efek lebih lancar. Bentuk gelombang ini sangat efektif untuk menciptakan efek crescendo atau diminuendo.

primitif komposisi, yang dijelaskan sebelumnya di halaman ini, diimplementasikan oleh produsen perangkat. Ponsel ini memberikan getaran yang jelas, singkat, dan menyenangkan yang sesuai dengan prinsip Haptic untuk haptic yang jelas. Untuk mengetahui detail selengkapnya tentang kemampuan ini dan cara kerjanya, lihat Primer aktuator getaran.

Android tidak menyediakan fallback untuk komposisi dengan primitif yang tidak didukung. Sebaiknya lakukan langkah-langkah berikut:

  1. Sebelum mengaktifkan haptic lanjutan, pastikan perangkat tertentu mendukung semua primitif yang Anda gunakan.

  2. Menonaktifkan kumpulan pengalaman yang konsisten yang tidak didukung, bukan hanya efek yang tidak memiliki primitif. Informasi selengkapnya tentang cara memeriksa dukungan perangkat ditampilkan sebagai berikut.

Anda dapat membuat efek getaran yang tersusun dengan VibrationEffect.Composition. Berikut adalah contoh efek naik perlahan diikuti dengan efek klik tajam:

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

Komposisi dibuat dengan menambahkan primitif untuk diputar secara berurutan. Setiap primitif juga dapat diskalakan, sehingga Anda dapat mengontrol amplitudo getaran yang dihasilkan oleh setiap primitif tersebut. Skala didefinisikan sebagai nilai antara 0 dan 1, dengan 0 sebenarnya dipetakan ke amplitudo minimum yang dapat digunakan (hampir) terasa oleh primitif ini.

Jika Anda ingin membuat versi yang lemah dan kuat dari primitif yang sama, sebaiknya timbangan berbeda dengan rasio 1,4 atau lebih, sehingga perbedaan intensitas dapat terlihat dengan mudah. Jangan mencoba membuat lebih dari tiga tingkat intensitas primitif yang sama, karena keduanya tidak terlihat berbeda. Misalnya, gunakan skala 0,5, 0,7, dan 1,0 untuk membuat versi primitif dengan intensitas rendah, sedang, dan tinggi.

Komposisi juga dapat menentukan penundaan yang akan ditambahkan di antara primitif yang berurutan. Penundaan ini dinyatakan dalam milidetik sejak akhir primitif sebelumnya. Secara umum, jarak 5 hingga 10 md antara dua primitif terlalu singkat untuk dapat dideteksi. Sebaiknya gunakan jeda dalam urutan 50 milidetik atau lebih jika Anda ingin membuat jarak yang jelas antara dua primitif. Berikut adalah contoh komposisi dengan penundaan:

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 berikut dapat digunakan untuk memverifikasi dukungan perangkat untuk primitif tertentu:

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.
}

Anda juga dapat memeriksa beberapa primitif, lalu menentukan primitif mana yang akan disusun berdasarkan tingkat dukungan perangkat:

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

Sampel: Tolak (dengan tick rendah)

Anda dapat mengontrol amplitudo getaran primitif untuk menyampaikan masukan yang berguna untuk tindakan yang sedang berlangsung. Nilai skala dengan jarak yang berdekatan dapat digunakan untuk membuat efek crescendo halus dari sebuah primitif. Penundaan antara primitif berturut-turut juga dapat ditetapkan secara dinamis berdasarkan interaksi pengguna. Hal ini diilustrasikan dalam contoh animasi tampilan berikut yang dikontrol oleh gestur tarik dan ditambah dengan haptic.

Animasi lingkaran yang ditarik ke bawah
Plot bentuk gelombang getaran input

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

Sampel: Meluaskan (dengan naik dan turun)

Ada dua primitif untuk meningkatkan intensitas getaran yang dirasakan: PRIMITIVE_QUICK_RISE dan PRIMITIVE_SLOW_RISE. Keduanya menjangkau target yang sama, tetapi dengan durasi yang berbeda. Hanya ada satu primitif untuk menurunkan performa, yaitu PRIMITIVE_QUICK_FALL. Primitif ini bekerja lebih baik bersama-sama untuk membuat segmen bentuk gelombang yang meningkat intensitasnya, lalu mati. Anda dapat menyelaraskan primitif yang diskalakan untuk mencegah lompatan tiba-tiba dalam amplitudo di antaranya, yang juga berfungsi dengan baik untuk memperpanjang durasi efek keseluruhan. Secara persepsi, pengguna selalu memperhatikan bagian yang naik lebih dari bagian yang turun, sehingga membuat bagian yang naik lebih pendek daripada bagian menurun dapat digunakan untuk menggeser penekanan ke arah yang turun.

Berikut adalah contoh penerapan komposisi ini untuk memperluas dan menciutkan lingkaran. Efek naik dapat meningkatkan perasaan ekspansi selama animasi. Kombinasi efek naik dan turun membantu menekankan penciutan di akhir animasi.

Animasi lingkaran yang meluas
Plot bentuk gelombang getaran input

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

Contoh: Wobble (dengan putaran)

Salah satu prinsip haptic utama adalah untuk menyenangkan pengguna. Cara menyenangkan untuk memperkenalkan efek getaran tak terduga yang menyenangkan adalah dengan menggunakan PRIMITIVE_SPIN. Primitif ini paling efektif jika dipanggil lebih dari sekali. Beberapa putaran yang digabungkan dapat menciptakan efek goyang dan tidak stabil, yang dapat ditingkatkan lebih lanjut dengan menerapkan penskalaan yang sedikit acak pada setiap primitif. Anda juga dapat bereksperimen dengan jeda antara primitif putaran berturut-turut. Dua putaran tanpa jeda (0 md di antaranya) akan menciptakan sensasi berputar yang kencang. Meningkatnya jeda antar-putaran dari 10 menjadi 50 milidetik akan menghasilkan sensasi berputar yang lebih longgar, dan dapat digunakan untuk menyesuaikan durasi video atau animasi.

Sebaiknya jangan gunakan jeda yang lebih dari 100 milidetik, karena putaran berturut-turut tidak lagi berintegrasi dengan baik dan mulai terasa seperti efek individual.

Berikut contoh bentuk elastis yang memantul kembali setelah ditarik ke bawah lalu dilepaskan. Animasi ditingkatkan dengan sepasang efek putaran, diputar dengan berbagai intensitas yang proporsional dengan perpindahan pantulan.

Animasi bentuk elastis memantul
Plot bentuk gelombang getaran input

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

Contoh: Pantulan (dengan bunyi bunyi)

Penerapan efek getaran lanjutan lainnya adalah untuk menyimulasikan interaksi fisik. PRIMITIVE_THUD dapat menciptakan efek yang kuat dan menggembirakan, yang dapat dipasangkan dengan visualisasi dampak, misalnya dalam video atau animasi, untuk meningkatkan pengalaman secara keseluruhan.

Berikut ini contoh animasi ball drop sederhana yang ditingkatkan dengan efek thud yang diputar setiap kali bola memantul dari bagian bawah layar:

Animasi bola yang dijatuhkan memantul dari bagian bawah layar
Plot bentuk gelombang getaran input

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