Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite wird anhand von Beispielen beschrieben, wie Sie mit verschiedenen Haptik-APIs um benutzerdefinierte Effekte in einer Android-App zu erstellen. Viele der Informationen auf der dass diese Seite auf gute Kenntnisse über die Funktionsweise des Vibrationsantriebs empfehlen wir, die Einführung zum Vibrationsaktuator zu lesen.

Diese Seite enthält die folgenden Beispiele.

Weitere Beispiele finden Sie unter Haptisches Feedback zu Ereignissen hinzufügen. befolgen Sie immer die Haptik-Designprinzipien.

Mit Fallbacks die Gerätekompatibilität gewährleisten

Beachten Sie bei der Implementierung eines benutzerdefinierten Effekts Folgendes:

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

Die Android haptics API-Referenz enthält Details dazu, wie Sie prüfen können, ob Unterstützung für haptische Komponenten, damit Ihre App einheitliches Gesamterlebnis.

Je nach Anwendungsfall solltest du benutzerdefinierte Effekte deaktivieren stellen alternative benutzerdefinierte Effekte bereit, die auf den unterschiedlichen Möglichkeiten basieren.

Planen Sie die folgenden übergeordneten Klassen von Gerätefunktionen ein:

  • Wenn Sie haptische Primitive verwenden: Geräte, die diese Primitive unterstützen benutzerdefinierten Effekten benötigt. (Im nächsten Abschnitt finden Sie Primitiven.)

  • Geräte mit Amplitudensteuerung.

  • Geräte mit einfacher Vibrationsunterstützung (ein/aus), also solche, fehlende Amplitudensteuerung.

Wenn diese Kategorien bei der Auswahl des haptischen Effekts Ihrer App sollte die haptische User Experience für jedes Gerät vorhersehbar bleiben.

Verwendung von haptischen Primitiven

Android enthält mehrere haptische Primitive, die sich sowohl in der Amplitude als auch Häufigkeit. Sie können allein eine Primitive oder mehrere Primitive in Kombination verwenden um haptische Effekte zu erzielen.

  • Verwenden Sie Verzögerungen von mindestens 50 ms für erkennbare Lücken zwischen zwei Primitiven unter Berücksichtigung der Primitiven Dauer wenn möglich.
  • Verwenden Sie Skalen, die sich um ein Verhältnis von 1,4 oder mehr unterscheiden, sodass die Differenz in die Intensität besser wahrnehmen.
  • Verwenden Sie Skalen von 0,5, 0,7 und 1,0, um eine niedrige, mittlere und hohe Intensitätsversion einer Primitive.

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig bei der haptischen Aufmerksamkeit, z. B. bei Benachrichtigungen, verwendet und Klingeltöne. Der Vibrator-Dienst kann lange Vibrationsmuster abspielen, die Amplitude der Vibration im Laufe der Zeit ändern. Solche Effekte werden als Wellenformen bezeichnet.

Wellenformeffekte sind leicht wahrnehmbar, plötzliche lange Vibrationen können jedoch den Nutzer erschrecken, wenn er in einer ruhigen Umgebung gespielt wird. Ausweitung auf eine Zielamplitude zu schnell werden, könnte dies ebenfalls hörbare Brummgeräusche verursachen. Die Empfehlung für Wellenformmuster haben, besteht darin, die Amplitudenübergänge zu glätten, die Erhöhung bzw. Senkung der Effekte.

Beispiel: Erhöhungsmuster

Wellenformen werden als VibrationEffect mit drei Parametern dargestellt:

  1. Timings:Ein Array von Dauer in Millisekunden für jede Wellenform Segment.
  2. Amplituden:die gewünschte Vibrations Amplitude für die angegebene Dauer im ersten Argument, dargestellt durch einen ganzzahligen Wert zwischen 0 und 255, mit 0 „Aus“ steht für „Vibrator“ und 255 ist die maximale Amplitude
  3. Wiederholungsindex: Index im Array, der im ersten Argument für mit der Wiederholung der Wellenform beginnen oder -1, wenn das Muster nur einmal abgespielt werden soll.

Hier sehen Sie eine Beispielwellenform, die zweimal mit einer Pause von 350 ms dazwischen pulsiert pulsiert. Der erste Impuls ist ein sanfter Anstieg bis zur maximalen Amplitude. Zweitens: eine schnelle Erhöhung zur Aufrechterhaltung der maximalen Amplitude. Das Anhalten am Ende ist definiert durch den negativen Wiederholungsindexwert.

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: Wiederholungsmuster

Wellenformen können so lange wiederholt abgespielt werden, bis sie abgebrochen werden. Die Art und Weise, mit einer sich wiederholenden Wellenform einen nicht negativen "Repeat"-Parameter festlegen. Wenn Sie eine Wellenform wiederholt, wird die Vibration so lange fortgesetzt, bis sie den Dienst:

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 unregelmäßigen Ereignissen, bei denen eine Nutzeraktion erforderlich ist. anerkennen. Beispiele für solche Ereignisse sind eingehende Anrufe und hat Alarme ausgelöst.

Beispiel: Muster mit Fallback

Die Steuerung der Amplitude einer Vibration hardwareabhängigen Funktionen. Beim Abspielen einer Wellenform auf einem ein Low-End-Gerät ohne diese Fähigkeit dazu führt, dass es bei maximaler Vibration amplitude für jeden positiven Eintrag im Amplitude-Array. Wenn Ihre App diese Geräte berücksichtigen, sollten Sie darauf achten, wird unter dieser Bedingung kein Brummen erzeugt, Sie entwerfen ein einfacheres EIN/AUS-Muster, das stattdessen als Fallback abgespielt 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 aufgezeigt, wie Sie diese längere und komplexere benutzerdefinierte Effekte. Darüber hinaus kannst du Haptik dank erweiterter Hardwarefunktionen. Sie können Kombinationen Effekte, die Amplitude und Frequenz verändern, um komplexere haptische Effekte zu erzeugen auf Geräten mit haptischen Bedienelementen und einer größeren Frequenzbandbreite.

Das Verfahren zum Erstellen einer benutzerdefinierten Vibrationsfunktion Mustern, die zuvor auf dieser Seite beschrieben wurden, wird erläutert, wie man die Vibrationsstärke reguliert, um gleichmäßige Effekte zu erzeugen. die sich nach oben und unten richten. Mit der Rich-Haptik verbessern wir dieses Konzept durch die breiterer Frequenzbereich des Gerätevibrators, um den Effekt noch gleichmäßiger zu machen. Diese Wellenformen erzeugen besonders effektiv ein Crescendo oder Diminuendo. Effekts.

Die weiter oben auf dieser Seite beschriebenen Zusammensetzungsprimitive werden durch an den Gerätehersteller. Sie erzeugen eine gestochen scharfe, kurze und angenehme Vibration die den Haptikprinzipien für ein klares haptisches Feedback entspricht. Weitere Informationen Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie unter Vibrationsbetätiger Primer

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, sollten Sie prüfen, ob das jeweilige Gerät alle von Ihnen verwendeten Primitiven.

  2. Konsistente, nicht unterstützte Websitevarianten deaktivieren, nicht nur die Effekten, bei denen eine Primitive fehlt. Weitere Informationen zum Prüfen der wird folgendermaßen angezeigt:

Mit VibrationEffect.Composition können Sie Vibrationseffekte erstellen. Hier ist 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 ist auch skalierbar, sodass Sie die Amplitude der Vibration die jeweils von ihnen generiert wurden. Die Skala ist definiert als ein Wert zwischen 0 und 1, wobei 0 einer minimalen Amplitude entspricht, mit der dieses Primitiv die Nutzenden (kaum) fühlen.

Wenn Sie eine schwache und starke Version derselben Primitive erstellen möchten, empfohlen, dass sich die Skalen um ein Verhältnis von 1,4 oder mehr voneinander unterscheiden, sodass der Unterschied Intensität leicht wahrnehmbar sein. Erstellen Sie nicht mehr als drei Intensitätsgraden derselben Primitive, weil sie nicht wahrnehmbar unterschiedlich sein. Verwenden Sie beispielsweise die Skalen 0,5, 0,7 und 1,0, um eine niedrige, mittlere, und Version einer Primitive mit hoher Intensität.

Für die Komposition können auch Verzögerungen angegeben werden, die zwischen aufeinanderfolgenden Elementen hinzugefügt werden sollen. Primitiven. Diese Verzögerung wird in Millisekunden seit dem Ende des das vorherige Primitiv. Im Allgemeinen ist auch eine Lücke von 5 bis 10 ms zwischen zwei Primitiven kurz, um erkennbar zu sein. Verwenden Sie eine Lücke von mindestens 50 ms wenn Sie eine erkennbare Lücke zwischen zwei Primitiven erzeugen möchten. Hier ist eine 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 kann die Unterstützung bestimmter Primitive:

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 zu entscheiden, welche je nach Supportstufe des Geräts erstellen:

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

Sie können die Amplitude der primitiven Vibration steuern, nützliches Feedback zu einer laufenden Aktion erhalten. Nahe beieinander liegende Skalenwerte können zur Erzeugung eines glatten Crescendo-Effekts eines Primitivs. Die Verzögerung zwischen Aufeinanderfolgende Primitive können auch dynamisch basierend auf dem Nutzer festgelegt werden. Interaktion. Dies wird im folgenden Beispiel einer Ansichtsanimation veranschaulicht: gesteuert und durch haptisches Feedback erweitert.

Animation eines Kreises, der nach unten gezogen wird
Darstellung der Vibrationswellenform der Eingabe

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

Es gibt zwei Primitive zum Erhöhen der empfundenen Vibrationsintensität: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE Mit beiden wird dasselbe Ziel erreicht, aber mit unterschiedlicher Dauer. Es gibt nur eine für die Herunterskalierung verwenden, PRIMITIVE_QUICK_FALL Diese Primitive arbeiten besser zusammen, um ein Wellenformsegment zu erstellen, das in und dann ab. Sie können skalierte Primitive ausrichten, um plötzliche Amplitudensprung zwischen ihnen, was sich auch gut zur Erweiterung des Dauer des Effekts. Wahrgenommen wird immer mehr als spürbar Der steigende Teil sollte also kürzer sein als der fallende Teil um die Betonung auf den fallenden Teil zu verlagern.

Hier ist ein Beispiel für eine Anwendung dieser Komposition zur Erweiterung und um einen Kreis zu minimieren. Der Aufstiegseffekt kann das Expansionsgefühl während der Animation. Die Kombination aus Anstiegs- und Falleffekten betont die die am Ende der Animation wieder minimiert wird.

Animation eines sich vergrößernden Kreises
Darstellung der Vibrationswellenform der Eingabe

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 wichtigsten Haptikprinzipien ist es, die Nutzer zu begeistern. Eine unterhaltsame Art einen angenehmen unerwarteten Vibrationseffekt zu erzeugen, PRIMITIVE_SPIN Dieses Primitiv ist am effektivsten, wenn es mehr als einmal aufgerufen wird. Mehrere verkettete Drehungen können zu wackeligen und instabilen Effekten führen, die sich durch die Anwendung einer etwas zufälligen Skalierung auf jedes Primitive weiter verbessert. Ich Sie können auch mit der Lücke zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Runden ohne Lücke (0 ms dazwischen) entsteht ein sich drehendes Gefühl. Steigerung führt die Inter-Spin-Lücke von 10 bis 50 ms zu einem langsameren Drehgefühl. kann verwendet werden, um die Dauer eines Videos oder einer Animation anzupassen.

Die Verwendung einer Lücke von mehr als 100 ms wird nicht empfohlen, da die Drehungen funktionieren nicht mehr gut und fühlt sich wie einzelne Effekte an.

Hier ist ein Beispiel für eine elastische Form, die nach dem Herunterziehen zurückprallt. und dann veröffentlicht. Die Animation wird mit zwei Dreheffekten optimiert, die mit unterschiedlicher Intensitäten, die proportional zur Verschiebung sind.

Animation einer hüpfenden elastischen Form
Darstellung der Vibrationswellenform der Eingabe

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: Hüpfen (mit Knöpfen)

Eine weitere erweiterte Anwendung von Vibrationseffekten besteht darin, Interaktionen. Die PRIMITIVE_THUD kann einen starken und nachhakenden Effekt erzeugen, der mit dem Visualisierung einer Wirkung, beispielsweise in einem Video oder in einer Animation, um den User Experience ausmacht.

Hier ist ein Beispiel für eine einfache Balltropfenanimation, die mit einem Thud-Effekt verbessert wurde. jedes Mal gespielt werden, wenn der Ball vom unteren Bildschirmrand abgeprallt wird:

Animation eines heruntergefallenen Balls, der vom unteren Displayrand springt
Darstellung der Vibrationswellenform der Eingabe

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