Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite finden Sie Beispiele für die Verwendung verschiedener Haptik-APIs, um benutzerdefinierte Effekte in einer Android-App zu erstellen. Da viele der Informationen auf dieser Seite auf der Funktionsweise eines Vibrationsauslösers beruhen, empfehlen wir Ihnen, die Einführung zum Vibrationsauslöser zu lesen.

Diese Seite enthält die folgenden Beispiele.

Weitere Beispiele finden Sie unter Haptisches Feedback zu Ereignissen hinzufügen und halten Sie sich an die Designprinzipien für haptisches Feedback.

Gerätekompatibilität mit Fallbacks verwalten

Berücksichtigen Sie bei der Implementierung von benutzerdefinierten Effekten Folgendes:

  • Welche Gerätefunktionen für den Effekt erforderlich sind
  • Was tun, wenn das Gerät den Effekt nicht wiedergeben kann?

In der Referenz zur Android Haptik API finden Sie Details dazu, wie Sie die Unterstützung für Komponenten Ihrer Haptik prüfen können, damit Ihre App eine einheitliche Gesamterfahrung bieten kann.

Je nach Anwendungsfall können Sie benutzerdefinierte Effekte deaktivieren oder alternative benutzerdefinierte Effekte basierend auf verschiedenen potenziellen Funktionen bereitstellen.

Berücksichtigen Sie die folgenden übergeordneten Klassen von Gerätefunktionen:

  • Wenn Sie haptische Primitive verwenden: Geräte, die die von den benutzerdefinierten Effekten benötigten Primitiven unterstützen Details zu Primitiven finden Sie im nächsten Abschnitt.

  • Geräte mit Amplitudenregelung.

  • Geräte mit grundlegender Vibrationsunterstützung (ein/aus), also Geräten, die keine Amplitudensteuerung bieten.

Wenn diese Kategorien aufgrund der Auswahl des haptischen Effekts Ihrer App berücksichtigt werden, sollte die haptische Nutzererfahrung für jedes einzelne Gerät vorhersehbar bleiben.

Haptische Primitive verwenden

Android umfasst mehrere Haptik-Primitive, die sich sowohl in der Amplitude als auch in der Frequenz unterscheiden. Sie können eine Primitive allein oder mehrere Primitive in Kombination verwenden, um umfassende haptische Effekte zu erzielen.

  • Verwenden Sie Verzögerungen von 50 ms oder länger für erkennbare Lücken zwischen zwei Primitiven. Berücksichtigen Sie dabei nach Möglichkeit auch die Primitivdauer.
  • Verwenden Sie Skalen, die sich um ein Verhältnis von 1,4 oder mehr unterscheiden, damit der Intensitätsunterschied besser wahrgenommen wird.
  • Verwenden Sie Skalen von 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitivs zu erstellen.

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig für Aufmerksamkeitshaptik wie Benachrichtigungen und Klingeltöne verwendet. Der Vibrator-Dienst kann lange Vibrationsmuster abspielen, die sich die Vibrationsamplitude mit der Zeit ändern. Solche Effekte werden Wellenformen genannt.

Wellenformeffekte sind leicht wahrnehmbar, aber plötzliche lange Vibrationen können den Nutzer schrecken, wenn sie in einer ruhigen Umgebung gespielt werden. Ein zu schnelles Anschalten auf eine Zielampplitude kann auch hörbare Brummgeräusche verursachen. Beim Entwerfen von Wellenformmustern wird empfohlen, die Amplitudenübergänge zu glätten, um Erhöhungs- und Abwärtseffekte zu erzeugen.

Beispiel: Erhöhung des Prozentsatzes

Wellenformen werden als VibrationEffect mit drei Parametern dargestellt:

  1. Timings:Array der Dauer in Millisekunden für jedes Wellenformsegment.
  2. Amplituden:Die gewünschte Vibrationsamplitude für jede im ersten Argument angegebene Dauer, dargestellt durch einen ganzzahligen Wert zwischen 0 und 255, wobei 0 für „aus“ und 255 die maximale Amplitude des Geräts darstellt.
  3. Wiederholungsindex:Der Index im Array, das im ersten Argument für die Wiederholung der Wellenform angegeben ist, oder -1, wenn das Muster nur einmal wiedergegeben werden soll.

Hier ist ein Beispiel für eine Wellenform, die zweimal mit einer Pause von 350 ms zwischen den Takten pulsiert. Der erste Impuls ist eine gleichmäßige Anlaufzeit bis zur maximalen Amplitude und der zweite Puls ist eine schnelle Rampe, um die maximale Amplitude beizubehalten. Das Anhalten am Ende wird durch den negativen Wiederholungsindexwert definiert.

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

Beispiel: Sich wiederholendes Muster

Wellenformen können auch wiederholt abgespielt werden, bis sie abgebrochen werden. Sie können eine sich wiederholende Wellenform erstellen, indem Sie einen nicht negativen Parameter "Repeat" festlegen. Wenn Sie eine sich wiederholende Wellenform wiedergeben, wird die Vibration so lange fortgesetzt, bis sie im Dienst explizit abgebrochen wird:

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

Dies ist sehr nützlich bei zeitweilig auftretenden Ereignissen, bei denen Nutzer eine Aktion ausführen müssen, um sie zu bestätigen. Beispiele für solche Ereignisse sind eingehende Anrufe und ausgelöste Alarme.

Beispiel: Muster mit Fallback

Die Kontrolle der Amplitude einer Vibration ist eine hardwareabhängige Funktion. Wird eine Wellenform ohne diese Funktion auf einem Low-End-Gerät abgespielt, vibriert dieses für jeden positiven Eintrag im Amplitudenarray mit der maximalen Amplitude. Wenn Ihre App für solche Geräte geeignet ist, sollten Sie darauf achten, dass Ihr Muster beim Spielen unter dieser Bedingung keinen Summeneffekt erzeugt, oder ein einfacheres EIN/AUS-Muster zu entwerfen, das stattdessen als Fallback gespielt werden kann.

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

Vibrationskompositionen erstellen

In diesem Abschnitt werden Möglichkeiten beschrieben, wie Sie diese zu längeren und komplexeren benutzerdefinierten Effekten zusammensetzen können. Darüber hinaus werden erweiterte Haptikfunktionen mit erweiterten Hardwarefunktionen erläutert. Sie können Kombinationen von Effekten verwenden, die Amplitude und Frequenz variieren, um komplexere haptische Effekte auf Geräten mit haptischen Bedienelementen zu erzeugen, die eine größere Frequenzbandbreite haben.

Im oben auf dieser Seite beschriebenen Verfahren zum Erstellen benutzerdefinierter Vibrationsmuster wird erläutert, wie Sie die Vibrationsamplitude steuern können, um sanfte Effekte bei einer Erhöhung und Verringerung zu erzielen. Die umfassende Haptik verbessert dieses Konzept, indem der größere Frequenzbereich der Gerätevibratorfunktion untersucht wird, um den Effekt noch gleichmäßiger zu gestalten. Diese Wellenformen sind besonders effektiv für einen Crescendo- oder Diminuendo-Effekt.

Die Zusammensetzungsprimitive, die weiter oben auf dieser Seite beschrieben werden, werden vom Gerätehersteller implementiert. Sie sorgen für eine kristallklare, kurze und angenehme Vibration, die den Haptikprinzipien für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie hier.

Android bietet keine Fallbacks für Kompositionen mit nicht unterstützten Primitiven. Wir empfehlen Ihnen, die folgenden Schritte auszuführen:

  1. Bevor Sie die erweiterte Haptik aktivieren, prüfen Sie, ob ein bestimmtes Gerät alle verwendeten Primitive unterstützt.

  2. Deaktivieren Sie die konsistenten Tests, die nicht unterstützt werden, und nicht nur die Auswirkungen, denen eine Primitive fehlt. Weitere Informationen zur Überprüfung der Geräteunterstützung findest du im Folgenden.

Mit VibrationEffect.Composition können Sie zusammengesetzte Vibrationseffekte erstellen. Hier ein Beispiel für einen langsam ansteigenden Effekt gefolgt von einem scharfen Klickeffekt:

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

Eine Komposition wird durch Hinzufügen von Primitiven erstellt, die nacheinander abgespielt werden sollen. Jedes Primitiv ist außerdem skalierbar, sodass Sie die Amplitude der von ihnen erzeugten Vibration steuern können. Die Skala ist als Wert zwischen 0 und 1 definiert, wobei 0 einer minimalen Amplitude zugeordnet wird, bei der dieses Primitiv vom Nutzer (kaum) wahrnehmbar ist.

Wenn Sie eine schwache und starke Version desselben Primitivs erstellen möchten, sollten sich die Skalen um ein Verhältnis von 1,4 oder mehr unterscheiden, damit der Intensitätsunterschied leicht zu erkennen ist. Versuchen Sie nicht mehr als drei Intensitätsstufen desselben Primitivs zu erzeugen, da sie nicht wahrnehmbar unterschiedlich sind. Verwenden Sie beispielsweise Skalen von 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitivs zu erstellen.

Die Zusammensetzung kann auch Verzögerungen angeben, die zwischen aufeinanderfolgenden Primitiven hinzugefügt werden sollen. Diese Verzögerung wird in Millisekunden seit dem Ende der vorherigen Primitive angegeben. Im Allgemeinen ist eine Lücke von 5 bis 10 ms zwischen zwei Primitiven zu kurz, um erkannt zu werden. Verwenden Sie eine Lücke in einer Größe von mindestens 50 ms, wenn Sie eine erkennbare Lücke zwischen zwei Primitiven erzeugen möchten. Hier ein Beispiel für eine Komposition mit Verzögerungen:

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

Mit den folgenden APIs können Sie die Geräteunterstützung für bestimmte Primitive prüfen:

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

Es ist auch möglich, mehrere Primitive zu prüfen und dann je nach Geräteunterstützungsstufe zu entscheiden, welche erstellt werden sollen:

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

Beispiel: Widerstand (mit niedrigen Zecken)

Sie können die Amplitude der primitiven Vibration steuern, um einer laufenden Aktion nützliches Feedback zu geben. Eng liegende Skalenwerte können verwendet werden, um einen gleichmäßigen Crescendo-Effekt eines Primitivs zu erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch anhand der Nutzerinteraktion festgelegt werden. Dies wird im folgenden Beispiel einer Ansichtsanimation veranschaulicht, die durch eine Ziehgeste gesteuert und mit Haptik erweitert wird.

Animation eines Kreises, der nach unten gezogen wird
Darstellung der Vibrationseingangswellenform

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

Beispiel: Maximieren (mit Erhöhung und Fall)

Es gibt zwei Primitive zum Erhöhen der wahrgenommenen Vibrationsintensität: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, nur mit unterschiedlicher Dauer. Es gibt nur eine einfache Methode zur Verringerung: PRIMITIVE_QUICK_FALL. Diese Primitive arbeiten besser zusammen, um ein Wellenformsegment zu erzeugen, das in der Intensität zunimmt und dann abfällt. Sie können skalierte Primitive ausrichten, um plötzliche Amplitudenanschläge zwischen ihnen zu vermeiden, was sich auch gut zur Verlängerung der Gesamteffektdauer eignet. Wahrgenommen wird der ansteigende Teil immer mehr als der abnehmende. Wenn der ansteigende Teil kürzer als der abnehmende Teil ist, kann die Betonung in Richtung des abnehmenden Teils verlagert werden.

Hier ist ein Beispiel für eine Anwendung dieser Zusammensetzung zum Maximieren und Minimieren eines Kreises. Der Anstiegseffekt kann das Gefühl einer Ausweitung während der Animation verstärken. Die Kombination aus Anstiegs- und Abfalleffekten trägt dazu bei, die Minimierung am Ende der Animation zu betonen.

Animation eines sich erweiternden Kreises
Darstellung der Vibrationseingangswellenform

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

Beispiel: Wackeln (mit Drehungen)

Eines der zentralen Haptikprinzipien besteht darin, Nutzer zu begeistern. Mit PRIMITIVE_SPIN lässt sich ein angenehmer unerwarteter Vibrationseffekt erzielen. Diese Primitive ist am effektivsten, wenn sie mehr als einmal aufgerufen wird. Mehrere verkettete Spins können zu Wackeln und instabilen Effekten führen, die durch die Anwendung einer zufälligen Skalierung auf jedes Primitiv weiter verbessert werden können. Sie können auch mit der Lücke zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Drehungen ohne Pause (0 ms dazwischen) erzeugen ein Gefühl, das sich genau dreht. Das Erhöhen des Abstands zwischen den Drehungen von 10 auf 50 ms führt zu einer weniger schnellen Bewegung und kann verwendet werden, um die Dauer eines Videos oder einer Animation anzupassen.

Wir raten davon ab, eine Lücke von mehr als 100 ms zu verwenden, da die aufeinanderfolgenden Drehungen nicht mehr gut integriert sind und sich wie einzelne Effekte anfühlen.

Hier ist ein Beispiel für eine elastische Form, die nach unten gezogen und dann wieder freigegeben wird. Die Animation wird mit zwei Spin-Effekten optimiert, die mit unterschiedlicher Intensität abgespielt werden, die proportional zur Bounce-Verlagerung sind.

Animation einer hüpfenden elastischen Form
Darstellung der Vibrationseingangswellenform

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

Beispiel: Sprung (mit Stößen)

Eine weitere Anwendung von Vibrationseffekten ist die Simulation physischer Interaktionen. Der PRIMITIVE_THUD kann einen starken Nachschlageeffekt erzeugen, der mit der Visualisierung einer Wirkung, z. B. in einem Video oder einer Animation, kombiniert werden kann, um das Gesamterlebnis zu verstärken.

Hier ist ein Beispiel für eine einfache Kugelanimation, die jedes Mal mit einem Thud-Effekt optimiert wird, wenn der Ball vom unteren Rand des Bildschirms springt:

Animation einer heruntergefallenen Kugel, die vom unteren Rand des Bildschirms springt
Darstellung der Vibrationseingangswellenform

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