Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite finden Sie Beispiele für die Verwendung verschiedener Haptik-APIs, um in einer Android-App benutzerdefinierte Effekte zu erstellen, die über die standardmäßigen Vibrationswellenformen hinausgehen.

Diese Seite enthält die folgenden Beispiele:

Weitere Beispiele findest du unter Haptisches Feedback zu Ereignissen hinzufügen. Beachte immer die Designprinzipien für haptisches Feedback.

Fallbacks zur Handhabung der Gerätekompatibilität verwenden

Bei der Implementierung benutzerdefinierter haptischer Effekte hängen die Gerätekompatibilität und das Fallback-Verhalten von der gewählten API-Oberfläche ab:

  • VibrationEffect.Builder (empfohlen): Ab Android 16 (26Q4) enthalten mit VibrationEffect.Builder erstellte Effekte einen automatischen Fallback auf Framework-Ebene. Wenn ein Gerät einen angeforderten Preset oder einen grundlegenden Envelope nicht nativ unterstützt, übersetzt das Framework ihn bei der Wiedergabe automatisch nach bestem Wissen und Gewissen in eine geeignete Alternative. Du musst die Gerätefunktionen nicht manuell für jedes Primitive prüfen, bevor du mit VibrationEffect.Builder erstellte Effekte abspielst.
    • Ausnahme: Erweiterte Wellenform-Hüllkurven, die mit WaveformEnvelopeBuilder erstellt wurden, unterstützen kein automatisches Fallback, da sie von bestimmten Hardware-Frequenzzuordnungen (FOAM) abhängen. Nicht unterstützte Dateien werden nicht wiedergegeben.
  • VibrationEffect.Composition: Für Kompositionen, die mit der startComposition() API erstellt wurden, gibt es keine automatischen Fallbacks. Wenn eine Komposition auch nur ein nicht unterstütztes Primitiv enthält, wird die gesamte Vibration nicht wiedergegeben. Sie müssen die Funktionen manuell mit vibrator.arePrimitivesSupported() prüfen.
  • Wellenformen mit Amplitudensteuerung:Amplituden, die nicht null sind, werden auf Geräten ohne Amplitudensteuerung auf 100% aufgerundet. Prüfe vibrator.hasAmplitudeControl() und verwende bei Bedarf ein explizit entworfenes EIN/AUS-Muster.

Verwendung haptischer Primitive

Android enthält mehrere Primitive für die Haptik, die sich sowohl in der Amplitude als auch in der Frequenz unterscheiden. Du kannst ein einzelnes Primitiv oder mehrere Primitive in Kombination verwenden, um komplexe haptische Effekte zu erzielen.

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

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig in der aufmerksamkeitsbezogenen Haptik verwendet, z. B. für Benachrichtigungen und Klingeltöne. Der Dienst Vibrator kann lange Vibrationsmuster abspielen, die die Vibrationsamplitude im Laufe der Zeit ändern. Solche Effekte werden als Wellenformen bezeichnet.

Wellenformeffekte sind in der Regel wahrnehmbar, aber plötzliche lange Vibrationen können den Nutzer erschrecken, wenn sie in einer ruhigen Umgebung abgespielt werden. Wenn die Zielamplitude zu schnell erreicht wird, kann dies ebenfalls zu hörbaren Brummgeräuschen führen. Entwerfen Sie Wellenformmuster, um die Amplitudenübergänge zu glätten und so Ramp-up- und Ramp-down-Effekte zu erzeugen.

Beispiele für Vibrationsmuster

In den folgenden Abschnitten finden Sie einige Beispiele für Vibrationsmuster:

Muster für die Erhöhung des Prozentsatzes

Wellenformen werden als VibrationEffect mit drei Parametern dargestellt:

  1. Timings: Ein Array von Dauern 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 von 0 bis 255, wobei 0 für den Vibrator „Aus“ und 255 für die maximale Amplitude des Geräts steht.
  3. Wiederholungsindex:Der Index im Array, der im ersten Argument angegeben ist, um die Wellenform zu wiederholen, 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 Impulsen pulsiert. Der erste Impuls ist ein sanfter Anstieg bis zur maximalen Amplitude, der zweite ein schneller Anstieg, um die maximale Amplitude zu halten. Das Anhalten am Ende wird durch den negativen Wiederholungsindexwert definiert.

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 // Don't repeat.

vibrator.vibrate(
    VibrationEffect.createWaveform(
        timings, amplitudes, repeatIndex
    )
)

Sich wiederholendes Muster

Wellenformen können auch wiederholt abgespielt werden, bis sie abgebrochen werden. Wenn Sie eine sich wiederholende Wellenform erstellen möchten, müssen Sie einen nicht negativen repeat-Parameter festlegen. Wenn du eine sich wiederholende Wellenform abspielst, wird die Vibration fortgesetzt, bis sie im Dienst explizit abgebrochen wird:

fun 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.
    val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform(
        timings, amplitudes, repeat
    )
    // repeatingEffect can be used in multiple places.

    vibrator.vibrate(repeatingEffect)
}

fun stopVibrating() {
    vibrator.cancel()
}

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

Muster mit Fallback

Die Steuerung der Amplitude einer Vibration ist eine hardwareabhängige Funktion. Wenn eine Wellenform auf einem Low-End-Gerät ohne diese Funktion wiedergegeben wird, vibriert das Gerät bei jedem positiven Eintrag im Amplituden-Array mit der maximalen Amplitude. Wenn deine App solche Geräte unterstützen muss, verwende entweder ein Muster, das bei der Wiedergabe in diesem Zustand keinen Brummton erzeugt, oder entwirf ein einfacheres EIN/AUS-Muster, das stattdessen als Fallback wiedergegeben werden kann.

if (vibrator.hasAmplitudeControl()) {
    vibrator.vibrate(
        VibrationEffect.createWaveform(
            smoothTimings, amplitudes, smoothRepeatIdx
        )
    )
} else {
    vibrator.vibrate(
        VibrationEffect.createWaveform(
            onOffTimings, onOffRepeatIdx
        )
    )
}

An der Zeitachse verankerte Kompositionen mit VibrationEffect.Builder

Ab Android 16 (26Q4) ist VibrationEffect.Builder die bevorzugte API zum Erstellen komplexer Vibrationseffekte und ‑kompositionen. Mit startTimeMillis kannst du ausdrucksstarke haptische Empfindungen erzeugen, indem du diskrete haptische Elemente entlang einer absoluten Zeitachse sequenzierst.

VibrationEffect.Builder unterstützt die Kombination mehrerer Vibrationstypen:

Vibrationen, die mit VibrationEffect.Builder erstellt wurden, haben auf Framework-Ebene einen integrierten automatischen Fallback für alle Elemente (einschließlich Voreinstellungen, grundlegenden Hüllkurven und kombinierten Vibrationseffekten). So wird eine einheitliche Nutzererfahrung auf verschiedenen Geräten gewährleistet, ohne dass manuelle Funktionsprüfungen erforderlich sind. (Erweiterte Wellenform-Hüllkurven, die mit WaveformEnvelopeBuilder erstellt wurden, erfordern Hardwareunterstützung und haben keine automatische Fallback-Unterstützung.)

Mit Voreinstellungen erstellen

Mit VibrationEffect.Preset kannst du einer Komposition gängige, vordefinierte kurze haptische Impulse (z. B. PRESET_CLICK, PRESET_TICK oder PRESET_LOW_TICK) hinzufügen. Voreinstellungen werden vom Gerätehersteller implementiert, um eine kurze, angenehme Vibration zu erzeugen, die den Prinzipien für haptisches Feedback entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise findest du unter Vibrationsaktuatoren – Grundlagen.

Voreinstellungen ersetzen die kurzen Primitive aus der VibrationEffect.Composition API. Für längere oder kontinuierliche haptische Empfindungen, z. B. beim Erhöhen und Verringern der Intensität, solltest du stattdessen Hüllkurven-Wellenformen (PWLE) verwenden.

Jeder Voreinstellung kann eine optionale Skalierung zwischen 0.0f und 1.0f zugewiesen werden. Sie wird zu einer expliziten Startzeit (in Millisekunden) ab dem Beginn der Komposition platziert.

Kotlin

val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)

val effect = VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build()

vibrator.vibrate(effect)

Java

VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);

VibrationEffect effect = new VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build();

vibrator.vibrate(effect);

Mit Hüllkurven und Voreinstellungen komponieren

Du kannst VibrationEffect.Envelope-Instanzen, die mit BasicEnvelopeBuilder oder WaveformEnvelopeBuilder erstellt wurden, nahtlos mit Voreinstellungen kombinieren, um komplexe haptische Muster mit mehreren Segmenten zu erstellen.

Hier ist ein Beispiel, bei dem ein sanfter Anstieg und ein Ausblenden gefolgt von einem scharfen Klick-Preset abgespielt werden:

Kotlin

val basicEnvelope = VibrationEffect.Envelope.create(
    VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
)

val mixedEffect = VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build()

vibrator.vibrate(mixedEffect)

Java

VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
    new VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
);

VibrationEffect mixedEffect = new VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build();

vibrator.vibrate(mixedEffect);

Vorhandene Ereignisse wiederverwenden und verschieben

Wenn Sie einen vorhandenen VibrationEffect (einschließlich VibrationEffect.Composition) wiederverwenden oder verketten möchten, rufen Sie die Liste der VibrationEffect.Event-Objekte mit getEvents() ab und hängen Sie sie mit einem Offset mit addEvents(startTimeShiftMillis, events) an (oder übergeben Sie den Effekt direkt an den VibrationEffect.Builder(effect)-Konstruktor). Wenn Sie VibrationEffect.Composition-Instanzen auf diese Weise importieren, konvertiert das Framework ihre Primitive automatisch in Voreinstellungen, sodass die Laufzeit-Fallback-Unterstützung aktiviert wird.

Kotlin

val existingEffect = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build()

// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
    .build()

vibrator.vibrate(combinedEffect)

Java

VibrationEffect existingEffect = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build();

// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
    .build();

vibrator.vibrate(combinedEffect);

Wiederkehrende Kompositionen erstellen

Verwenden Sie setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis), um einer Komposition ein sich wiederholendes Muster hinzuzufügen:

Kotlin

val repeatingPattern = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build()

val repeatingEffect = VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build()

vibrator.vibrate(repeatingEffect)

Java

VibrationEffect repeatingPattern = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build();

VibrationEffect repeatingEffect = new VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build();

vibrator.vibrate(repeatingEffect);

Timing, Validierung und Drift-Management

Beachten Sie beim Entwerfen von Kompositionen mit VibrationEffect.Builder die folgenden Timing- und Validierungsregeln:

  • Streng ansteigende Startzeiten: Jedes Element, das dem Builder hinzugefügt wird, muss eine startTimeMillis haben, die streng größer oder gleich der Startzeit des vorherigen Elements ist.
  • Validierung während der Erstellung:Der Builder führt während der build()-Zeit eine Best-Effort-Validierung durch. Dabei werden bekannte Elementdauern (oder ein Minimum von 1 ms für Voreinstellungen) verwendet. Wenn eine unmögliche Überschneidung erkannt wird, wird ein IllegalArgumentException ausgelöst.
  • Sequenzielle Verschiebung der Wiedergabe:Wenn ein vorheriges Vibrationselement noch physisch wiedergegeben wird, wenn die Startzeit des nächsten Elements erreicht ist, verschiebt das Framework das nächste Element automatisch auf das früheste verfügbare Zeitfenster. So wird sichergestellt, dass sich Ereignisse nicht überschneiden und keine Vibrationen verloren gehen. Es kann jedoch zu leichten Zeitabweichungen kommen, wenn Ereignisse zu dicht beieinander geplant sind. Um die Abweichung zu minimieren, solltest du zwischen aufeinanderfolgenden haptischen Ereignissen ausreichend Zeit einplanen (z.B. 50 ms oder mehr).

Vibrationsprimitive erstellen

In diesem Abschnitt wird beschrieben, wie du Vibrationen mit VibrationEffect.Composition erstellst. Die weiter oben auf dieser Seite beschriebenen Primitive für die Komposition werden vom Gerätehersteller implementiert. Sie bieten eine kurze, angenehme Vibration, die den Prinzipien für haptisches Feedback entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise findest du unter Vibrationsaktuatoren – Grundlagen.

Im Gegensatz zu VibrationEffect.Builder bietet die VibrationEffect.Composition API keine automatischen Fallbacks für nicht unterstützte Primitive. Dementsprechend gilt:

  1. Bevor du die erweiterte Haptik aktivierst, solltest du prüfen, ob das jeweilige Gerät alle von dir verwendeten Primitiven unterstützt.

  2. Deaktivieren Sie alle nicht unterstützten Funktionen, nicht nur die Effekte, für die ein Primitiv fehlt.

  • Kotlin: val fallbackEffect = VibrationEffect.Builder(compositionEffect).build() (oder .addEvents(0L, compositionEffect.events))
  • Java: VibrationEffect fallbackEffect = new VibrationEffect.Builder(compositionEffect).build(); (oder .addEvents(0L, compositionEffect.getEvents()))

Wenn das Framework mit VibrationEffect.Builder erstellt wird, wandelt es die Kompositionsprimitive in Voreinstellungen um und stellt automatisch einen Laufzeit-Fallback bereit, wenn ein Primitiv auf dem Gerät des Nutzers nicht unterstützt wird.

Zusammengesetzte Vibrationseffekte

Mit VibrationEffect.Composition kannst du zusammengesetzte Vibrationseffekte erstellen. Hier ist ein Beispiel für einen langsam ansteigenden Effekt, gefolgt von einem scharfen Klick-Effekt:

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 nacheinander abgespielt werden sollen. Jedes Primitiv ist außerdem skalierbar, sodass du die Amplitude der von jedem Primitiv erzeugten Vibration steuern kannst. Die Skala ist als Wert zwischen 0 und 1 definiert, wobei 0 der Mindestamplitude entspricht, bei der dieses Primitiv vom Nutzer (kaum) wahrgenommen werden kann.

Varianten in Vibrations-Primitives erstellen

Wenn du eine schwache und eine starke Version desselben Primitivs erstellen möchtest, erstelle Stärkeverhältnisse von 1,4 oder mehr, damit der Unterschied in der Intensität leicht wahrgenommen werden kann. Versuche nicht, mehr als drei Intensitätsstufen desselben Primitivs zu erstellen, da sie nicht wahrnehmbar sind. Verwenden Sie beispielsweise die Skalierungen 0,5, 0,7 und 1,0, um Versionen eines Primitivs mit niedriger, mittlerer und hoher Intensität zu erstellen.

Pausen zwischen Vibrations-Primitives hinzufügen

In der Komposition können auch Verzögerungen angegeben werden, die zwischen aufeinanderfolgenden Primitiven hinzugefügt werden sollen. Diese Verzögerung wird in Millisekunden seit dem Ende des vorherigen Primitivs angegeben. Im Allgemeinen ist ein Abstand 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 eine erkennbare Lücke zwischen zwei Primitiven erstellen möchten. Hier ist ein Beispiel für eine Komposition mit Verzögerungen:

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

Prüfen, welche Primitive unterstützt werden

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

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

Es ist auch möglich, mehrere Primitive zu prüfen und dann anhand des Geräteunterstützungsgrads zu entscheiden, welche zusammengesetzt werden sollen:

val primitives: IntArray = intArrayOf(
    VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
    VibrationEffect.Composition.PRIMITIVE_TICK,
    VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)

Beispiele für Vibrationskompositionen

In den folgenden Abschnitten finden Sie mehrere Beispiele für Vibrationskompositionen aus der Beispiel-App für haptisches Feedback auf GitHub.

Widerstand (mit niedrigen Ticks)

Du kannst die Amplitude der primitiven Vibration steuern, um nützliches Feedback zu einer laufenden Aktion zu geben. Mit eng beieinander liegenden Skalierungswerten lässt sich ein gleichmäßiger Crescendo-Effekt eines Primitivs erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch basierend auf der Nutzerinteraktion festgelegt werden. Dies wird im folgenden Beispiel einer Ansichtsanimation veranschaulicht, die durch eine Ziehgeste gesteuert und durch haptisches Feedback ergänzt wird.

Animation eines Kreises, der nach unten gezogen wird.
Diagramm der Eingabevibrationswellenform.

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

@Composable
fun ResistScreen() {
    // Control variables for the dragging of the indicator.
    var isDragging by remember { mutableStateOf(false) }
    var dragOffset by remember { mutableFloatStateOf(0f) }

    // Only vibrates while the user is dragging
    LaunchedEffect(isDragging) {
        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        while (isDragging) {
            // 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()
            )
        }
    }

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

Maximieren (mit Anstieg und Abfall)

Es gibt zwei Primitive, um die wahrgenommene Vibrationsintensität zu erhöhen: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, aber mit unterschiedlicher Dauer. Es gibt nur ein Primitiv für das Herunterfahren: PRIMITIVE_QUICK_FALL. Diese Primitiven funktionieren besser zusammen, um ein Wellenformsegment zu erstellen, das an Intensität zunimmt und dann abklingt. Sie können skalierte Primitive ausrichten, um plötzliche Amplitudensprünge zwischen ihnen zu verhindern. Das funktioniert auch gut, um die Gesamtdauer des Effekts zu verlängern. In der Wahrnehmung wird der ansteigende Teil immer stärker wahrgenommen als der abfallende Teil. Wenn der ansteigende Teil kürzer als der abfallende Teil ist, kann der Schwerpunkt auf den abfallenden Teil verlagert werden.

Hier ist ein Beispiel für die Anwendung dieser Komposition zum Maximieren und Minimieren eines Kreises. Der Anstiegseffekt kann das Gefühl der Erweiterung während der Animation verstärken. Die Kombination aus Auf- und Abstiegseffekten hilft, das Zusammenbrechen am Ende der Animation zu betonen.

Animation eines sich ausdehnenden Kreises.
Diagramm der Eingabevibrationswellenform.

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

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)

    Column(
        Modifier
            .clickable(
                onClick = {
                    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)
    }
}

Wobble (mit Drehungen)

Eines der wichtigsten Prinzipien der Haptik ist es, Nutzer zu begeistern. Eine gute Möglichkeit, einen angenehmen, unerwarteten Vibrationseffekt einzuführen, ist die Verwendung von PRIMITIVE_SPIN. Dieses Primitiv ist am effektivsten, wenn es mehrmals aufgerufen wird. Wenn mehrere Drehungen verkettet werden, kann ein wackeliger und instabiler Effekt entstehen, der durch eine etwas zufällige Skalierung der einzelnen Primitive noch verstärkt werden kann. Sie können auch mit dem Abstand zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Drehungen ohne Pause (0 ms dazwischen) erzeugen ein Gefühl von schneller Drehung. Wenn du den Abstand zwischen den Drehungen von 10 auf 50 ms erhöhst, fühlt sich die Drehung lockerer an. Das kann verwendet werden, um die Dauer eines Videos oder einer Animation anzupassen.

Verwende keine Pause, die länger als 100 ms ist, da die aufeinanderfolgenden Drehungen dann nicht mehr gut zusammenpassen und sich wie einzelne Effekte anfühlen.

Hier ist ein Beispiel für eine elastische Form, die zurückspringt, nachdem sie nach unten gezogen und dann losgelassen wurde. Die Animation wird durch zwei Dreheffekte optimiert, die mit unterschiedlicher Intensität abgespielt werden, die proportional zur Sprungverschiebung ist.

Animation einer hüpfenden elastischen Form
Diagramm der Eingabevibrationswellenform

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

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableFloatStateOf(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
        ),
        // The spring ends on its target. An end value of 0f means that the
        // shape is back at rest, so the wobble is over.
        finishedListener = { endValue -> if (endValue == 0f) isWobbling = false },
    )

    LaunchedEffect(isWobbling) {
        while (isWobbling) {
            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 = {
                    // Wobble only if the shape is far enough from rest to vibrate.
                    // It then springs back to 0f, and the finishedListener ends
                    // the wobble.
                    val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                    isWobbling = displacement > SPIN_MIN_DISPLACEMENT
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value. The
        // lambda defers the read to the draw phase, so the animation doesn't
        // recompose this screen on every frame.
        WobbleShape { dragDistanceAnimated }
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
    // Generate a random offset in the range [-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)
}

Hüpfen (mit dumpfen Geräuschen)

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

Hier ist ein Beispiel für eine Animation, bei der ein Ball fällt und jedes Mal, wenn er vom unteren Bildschirmrand abprallt, ein dumpfer Aufprall zu hören ist:

Animation eines fallengelassenen Balls, der vom unteren Bildschirmrand abprallt.
Diagramm der Eingabevibrationswellenform.

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

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
    // Control variable for the state of the ball.
    var ballPosition by remember { mutableStateOf(BallPosition.Start) }
    // Plain holder for the bounce count. It's read only in the effect and in
    // the click handler, never in composition, so it needs no snapshot state.
    val bounceCount = remember { intArrayOf(0) }

    // Animation for the bouncing ball.
    val transitionData = updateTransitionData(ballPosition)
    val collisionData = updateCollisionData(transitionData)

    // Ball is about to contact floor. The effect runs only when the collision
    // state changes, so it vibrates once per collision. Nothing here suspends,
    // so a keyed SideEffect does the job without launching a coroutine.
    SideEffect(collisionData.collisionWithFloor) {
        if (collisionData.collisionWithFloor) {
            val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat()
            vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_THUD,
                    vibrationScale
                ).compose()
            )
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .clickable {
                if (transitionData.isAtStart) {
                    ballPosition = BallPosition.End
                } else {
                    ballPosition = BallPosition.Start
                    bounceCount[0] = 0
                }
            },
    ) {
        // Build the ball UI based on the current state.
        BouncingBall(transitionData)
    }
}

Vibrationswellenform mit Hüllkurven

Beim Erstellen benutzerdefinierter Vibrationsmuster kannst du die Vibrationsamplitude steuern, um flüssige Effekte für das Erhöhen und Verringern der Vibration zu erzeugen. In diesem Abschnitt wird erläutert, wie Sie dynamische haptische Effekte mithilfe von Wellenform-Hüllkurven erstellen, die eine präzise Steuerung der Vibrationsamplitude und ‑frequenz im Zeitverlauf ermöglichen. So kannst du vielfältigere und differenziertere haptische Erlebnisse schaffen.

Ab Android 16 (API-Level 36) bietet das System die folgenden APIs, um eine Hüllkurve für Vibrationswellenformen zu erstellen, indem eine Sequenz von Kontrollpunkten definiert wird:

  • BasicEnvelopeBuilder: Ein barrierefreier Ansatz zum Erstellen von hardwareunabhängigen haptischen Effekten.
  • WaveformEnvelopeBuilder: Ein fortschrittlicherer Ansatz zum Erstellen haptischer Effekte, der Kenntnisse der haptischen Hardware erfordert.

Android bietet keine Fallbacks für Envelope-Effekte. Wenn Sie diese Unterstützung benötigen, führen Sie die folgenden Schritte aus:

  1. Prüfe mit Vibrator.areEnvelopeEffectsSupported(), ob ein bestimmtes Gerät Envelope-Effekte unterstützt.
  2. Deaktivieren Sie die einheitlichen Funktionen, die nicht unterstützt werden, oder verwenden Sie benutzerdefinierte Vibrationsmuster oder Kompositionen als Fallback-Alternativen.

Wenn Sie einfachere Envelope-Effekte erstellen möchten, verwenden Sie die BasicEnvelopeBuilder mit den folgenden Parametern:

  • Ein Intensitätswert im Bereich \( [0, 1] \), der die wahrgenommene Stärke der Vibration darstellt. Ein Wert von \( 0.5 \)wird beispielsweise als die Hälfte der globalen maximalen Intensität wahrgenommen, die vom Gerät erreicht werden kann.
  • Ein Schärfewert im Bereich von \( [0, 1] \), der die Klarheit der Vibration darstellt. Niedrigere Werte führen zu sanfteren Vibrationen, während höhere Werte ein schärferes Gefühl erzeugen.

  • Ein Dauer-Wert, der die Zeit in Millisekunden darstellt, die für den Übergang vom letzten Kontrollpunkt (d. h. ein Paar aus Intensität und Schärfe) zum neuen Kontrollpunkt benötigt wird.

Hier ist ein Beispiel für eine Wellenform, bei der die Intensität über 500 ms von einer niedrigen zu einer hohen Frequenz mit maximaler Stärke ansteigt und dann über 100 ms wieder auf\( 0 \) (Aus) abfällt.

vibrator.vibrate(
    VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 500)
        .addControlPoint(0.0f, 1.0f, 100)
        .build()
)

Wenn du dich mit Haptik auskennst, kannst du mit WaveformEnvelopeBuilder Hüllkurveneffekte definieren. Wenn du dieses Objekt verwendest, kannst du über VibratorFrequencyProfile auf die Zuordnung von Frequenz zu Ausgabebeschleunigung (Frequency-to-Output-Acceleration Mapping, FOAM) zugreifen.

  • Ein Amplitudenwert im Bereich \( [0, 1] \), der die erreichbare Vibrationsstärke bei einer bestimmten Frequenz darstellt, wie vom FOAM des Geräts bestimmt. Ein Wert von \( 0.5 \) erzeugt beispielsweise die Hälfte der maximalen Ausgabebeschleunigung, die bei der angegebenen Frequenz erreicht werden kann.
  • Ein Frequenzwert in Hertz.

  • Ein duration-Wert, der die Zeit in Millisekunden darstellt, die für den Übergang vom letzten Kontrollpunkt zum neuen Kontrollpunkt benötigt wird.

Der folgende Code zeigt ein Beispiel für eine Wellenform, die einen Vibrationseffekt von 400 ms definiert. Es beginnt mit einer 50-ms-Amplitudenrampe, von aus bis voll, bei konstanten 60 Hz. Dann steigt die Frequenz über die nächsten 100 ms auf 120 Hz an und bleibt 200 ms lang auf diesem Niveau. Schließlich sinkt die Amplitude auf \( 0 \)und die Frequenz kehrt über die letzten 50 ms auf 60 Hz zurück:

vibrator.vibrate(
    VibrationEffect.WaveformEnvelopeBuilder()
        .addControlPoint(1.0f, 60f, 50)
        .addControlPoint(1.0f, 120f, 100)
        .addControlPoint(1.0f, 120f, 200)
        .addControlPoint(0.0f, 60f, 50)
        .build()
)

In den folgenden Abschnitten finden Sie mehrere Beispiele für Vibrationswellenformen mit Hüllkurven.

Springende Feder

In einem vorherigen Beispiel wurde PRIMITIVE_THUD verwendet, um physische Abprallinteraktionen zu simulieren. Die Basic Envelope API bietet eine deutlich genauere Steuerung, mit der du die Intensität und Schärfe der Vibration präzise anpassen kannst. Das Ergebnis ist ein haptisches Feedback, das besser zu den animierten Ereignissen passt.

Hier ist ein Beispiel für eine ungehindert fallende Feder, deren Animation mit einem einfachen Hüllkurveneffekt verbessert wurde, der jedes Mal abgespielt wird, wenn die Feder vom unteren Bildschirmrand abprallt:

Animation einer fallenden Feder, die vom unteren Bildschirmrand abprallt.
Diagramm der Eingabevibrationswellenform.

Abbildung 5: Ein Diagramm der Beschleunigungswellenform für eine Vibration, die eine springende Feder simuliert.

@Composable
fun BouncingSpringAnimation() {
    // The spring's height above the floor: 1f is the drop height, 0f is the floor.
    val springHeight = remember { Animatable(1f) }
    var isAnimating by remember { mutableStateOf(true) }

    LaunchedEffect(isAnimating) {
        if (!isAnimating) return@LaunchedEffect
        springHeight.snapTo(1f)

        // Each bounce reaches a fraction of the previous height.
        var bounceHeight = 1f
        while (bounceHeight > MIN_BOUNCE_HEIGHT) {
            // Fall to the floor, speeding up like a falling object. A fall from
            // a greater height takes longer.
            val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
            springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad))

            // The spring hits the floor: play a "boing" envelope effect that fades
            // out. A higher bounce gives a stronger and sharper vibration.
            vibrator.vibrate(
                VibrationEffect.BasicEnvelopeBuilder()
                    // Starting from zero sharpness here, will simulate a smoother
                    // "boing" effect.
                    .setInitialSharpness(0f)
                    // Add a control point to reach the target intensity and
                    // sharpness very quickly.
                    .addControlPoint(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L)
                    // Add a control point to fade out the vibration intensity while
                    // maintaining sharpness.
                    .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong())
                    .build()
            )

            // Bounce back up to a lower height, slowing down like a thrown object.
            // When the next bounce is too small, the spring stays on the floor.
            bounceHeight *= BOUNCE_DAMPING
            if (bounceHeight > MIN_BOUNCE_HEIGHT) {
                val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
                springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad))
            }
        }
        isAnimating = false
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = !isAnimating) { isAnimating = true }
            .drawBehind {
                // Read the height in the draw phase, and scale it to the distance
                // between the top of the box and the floor.
                val floorTop = size.height - FLOOR_HEIGHT.toPx()
                val dropDistance = floorTop - SPRING_HEIGHT.toPx()
                drawSpring(topY = dropDistance * (1f - springHeight.value))
                drawFloor(top = floorTop)
            }
    ) {
        if (!isAnimating) {
            Text("Tap to restart", Modifier.align(Alignment.Center))
        }
    }
}

Raketenstart

In einem früheren Beispiel wurde gezeigt, wie Sie mit der grundlegenden Envelope API eine federnde Reaktion simulieren können. Der WaveformEnvelopeBuilder ermöglicht eine präzise Steuerung des gesamten Frequenzbereichs des Geräts und damit hochgradig personalisierte haptische Effekte. In Kombination mit FOAM-Daten können Sie Vibrationen auf bestimmte Frequenzbereiche abstimmen.

Hier ist ein Beispiel für die Simulation eines Raketenstarts mit einem dynamischen Vibrationsmuster. Der Effekt reicht von der minimal unterstützten Frequenzbeschleunigungsausgabe (0,1 G) bis zur Resonanzfrequenz, wobei immer eine Amplitudeneingabe von 10 % beibehalten wird. Dadurch beginnt der Effekt mit einer relativ starken Ausgabe und die wahrgenommene Intensität und Schärfe wird erhöht, obwohl die Antriebsamplitude gleich ist. Wenn die Resonanz erreicht ist, sinkt die Effektfrequenz wieder auf das Minimum, was als abnehmende Intensität und Schärfe wahrgenommen wird. Dadurch entsteht ein Gefühl des anfänglichen Widerstands, gefolgt von einer Freisetzung, die einen Start in den Weltraum nachahmt.

Dieser Effekt ist mit der einfachen Envelope API nicht möglich, da sie die gerätespezifischen Informationen zur Resonanzfrequenz und zur Beschleunigungskurve der Ausgabe abstrahiert. Wenn du die Schärfe erhöhst, kann die entsprechende Frequenz über die Resonanz hinausgehen, was zu einem unbeabsichtigten Abfall der Beschleunigung führen kann.

Animation einer Rakete, die vom unteren Bildschirmrand startet.
Diagramm der Eingabevibrationswellenform.

Abbildung 6. Ein Diagramm der Ausgangsbeschleunigungswellenform für eine Vibration, die einen Raketenstart simuliert.

@Composable
fun RocketLaunchAnimation() {
    var isLaunched by remember { mutableStateOf(false) }
    val animation = remember { Animatable(0f) }

    val animationDuration = 3000
    LaunchedEffect(isLaunched) {
        if (isLaunched) {
            animation.animateTo(
                1.2f, // Overshoot so that the rocket goes off the screen.
                animationSpec = tween(
                    durationMillis = animationDuration,
                    // Applies an easing curve with a slow start and rapid acceleration
                    // towards the end.
                    easing = CubicBezierEasing(1f, 0f, 0.75f, 1f)
                )
            )
            animation.snapTo(0f)
            isLaunched = false
        }
    }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .clickable {
                if (!isLaunched) {
                    // Play vibration with same duration as the animation, using 70% of
                    // the time for the rise of the vibration, to match the easing curve
                    // defined previously.
                    playVibration(vibrator, animationDuration.toLong(), 0.7f)
                    isLaunched = true
                }
            }
            .background(colorResource(R.color.background))
    ) {
        // Read the animation in the draw phase, and scale it to the canvas height.
        drawRocket(size.height * animation.value)
    }
}

private fun playVibration(
    vibrator: Vibrator,
    totalDurationMs: Long,
    riseBias: Float,
    minOutputAccelerationGs: Float = 0.1f,
) {
    require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." }

    if (!vibrator.areEnvelopeEffectsSupported()) {
        return
    }

    val resonantFrequency = vibrator.resonantFrequency
    if (resonantFrequency.isNaN()) {
        // Device doesn't have or expose a resonant frequency.
        return
    }

    val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return

    if (startFrequency >= resonantFrequency) {
        // Vibrator can't generate the minimum required output at lower frequencies.
        return
    }

    val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis
    val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs
    val rampDownDurationMs = totalDurationMs - rampUpDurationMs - minDurationMs

    vibrator.vibrate(
        VibrationEffect.WaveformEnvelopeBuilder()
            // Quickly reach the target output at the start frequency
            .addControlPoint(0.1f, startFrequency, minDurationMs)
            .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs)
            .addControlPoint(0.1f, startFrequency, rampDownDurationMs)
            // Controlled ramp down to zero to avoid ringing after the vibration.
            .addControlPoint(0.0f, startFrequency, minDurationMs)
            .build()
    )
}

LavaBeats

Wie im Beispiel Raketenstart ermöglicht die WaveformEnvelopeBuilder API das Design vieler komplexer haptischer Effekte, indem sie die Steuerung von Amplituden- und Frequenzsegmenten in einer Vibration ermöglicht. Ein weiteres Beispiel für ein solches Design ist die Emulation abstrakterer physischer Empfindungen wie „Lebendigkeit“.

Dazu werden Biomarker eines typischen Elektrokardiogramm-Signals (EKG) mit Vibrationssegmenten bestimmter Amplituden und Frequenzen dargestellt. LavaBeats ist ein Beispiel, bei dem zwei charakteristische Segmente einer EKG-Aufzeichnung als zwei Impulse dargestellt werden, die durch eine Zeitverzögerung getrennt sind. Der erste charakteristische Puls ist der QRS-Komplex, der als scharfe Spitze mit hoher Amplitude und kurzer Dauer angezeigt wird. Der zweite Puls ist die T-Welle, die eine geringere Amplitude, eine längere Dauer und eine glattere Form hat (siehe Abbildung 7).

Verwenden Sie die WaveformEnvelopeBuilder, um verschiedene Wiederholungen dieser beiden Impulse zu erstellen, die durch eine feste Verzögerung zwischen dem ersten und dem zweiten Impuls getrennt sind. Der erste Impuls kann ein Chirp-Signal sein, das bei einer niedrigen Frequenz beginnt und über eine kurze Dauer bei einer höheren Frequenz endet. Der zweite Puls kann als einzelne Periode einer Sinuskurve mit niedriger Frequenz dargestellt werden. Wir können die beiden Impulse zu einem Beat zusammenfügen und die Komposition mehrmals mit einer Verzögerung dazwischen wiederholen, wobei wir einer typischen Beats-per-Minute-Rate (bpm) folgen. Das Ergebnis ist ein haptischer Effekt, der einem schlagenden Herzen ähnelt.

Du kannst LavaBeats in unserer Beispiel-App für haptisches Feedback auf GitHub ausprobieren und den Effekt zusammen mit einer Lavaleuchten-Visualisierung spüren, die im selben Rhythmus wie der haptische Effekt schlägt. Du kannst auch die Einstellungen des Effekts ändern, um verschiedene Schwebungsempfindungen zu erzeugen, indem du die Amplituden, Frequenzen, Dauern und Verzögerungen der beiden Impulse änderst.

Lavalampen-Animation, die im haptischen Herzschlagmuster pulsiert.
Diagramm eines EKG-Segments und seiner charakteristischen Wellenformen

Abbildung 7: Ein Segment einer EKG-Aufzeichnung mit dem QRS-Komplex und der T-Welle

private fun createEnvelopeEffect(
    beatParameters: List<BeatParameter>
): VibrationEffect =
    VibrationEffect.WaveformEnvelopeBuilder()
        .apply {
            repeat(beatParameters.getNumBeats()) {
                // First pulse chirp
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseStartFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstPulseDurationMillis().toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )

                // Delay between first and second pulse
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstToSecondPulseDelayMillis().toLong(),
                )

                // Second pulse
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    beatParameters.getBeatDelayMillis().toLong(),
                )
            }
        }
        .build()

/** A parameter of a haptic beat effect that represents an ECG signal parameter */
data class BeatParameter(
    val description: String = "",
    val value: Float = 0f,
    val range: ClosedFloatingPointRange<Float> = 0f..1f,
    val steps: Int = 0,
    val isFrequencyType: Boolean = false,
)