Twórz własne efekty haptyczne

Z tej strony dowiesz się, jak korzystać z różnych interfejsów API haptycznych, by tworzyć niestandardowe efekty w aplikacjach na Androida. Większość informacji na temat ta strona opiera się na dobrej znajomości działania mechanizmu wibracyjnego, zalecamy przeczytanie artykułu Wprowadzenie do mechanizmu wibracyjnego.

Na tej stronie znajdują się następujące przykłady.

Więcej przykładów znajdziesz w sekcji Dodawanie reakcji haptycznych do zdarzeń. muszą być zawsze zgodne z zasadami projektowania haptycznego.

Obsługa kreacji zastępczych w celu obsługi zgodności urządzeń

Wdrażając efekty niestandardowe, weź pod uwagę te kwestie:

  • Jakie funkcje urządzenia są wymagane do uzyskania efektu
  • Co zrobić, gdy urządzenie nie może odtworzyć efektu

Dokumentacja interfejsu haptics API na Androidzie zawiera szczegółowe informacje o tym, jak sprawdzić obsługa komponentów wchodzących w skład czujnika haptycznego. Dzięki temu aplikacja może i spójny interfejs.

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

Zaplanuj konfigurację pod kątem tych ogólnych klas możliwości urządzeń:

  • Jeśli używasz elementów głównych z obsługą dotykową: urządzenia, które je obsługują. potrzebne efekty niestandardowe. (W następnej sekcji znajdziesz szczegółowe informacje na temat elementów podstawowych).

  • Urządzenia z regulacją amplitudy.

  • urządzeń z podstawową obsługą wibracji (włączoną/wyłączoną), których nie da się kontrolować amplitudy.

Jeśli wybór efektu haptycznego aplikacji uwzględnia te kategorie, wrażenia haptyczne użytkownika powinny być przewidywalne w przypadku każdego urządzenia.

Zastosowanie elementów haptycznych

Android ma kilka elementów głównych haptycznych, które różnią się amplitudą i i częstotliwości. Możesz używać jednego elementu podstawowego oddzielnie lub kilku obiektów podstawowych w połączeniu aby uzyskać mocne efekty haptyczne.

  • Użyj opóźnień co najmniej 50 ms w celu uzyskania wyraźnych przerw między 2 sekundami podstawowe, uwzględniając też funkcję podstawowa czas trwania jeśli to możliwe.
  • Używaj skal, które różnią się współczynnikiem wynoszącym co najmniej 1,4, aby różnica w jest lepiej zauważalna.
  • Użyj skal 0,5, 0,7 i 1,0, aby ocenić niską, średnią i wysoką i wersji intensywności obiektu podstawowego.

Twórz własne wzory wibracji

Wzorce wibracji są często wykorzystywane w inteligentnych czujnikach haptycznych, np. w powiadomieniach i dzwonki. Usługa Vibrator może odtwarzać długie wzorce wibracji, które zmieniać amplitudę wibracji w czasie. Efekty te są nazywane falami.

Efekty fali mogą być łatwo zauważalne, ale nagłe, długie wibracje mogą zaskoczenie użytkownika przy odtwarzaniu w cichym otoczeniu. Zmniejszenie do docelowej amplitudy Zbyt szybko może też być słychać słyszalne buczenie. Zalecenie dla projektowanie wzorców fali ma na celu wygładzenie przejść amplitudy w celu utworzenia efekty zwiększania i zmniejszania.

Przykład: Wzorzec wyświetlania

Fale są reprezentowane jako VibrationEffect z 3 parametrami:

  1. Czasy: tablica czasu trwania każdej fali (w milisekundach) segmentację.
  2. Amplitudy: pożądana amplituda wibracji dla każdego określonego czasu trwania w pierwszym argumencie reprezentowany przez liczbę całkowitą od 0 do 255, przy czym 0 symbolizujący wyłączenie wibracji a 255 to maksymalna liczba znaków w urządzeniu. amplituda.
  3. Powtórz indeks: indeks w tablicy określonej w pierwszym argumencie do rozpocząć powtarzanie fali lub wartość -1, jeśli ma ona odtworzyć falę tylko raz.

Oto przykład fali, która miga dwukrotnie z przerwami wynoszącymi 350 ms pulsować. Pierwszy puls jest płynny, aż do maksymalnej amplitudy, a sekunda to szybka rampa do utrzymania maksymalnej amplitudy. Zatrzymanie na końcu jest zdefiniowane przez wartość indeksu powtarzania ujemnym.

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ć w taki sposób, dopóki nie zostaną anulowane. Sposób tworzenia powtarzanie fali oznacza ustawienie nieujemnego parametru „powtarzania”. Gdy zagrasz w będzie się powtarzał, dopóki nie zostanie wyraźnie anulowany usługa:

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 przejściowych zdarzeń, które wymagają działania użytkownika, aby potwierdzić. Mogą to być na przykład przychodzące rozmowy telefoniczne oraz uruchomiono alarmy.

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

Sterowanie amplitudą drgań jest funkcje zależne od sprzętu. Odtwarzanie fali na słabsze urządzenie bez tej funkcji powoduje, że wibruje z maksymalną wibracją. amplituda dla każdego dodatniego wpisu w tablicy amplitudy. Jeśli aplikacja musi do takich urządzeń, należy upewnić się, nie generuje brzęczenia przy odtwarzaniu w takich warunkach lub zaprojektować prostszy wzorzec włączania/wyłączania, który można odtworzyć jako kreację zastępczą.

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

Twórz kompozycje wibracyjne

W tej sekcji przedstawiamy sposoby ich tworzenia bardziej skomplikowane i złożone efekty niestandardowe. czujnik haptyczny wykorzystujący bardziej zaawansowane możliwości sprzętowe. Możesz użyć kombinacji atrybutów efekty o różnej amplitudzie i częstotliwości, co pozwala uzyskać bardziej złożone efekty haptyczne. na urządzeniach z czujnikami haptycznymi o szerszej przepustowości.

Proces tworzenia niestandardowych wibracji omówionych wcześniej na tej stronie wyjaśnia, jak kontrolować amplitudę wibracji, aby uzyskać płynne efekty w górę i w dół. Ulepszona funkcja haptyczna ulepsza tę koncepcję, analizując poszerz zakres częstotliwości wibracji, aby efekt był jeszcze gładszy. Fale te są szczególnie skuteczne przy tworzeniu crescendo lub diminuendo efektu.

Opisane wcześniej na tej stronie elementy podstawowe są zaimplementowane przez od producenta urządzenia. Zapewniają wyraźne, krótkie i przyjemne wibracje który jest zgodny z zasadami haptyki zapewniającymi wyraźny sygnał haptyczny. Więcej szczegółowe informacje na temat tych funkcji i sposobu ich działania można znaleźć w artykule Mechanizmy wibracyjne Primer.

Android nie udostępnia kreacji zastępczych w przypadku kompozycji, które nie są obsługiwane elementów podstawowych. Zalecamy wykonanie tych czynności:

  1. Zanim aktywujesz zaawansowane czujniki haptyczne, sprawdź, czy urządzenie obsługuje wszystkich używanych elementów podstawowych.

  2. Wyłącz ten spójny zestaw, które nie są obsługiwane, a nie tylko w których brakuje elementu podstawowego. Więcej informacji o sprawdzaniu obsługi urządzenia jest przedstawiona w następujący sposób.

Możesz tworzyć różne efekty wibracji w VibrationEffect.Composition. Oto przykład efektu powoli rosnącego z efektem ostrego 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());

Kompozycję tworzy się przez dodanie elementów podstawowych, które mają być odtwarzane w sekwencji. Każdy Element podstawowy jest również skalowalny, dzięki czemu można kontrolować amplitudę wibracji generowane przez każdą z nich. Skala jest zdefiniowana jako wartość z zakresu od 0 do 1, gdzie 0 faktycznie odpowiada minimalnej amplitudzie, przy której ten element podstawowy może być odczuć użytkownika.

Jeśli chcesz utworzyć słabą i mocną wersję tego samego obiektu podstawowego, zalecana, aby skale różniły się współczynnikiem co najmniej 1,4, aby różnica może być łatwo zauważona. Nie próbuj tworzyć więcej niż trzech poziomów intensywności tego samego elementu podstawowego, ponieważ nie są one do różnych celów. Na przykład użyj skal 0,5, 0,7 i 1,0, aby stworzyć niską, średnią wartość, i o wysokiej intensywności konstrukcji obiektu podstawowego.

Kompozycja może również określać opóźnienia, które mają być dodawane między kolejnymi elementów podstawowych. Opóźnienie jest wyrażone w milisekundach od zakończenia do poprzedniego elementu podstawowego. Ogólnie przerwa 5–10 ms między 2 podstawowymi elementami też jest zbyt duża są krótkie, aby można je było wykryć. Rozważ użycie przerwy mierzącej co najmniej 50 ms. gdy chcemy stworzyć zauważalną lukę między 2 elementami podstawowymi. Oto przykład kompozycji z opóźnieniem:

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ą poniższych interfejsów API można zweryfikować obsługę określonych urządzeń elementy podstawowe:

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ż zaznaczyć kilka elementów podstawowych i zdecydować, które z nich tworzenie wiadomości w zależności od poziomu obsługi urządzeń:

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 (niskie kreski)

Możesz kontrolować amplitudę wibracji podstawowych, które mają przekazywać przydatne informacje zwrotne dotyczące trwającego działania. Ściśle rozmieszczone wartości skali jest używany do uzyskania gładkiego efektu crescendo obiektu podstawowego. Opóźnienie między kolejne elementy podstawowe można też ustawić dynamicznie na podstawie nazwy użytkownika interakcji. Zostało to pokazane w poniższym przykładzie animacji widoku jest sterowana gestem przeciągania i włączona za pomocą reakcji haptycznych.

Animacja przeciągniętego okręgu w dół
Wykres fali wibracji wejściowych

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 elementy podstawowe pozwalające zwiększyć intensywność wibracji: PRIMITIVE_QUICK_RISE. oraz PRIMITIVE_SLOW_RISE Oba rodzaje są kierowane do tego samego celu, ale z różnym czasem trwania. Jest tylko jedna dla łagodzenia różnic, PRIMITIVE_QUICK_FALL Te elementy podstawowe współdziałają lepiej przy tworzeniu segmentu fali, który rośnie w i umrze. Możesz wyrównać skalowane elementy podstawowe, aby zapobiec nagłym przeskakuje w amplitudzie między nimi, co również dobrze sprawdza się jako poszerzenie czas trwania efektu. Zauważamy, że użytkownicy zawsze częściej obserwują rosnącą część część opadającą, więc krótsza niż część rosnąca można wykorzystać do przeniesienia podkreślenia na część „opadającą”.

Oto przykład zastosowania tej kompozycji do rozwijania i rozwijania, zwijania okręgu. Efekt wzrostu może wzmocnić wrażenie rozszerzania w trakcie animację. Połączenie efektów wschodu i spadu pomaga podkreślić zwijania się na końcu animacji.

Animacja rozwijającego się okręgu
Wykres fali wibracji wejściowych

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 (z obrotami)

Jedną z kluczowych zasad haptycznych jest sprawianie wrażenia użytkowników. Zabawny sposób w celu uzyskania przyjemnego, nieoczekiwanego efektu wibracji jest użycie PRIMITIVE_SPIN Ten element podstawowy jest najskuteczniejszy, gdy jest wywoływany więcej niż raz. Wiele obroty scalone mogą powodować chwienie się i niestabilne działanie, które może być dodatkowo ulepszona przez zastosowanie do każdego podstawowego skalowania nieco losowego. Ty Możesz też eksperymentować z luką między kolejnymi liczbami argumentów podstawowych. 2 obroty bez przerwy (pomiędzy 0 ms) wywołuje uczucie wirowania. Rosnący przerwa między wirowaniami między 10 a 50 ms zapewnia luźniejsze wirowanie. można użyć, aby dopasować czas trwania filmu lub animacji.

Nie zalecamy korzystania z przerwy dłuższej niż 100 ms, ponieważ nie będzie już dobrze integrować się z grą i zacznie się odtwarzać jak indywidualne efekty.

Oto przykład elastycznego kształtu, który odbija się po przeciągnięciu w dół a potem zwolnił. Animacja jest wzbogacona o 2 efekty obrotowe, o zróżnicowanej intensywności proporcjonalnej do przemieszczenia po odbiciu.

Animacja odbijającego się kształtu elastycznego
Wykres fali wibracji wejściowych

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: Podskok (z głośnością)

Innym zaawansowanym zastosowaniem efektów wibracji jest symulowanie interakcje. PRIMITIVE_THUD mogą stworzyć mocny i pogłosowy efekt, który można połączyć wizualizacji efektu, np. w filmie lub animacji, aby wzbogacić ogólne wrażenia.

Oto przykład prostej animacji upuszczania piłki wzbogaconej o efekt głośnego uderzenia odtwarzane za każdym razem, gdy piłka odbija się od dolnej krawędzi ekranu:

Animacja przedstawiająca upuszczoną piłkę odbijającą się z dołu ekranu
Wykres fali wibracji wejściowych

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