Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite finden Sie Beispiele dafür, wie Sie mit verschiedenen Haptik-APIs benutzerdefinierte Effekte in einer Android-Anwendung erstellen. Da viele der Informationen auf dieser Seite auf einem guten Verständnis der Funktionsweise eines Vibrationsaktors beruhen, empfehlen wir Ihnen, den Einstieg in Vibrationsaktoren zu lesen.

Auf dieser Seite finden Sie die folgenden Beispiele.

Weitere Beispiele finden Sie unter Ereignissen haptisches Feedback hinzufügen. Beachten Sie außerdem immer die Designprinzipien für Haptik.

Fallbacks zur Gerätekompatibilität verwenden

Beachten Sie bei der Implementierung benutzerdefinierter Effekte Folgendes:

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

In der Referenz für die Android-Haptik API erfahren Sie, wie Sie die Unterstützung für Komponenten prüfen, die für die Haptik Ihrer App relevant sind, damit sie eine einheitliche Nutzererfahrung bietet.

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

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

  • Bei Verwendung von haptischen Primitiven: Geräte, die die für die benutzerdefinierten Effekte erforderlichen Primitives unterstützen. Weitere Informationen zu Primitiven finden Sie im nächsten Abschnitt.

  • Geräte mit Amplitudenregelung

  • Geräte mit grundlegender Vibrationsunterstützung (An/Aus), d. h. ohne Amplitudenregelung.

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

Haptische Primitive verwenden

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

  • Verwenden Sie Verzögerungen von mindestens 50 ms für erkennbare Lücken zwischen zwei Elementen. Berücksichtigen Sie dabei nach Möglichkeit auch die Dauer des Elements.
  • Verwenden Sie Skalen, die sich um ein Verhältnis von mindestens 1,4 unterscheiden, damit der Unterschied in der Intensität besser wahrgenommen wird.
  • Verwenden Sie die Skalen 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitives zu erstellen.

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig für haptische Warnungen verwendet, z. B. für Benachrichtigungen und Klingeltöne. Der Dienst Vibrator kann lange Vibrationsmuster abspielen, bei denen sich die Vibrationsamplitude im Laufe der Zeit ändert. Solche Effekte werden als Wellenformen bezeichnet.

Wellenformeffekte sind leicht wahrnehmbar, aber plötzliche, lange Vibrationen können Nutzer erschrecken, wenn sie in einer ruhigen Umgebung abgespielt werden. Wenn die Zielamplitude zu schnell erreicht wird, kann es auch zu hörbaren Brummgeräuschen kommen. Beim Entwerfen von Wellenformmustern wird empfohlen, die Amplitudenübergänge zu glätten, um Ein- und Ausblendeffekte zu erzeugen.

Beispiel: Steigerungsmuster

Wellenformen werden als VibrationEffect mit drei Parametern dargestellt:

  1. Timings:ein Array mit Dauern in Millisekunden für jedes Wellenformsegment.
  2. Amplituden:Die gewünschte Vibrationsamplitude für jede im ersten Argument angegebene Dauer, dargestellt als Ganzzahl zwischen 0 und 255. Dabei steht 0 für „Vibrator aus“ und 255 für die maximale Amplitude des Geräts.
  3. Wiederholindex:Der Index im im ersten Argument angegebenen Array, ab dem die Wellenform wiederholt werden soll, oder -1, wenn das Muster nur einmal wiedergegeben werden soll.

Hier ist eine Beispielwellenform, die zweimal mit einer Pause von 350 ms zwischen den Impulsen pulsiert. Der erste Impuls ist eine glatte Steigerung bis zur maximalen Amplitude und der zweite ist eine schnelle Steigerung, um die maximale Amplitude aufrechtzuerhalten. Das Ende wird durch den negativen Wert des Wiederholungsindexes 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

Sie können auch wiederholt abgespielt werden, bis sie abgebrochen werden. Um eine sich wiederholende Wellenform zu erstellen, müssen Sie einen nicht negativen Parameter „repeat“ festlegen. Wenn Sie eine sich wiederholende Wellenform abspielen, 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();
}

Das ist sehr nützlich für gelegentliche Ereignisse, die eine Nutzeraktion erfordern, um sie zu bestätigen. Beispiele für solche Ereignisse sind eingehende Anrufe und ausgelöste Wecker.

Beispiel: Muster mit Fallback

Die Steuerung der Amplitude einer Vibration ist eine hardwareabhängige Funktion. Wenn Sie eine Wellenform auf einem Low-End-Gerät ohne diese Funktion abspielen, vibriert es für jeden positiven Eintrag im Amplitudenarray mit der maximalen Amplitude. Wenn Ihre App solche Geräte unterstützen muss, sollten Sie darauf achten, dass Ihr Muster in diesem Zustand keinen Brummeffekt erzeugt, oder ein einfacheres EIN/AUS-Muster entwerfen, das stattdessen als Fallback wiedergegeben 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 erfahren Sie, wie Sie sie zu längeren und komplexeren benutzerdefinierten Effekten zusammenstellen. Außerdem werden erweiterte Hardwarefunktionen für reichhaltige Haptik vorgestellt. Sie können Kombinationen von Effekten verwenden, die Amplitude und Frequenz variieren, um komplexere haptische Effekte auf Geräten mit haptischen Aktuatoren mit einer breiteren Frequenzbandbreite zu erzeugen.

Im Abschnitt zum Erstellen benutzerdefinierter Vibrationsmuster auf dieser Seite wird beschrieben, wie Sie die Vibrationsamplitude steuern, um eine gleichmäßige Steigerung und Abnahme der Vibration zu erzielen. Die Funktion „Rich Haptics“ verbessert dieses Konzept, indem der breitere Frequenzbereich des Vibrationsmotors genutzt wird, um den Effekt noch weicher zu machen. Diese Wellenformen eignen sich besonders gut, um einen Crescendo- oder Diminuendo-Effekt zu erzeugen.

Die Kompositionsgrundlagen, die weiter oben auf dieser Seite beschrieben wurden, werden vom Gerätehersteller implementiert. Sie bieten eine knackige, kurze und angenehme Vibration, die den Haptikprinzipien für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie im Einführungsleitfaden für Vibrationsaktoren.

Android bietet keine Fallbacks für Kompositionen mit nicht unterstützten Primitiven. Wir empfehlen Folgendes:

  1. Bevor Sie die erweiterten Haptikfunktionen aktivieren, prüfen Sie, ob ein bestimmtes Gerät alle von Ihnen verwendeten Primitive unterstützt.

  2. Deaktivieren Sie alle nicht unterstützten Funktionen, nicht nur die Effekte, für die ein Primitive fehlt. Weitere Informationen dazu, wie du die Unterstützung deines Geräts prüfen kannst, findest du unten.

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 erstellt, indem Primitive hinzugefügt werden, die in einer bestimmten Reihenfolge abgespielt werden sollen. Jede Primitive ist außerdem skalierbar, sodass Sie die Amplitude der von ihnen erzeugten Vibration steuern können. Die Skala wird als Wert zwischen 0 und 1 definiert, wobei 0 einer minimalen Amplitude entspricht, bei der dieses Element vom Nutzer (kaum) wahrgenommen werden kann.

Wenn Sie eine schwache und eine starke Version desselben Primitives erstellen möchten, sollten sich die Skalen im Verhältnis von mindestens 1,4 unterscheiden, damit der Unterschied in der Intensität leicht wahrgenommen werden kann. Versuchen Sie nicht, mehr als drei Intensitätsstufen desselben Primitives zu erstellen, da sie nicht wahrnehmbar voneinander zu unterscheiden sind. Verwenden Sie beispielsweise die Skalen 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitives zu erstellen.

In der Komposition können auch Verzögerungen zwischen aufeinanderfolgenden Primitiven angegeben werden. Diese Verzögerung wird in Millisekunden seit dem Ende des vorherigen Primitives 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 von etwa 50 ms oder länger, wenn Sie zwischen zwei Elementen eine deutlich sichtbare Lücke schaffen 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 basierend auf der Geräteunterstützungsebene zu entscheiden, welche davon 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 wenigen Zählern)

Sie können die Amplitude der primitiven Vibration steuern, um nützliches Feedback zu einer laufenden Aktion zu geben. Mit eng beieinander liegenden Skalenwerten lässt sich ein glatter Crescendo-Effekt eines Primitives erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch basierend auf der Nutzerinteraktion festgelegt werden. Das folgende Beispiel einer Ansichtsanimation, die durch eine Ziegesten gesteuert und mit Haptik ergänzt wird, veranschaulicht dies.

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

Abbildung 1: Diese Wellenform stellt die Ausgabebeschleunigung der Vibration auf einem Gerät dar.

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 Steigerung und Abfall)

Es gibt zwei Primitive, mit denen die wahrgenommene Vibrationsintensität erhöht werden kann: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, aber mit unterschiedlicher Dauer. Es gibt nur ein Primitive für die Abnahme, das PRIMITIVE_QUICK_FALL. Diese Primitive eignen sich gut, um ein Wellenformsegment zu erstellen, das an Intensität zunimmt und dann wieder abnimmt. Sie können skalierte Primitive ausrichten, um plötzliche Amplitudensprünge zwischen ihnen zu vermeiden. Das eignet sich auch gut, um die Gesamtdauer des Effekts zu verlängern. Menschen nehmen den steigenden Teil immer stärker wahr als den fallenden Teil. Wenn Sie den steigenden Teil kürzer als den fallenden Teil gestalten, können Sie den Schwerpunkt auf den fallenden Teil verlagern.

Hier sehen Sie ein Beispiel für die Anwendung dieser Komposition zum Maximieren und Minimieren eines Kreises. Der Aufwärtseffekt kann das Gefühl der Ausdehnung während der Animation verstärken. Die Kombination aus Auf- und Abstiegseffekten unterstreicht das Zusammenfallen am Ende der Animation.

Animation eines sich ausdehnenden Kreises
Darstellung der Wellenform der Eingabevibration

Abbildung 2: Diese Wellenform stellt die Ausgabebeschleunigung der Vibration auf einem Gerät dar.

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: Wobble (mit Drehungen)

Eines der wichtigsten Haptikprinzipien besteht darin, die Nutzer zu begeistern. Mit der PRIMITIVE_SPIN können Sie einen angenehmen, unerwarteten Vibrationseffekt einfügen. Dieses Primitive ist am effektivsten, wenn es mehrmals aufgerufen wird. Mehrere zusammengesetzte Drehungen können einen wackeligen und instabilen Effekt erzeugen, der durch eine etwas zufällige Skalierung der einzelnen Primitivformen noch verstärkt werden kann. Sie können auch mit dem Abstand zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Umdrehungen ohne Lücke (0 ms dazwischen) sorgen für ein flüssiges Drehgefühl. Wenn Sie die Pause zwischen den einzelnen Umdrehungen von 10 auf 50 ms erhöhen, wirkt sich das auf die Drehung aus. Sie können die Pause auch so einstellen, dass sie der Dauer eines Videos oder einer Animation entspricht.

Wir empfehlen, keine Lücke von mehr als 100 ms zu verwenden, da sich die aufeinanderfolgenden Drehungen sonst nicht gut ineinanderfügen und wie einzelne Effekte wirken.

Hier ist ein Beispiel für eine elastische Form, die nach dem Ziehen nach unten wieder in ihre Ausgangsposition zurückkehrt. Die Animation wird durch zwei Dreheffekte ergänzt, die mit unterschiedlicher Intensität abgespielt werden, die der Sprungverschiebung proportional ist.

Animation einer elastischen Form, die hüpft
Darstellung der Wellenform der Eingabevibration

Abbildung 3: Diese Wellenform stellt die Ausgabebeschleunigung der Vibration auf einem Gerät dar.

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: Nachhall (mit dumpfem Geräusch)

Eine weitere erweiterte Anwendung von Vibrationseffekten ist die Simulation physischer Interaktionen. Die PRIMITIVE_THUD kann einen starken und nachhallenden Effekt erzeugen, der mit der Visualisierung eines Aufpralls kombiniert werden kann, z. B. in einem Video oder einer Animation, um den Gesamteindruck zu verstärken.

Hier ist ein Beispiel für eine einfache Animation, bei der ein Ball nach unten fällt und jedes Mal ein dumpfer Schlag ertönt, wenn der Ball auf den Boden des Bildschirms aufprallt:

Animation einer fallengelassenen Kugel, die vom unteren Bildschirmrand abprallt
Darstellung der Wellenform der Eingabevibration

Abbildung 4: Diese Wellenform stellt die Ausgabebeschleunigung der Vibration auf einem Gerät dar.

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