Twórz własne efekty haptyczne

Ta strona zawiera przykłady użycia różnych interfejsów API dotyku do tworzenia niestandardowych efektów w aplikacji na Androida. Ponieważ większość informacji na tej stronie opiera się na dobrej wiedzy na temat działania mechanizmu wibracyjnego, zalecamy przeczytanie materiału podstawowego na temat działania mechanizmu wibracji.

Przykłady znajdziesz na tej stronie.

Więcej przykładów znajdziesz w artykule o dodawaniu do zdarzeń reakcji na dotyk. Pamiętaj, aby zawsze przestrzegać zasad projektowania danych haptycznych.

Używanie kreacji zastępczych do obsługi zgodności urządzenia

Implementując dowolny efekt niestandardowy, weź pod uwagę te kwestie:

  • Możliwości urządzenia wymagane do uzyskania efektu
  • Co zrobić, gdy urządzenie nie potrafi odtworzyć efektu

Dokumentacja interfejsu Android haptics API zawiera szczegółowe informacje o tym, jak sprawdzić obsługę komponentów wpływających na reakcję haptyczną, tak aby aplikacja działała w spójny sposób.

W zależności od zastosowania możesz wyłączyć efekty niestandardowe lub udostępnić alternatywne efekty niestandardowe oparte na różnych potencjalnych możliwościach.

Zaplanuj pod kątem tych ogólnych klas funkcji urządzenia:

  • Jeśli korzystasz z podstawowych elementów haptycznych: urządzeń obsługujących te elementy, które są wymagane przez efekty niestandardowe. Szczegółowe informacje o elementach podstawowych znajdziesz w następnej sekcji.

  • Urządzenia z regulacją amplitudy.

  • Urządzenia z podstawową obsługą wibracji (włączone/wyłączone), czyli takie, które nie mają kontroli amplitudy.

Jeśli w przypadku tych kategorii wybrany jest efekt haptyczny aplikacji, jej wrażenia haptyczne powinny być przewidywalne na każdym urządzeniu.

Wykorzystanie elementów podstawowych haptycznych

Android zawiera kilka podstawowych elementów haptycznych, które różnią się amplitudą i częstotliwością. Aby uzyskać bogate efekty haptyczne, możesz użyć samego obiektu podstawowego lub kilku elementów podstawowych w połączeniu.

  • W przypadku widocznych przerw między 2 elementami podstawowymi używaj opóźnień wynoszących co najmniej 50 ms, biorąc pod uwagę czas trwania podstawowych, jeśli to możliwe.
  • Używaj skal różniących się współczynnikiem wynoszącym 1,4 lub wyższym, aby różnica w intensywności była lepiej widoczna.
  • Używaj skal 0,5, 0,7 i 1,0, aby utworzyć wersję obiektu podstawowego o niskiej, średniej i dużej intensywności.

Tworzenie niestandardowych wzorców wibracji

Wzorce wibracji często są wykorzystywane w sygnałach haptycznych, np. w powiadomieniach i dzwonkach. Usługa Vibrator może odtwarzać długie wzorce wibracji, które z czasem zmieniają amplitudę wibracji. Takie efekty nazywamy falami.

Efekty fali są łatwo zauważalne, ale nagłe, długie wibracje mogą wystraszyć użytkownika, jeśli grasz w cichym otoczeniu. Zbyt szybkie osiągnięcie docelowej amplitudy może też powodować brzęczenie. W przypadku projektowania wzorców fal zalecanym jest wygładzanie przejść amplitudy w celu uzyskania efektów w górę i w dół.

Przykład: Wzorzec wzrostu

Fale są przedstawiane jako VibrationEffect z 3 parametrami:

  1. Czasy: tablica czasu trwania w milisekundach dla każdego segmentu fali.
  2. Wzmocnienia: wymagana amplituda wibracji dla każdego czasu trwania określonego w pierwszym argumencie wyrażona w postaci liczby całkowitej od 0 do 255, przy czym 0 oznacza wyłączenie wibracji, a 255 to maksymalną amplitudę urządzenia.
  3. Indeks powtarzania: indeks w tablicy określony w pierwszym argumencie, aby rozpocząć powtarzanie fali, lub -1, jeśli ma odtworzyć wzorzec tylko raz.

Oto przykładowa fala, która pulsuje 2 razy z przerwami 350 ms między kolejnymi uderzeniami. Pierwszy puls to płynny wzrost do maksymalnej amplitudy, a drugi to szybki zjazd, który umożliwia utrzymanie maksymalnej amplitudy. Zakończenie na końcu jest definiowane przez ujemną wartość indeksu powtórzeń.

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

Przykład: powtarzający się wzór

Fale można też odtwarzać wielokrotnie, dopóki nie anulujesz. Aby utworzyć powtarzającą się falę, trzeba ustawić nieujemny parametr „powtarzanie”. Gdy odtwarzasz powtarzającą się falę, wibracje są aktywne, dopóki nie zostaną wyraźnie anulowane w usłudze:

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

Jest to bardzo przydatne w przypadku przerywanych zdarzeń, które wymagają działania użytkownika, aby je potwierdzić. Mogą to być na przykład połączenia przychodzące i alarmy.

Przykład: wzór z wartością zastępczą

Sterowanie amplitudą wibracji jest funkcją zależną od sprzętu. Odtworzenie fali na słabszym urządzeniu bez tej możliwości powoduje wibrację z maksymalną amplitudą przy każdym dodatnim wejściu w tablicy amplitudy. Jeśli Twoja aplikacja musi działać na takich urządzeniach, upewnij się, że wzór nie generuje efektu buczenia podczas odtwarzania w tych warunkach. Nie zapomnij też zaprojektować prostszego wzorca włączania i wyłączania, który można odtworzyć w zastępstwie.

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

Tworzenie kompozycji wibracyjnych

W tej sekcji dowiesz się, jak łączyć je w dłuższe i bardziej złożone efekty niestandardowe, a także poznać więcej zaawansowanych funkcji sprzętowych w tym zakresie. Możesz korzystać z kombinacji efektów o różnej amplitudzie i częstotliwości, aby uzyskać bardziej złożone efekty haptyczne na urządzeniach z elementami sterującymi haptycznymi, które mają większą przepustowość.

Proces tworzenia niestandardowych wzorców wibracji, opisany wcześniej na tej stronie, zawiera objaśnienie, jak kontrolować amplitudę wibracji, aby uzyskać płynny efekt podnoszenia i w dół. Technologia haptyczna udoskonala tę koncepcję, badając szerszy zakres częstotliwości wibracji urządzenia, aby efekt był jeszcze bardziej płynny. Te fale są szczególnie skuteczne w generowaniu efektu crescendo lub diminuendo.

Opisane wcześniej na tej stronie podstawowe elementy kompozycji zostały wdrożone przez producenta urządzenia. Zapewniają wyraźną, krótką i przyjemną wibrację, która jest zgodna z zasadami haptycznymi, zapewniając czytelną reakcję na dotyk. Więcej informacji o tych funkcjach i sposobie ich działania znajdziesz w artykule Programy uruchamiające wibracje.

Android nie udostępnia kreacji zastępczych w przypadku kompozycji z nieobsługiwanymi elementami podstawowymi. Zalecamy wykonanie tych czynności:

  1. Zanim włączysz zaawansowane reakcje haptyczne, sprawdź, czy dane urządzenie obsługuje wszystkie używane przez Ciebie podstawowe funkcje.

  2. Wyłącz spójny zestaw, które nie są obsługiwane, a nie tylko efekty, w których brakuje elementów podstawowych. Więcej informacji o sprawdzaniu obsługi urządzenia znajdziesz poniżej.

Skomponowane efekty wibracyjne możesz tworzyć w narzędziu VibrationEffect.Composition. Oto przykład efektu wolno rosnącego, po którym następuje silny efekt kliknięcia:

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

Kompozycja powstaje przez dodanie podstawowych elementów, które mają być odtwarzane w określonej kolejności. Każdy element podstawowy jest również skalowalny, dzięki czemu możesz kontrolować amplitudę generowanych przez nie wibracji. Skala jest zdefiniowana jako wartość z zakresu od 0 do 1, gdzie 0 odpowiada minimalnej amplitudzie, przy której użytkownik może (prawdopodobnie) odczuć ten element podstawowy.

Jeśli chcesz utworzyć słabą i silną wersję tego samego obiektu podstawowego, zalecamy, aby skala różniła się od wartości 1,4 lub większej, aby różnica w intensywności była dobrze widoczna. Nie staraj się tworzyć więcej niż 3 poziomów intensywności tego samego obiektu podstawowego, ponieważ nie odróżniają się one percepcyjnie. Na przykład użyj skal 0,5, 0,7 i 1,0, aby utworzyć wersję obiektu podstawowego o małej, średniej i dużej intensywności.

Kompozycja może też określać opóźnienia między kolejnymi elementami podstawowymi. Opóźnienie to jest wyrażone w milisekundach od końca poprzedniego obiektu podstawowego. Ogólnie 5–10 ms przerwy między 2 elementami podstawowymi jest zbyt krótkie, aby można ją było wykryć. Jeśli chcesz utworzyć wyraźną przerwę między 2 elementami podstawowymi, rozważ użycie przerwy o długości co najmniej 50 ms. Oto przykład kompozycji z opóźnieniami:

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

Za pomocą tych interfejsów API można sprawdzić, czy urządzenie obsługuje określone podstawowe elementy:

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

Możesz też sprawdzić wiele obiektów podstawowych, a następnie zdecydować, które z nich utworzyć na podstawie poziomu obsługi klienta na danym urządzeniu:

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

Przykład: Resist (z niskim znacznikiem)

Możesz sterować amplitudą tych podstawowych wibracji, aby przekazywać przydatne informacje o toczącym się działaniu. Aby uzyskać płynny efekt crescendo na elementach podstawowych, można używać ściśle dopasowanych wartości skali. Opóźnienie między kolejnymi elementami podstawowymi można też ustawiać dynamicznie w zależności od interakcji użytkownika. Przedstawiliśmy to na przykładzie animacji widoku sterowanej za pomocą gestu przeciągania i uzupełnionej o technologię haptyczną.

Animacja przedstawiająca przeciąganie okręgu w dół
Wykres przedstawiający wejście fali wibracji

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

Przykład: rozwinięcie (ze wzrostem i spadkiem)

Istnieją 2 podstawowe elementy do zwiększania intensywności postrzeganej intensywności wibracji: PRIMITIVE_QUICK_RISE i PRIMITIVE_SLOW_RISE. Oba cele osiągają tę samą wartość docelową, ale z różnym czasem trwania. Jest tylko 1 element podstawowy do zmniejszania: PRIMITIVE_QUICK_FALL. Te podstawowe elementy działają lepiej razem, tworząc segment fali, który rośnie, a następnie zanika. Możesz wyrównać przeskalowane prymitywy, aby zapobiec nagłym skokom amplitudy między nimi. Takie rozwiązanie dobrze się też sprawdza przy wydłużaniu ogólnego czasu trwania efektu. Ludzie zawsze dostrzegają większą część rosnącą niż spadającą, więc skrócenie części opadającej może zwiększyć zainteresowanie tym fragmentem.

Oto przykład użycia tej kompozycji do rozwijania i zwijania okręgu. Efekt wzrostu może wzmocnić uczucie rozwinięcia w trakcie animacji. Połączenie efektów wzniesienia i spadku pomaga uwydatnić zwinięcie na końcu animacji.

Animacja przedstawiająca rozwijany okrąg
Wykres przedstawiający wejście fali wibracji

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

Przykład: Wahadło (ze wibracjami)

Jedną z najważniejszych zasad dotyczących dotyku jest zachwycanie użytkowników. Zabawnym sposobem na wprowadzenie przyjemnego, nieoczekiwanego efektu wibracji jest użycie PRIMITIVE_SPIN. Ten element podstawowy jest najskuteczniejszy, gdy jest wywoływany więcej niż raz. Duża liczba połączonych obrotów może powodować chwieje się i niestabilny efekt, który można dodatkowo wzmocnić przez zastosowanie pewnego losowego skalowania do każdego obiektu podstawowego. Możesz też przeprowadzić eksperyment z odstępem między kolejnymi pierwotnymi spinami. 2 obroty bez żadnych przerw (0 ms) dają wrażenie wirowania. Zwiększenie odstępu między obrotami z 10 do 50 ms daje luźniejsze wrażenie wirowania i może służyć do dopasowywania czasu trwania filmu lub animacji.

Nie zalecamy korzystania z przerwy dłuższej niż 100 ms, ponieważ kolejne obroty nie integrują się już prawidłowo i uzyskują odczucia indywidualne.

Oto przykład kształtu elastycznego, który odbija się po przeciągnięciu i puszczeniu w dół. Animacja jest ulepszana za pomocą pary efektów obrotu, które są odtwarzane z różną intensywnością proporcjonalną do przesunięcia odbijającego się.

Animacja przedstawiająca odbijający się kształt elastyczny
Wykres przedstawiający wejście fali wibracji

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

Przykład: Odrzucenie (z trzaskami)

Innym zaawansowanym zastosowaniem efektów wibracji jest symulacja interakcji fizycznych. PRIMITIVE_THUD może wywoływać silny i pogłosowy efekt, który można połączyć z wizualizacją wpływu, na przykład w filmie lub animacji, aby poprawić ogólne wrażenia.

Oto przykład prostej animacji polegającej na upuszczeniu piłki na podstawie efektu głośnego uderzenia odtwarzany za każdym razem, gdy piłka odbija się od dołu ekranu:

Animacja przedstawiająca upuszczoną piłkę, która odbija się od dołu ekranu
Wykres przedstawiający wejście fali wibracji

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