Beispiele und Anleitungen zum Erstellen benutzerdefinierter haptischer Effekte in Android, einschließlich zeitachsenverankerter Kompositionen mit VibrationEffect.Builder, benutzerdefinierter Vibrationsmuster und erweiterter Wellenform-Envelopes. keywords_public: > Android, Haptik, benutzerdefinierte Effekte, Vibration, haptische APIs, Vibrationsmuster, VibrationEffect.Builder, Kompositionen, haptische Primitiven, Wellenform-Envelopes, Benutzeroberfläche
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 Vibrations-Waveforms hinausgehen.
Diese Seite enthält die folgenden Beispiele:
- Zeitachsenbasierte Kompositionen mit
VibrationEffect.Builder- Mit Voreinstellungen komponieren: Sie können vordefinierte haptische Empfindungen in einer bestimmten Reihenfolge abspielen.
- Mit Hüllkurven und Presets komponieren: Kombinieren Sie Hüllkurven und Presets auf einer Zeitachse.
- Ereignisse wiederverwenden und verschieben: Sie können vorhandene Kompositionsereignisse verschieben und wiederverwenden.
- Wiederholende Kompositionen: Erstellen Sie wiederholende Zeitleisteneffekte.
- Benutzerdefinierte Vibrationsmuster
- Ramp-up-Muster: Ein Muster, das sanft beginnt.
- Wiederholendes Muster: Ein Muster ohne Ende.
- Muster mit Fallback: Eine Fallback-Demonstration.
- Zusammensetzungen von Vibrationsprimitiven
- Widerstand: Ein Zieheffekt mit dynamischer Intensität.
- Expand (Erweitern): Ein Effekt, bei dem die Lautstärke zuerst ansteigt und dann wieder abfällt.
- Wobble: Ein Wackeleffekt mit dem
SPIN-Primitive. - Abprallen: Ein Abpralleffekt mit dem
THUD-Primitive.
- Vibrationswellenform mit Hüllkurven
- Bouncing spring (Federung): Ein federnder Effekt mit grundlegenden Hüllkurveneffekten.
- Raketenstart: Ein Raketenstart-Effekt mit Wellenform-Hüllkurven-Effekten.
Weitere Beispiele finden Sie unter Haptisches Feedback zu Ereignissen hinzufügen. Beachten Sie dabei immer die Designprinzipien für Haptik.
Fallbacks zur Behandlung der Gerätekompatibilität verwenden
Bei der Implementierung benutzerdefinierter Haptik-Effekte hängen die Gerätekompatibilität und das Fallback-Verhalten von der ausgewählten API-Oberfläche ab:
VibrationEffect.Builder(empfohlen): Ab Android 16 (4. Quartal 2026) enthalten mitVibrationEffect.Buildererstellte Effekte automatische Fallback-Lösungen auf Framework-Ebene. Wenn ein Gerät eine angefordertePresetoder eine grundlegendeEnvelopenicht nativ unterstützt, übersetzt das Framework sie bei der Wiedergabe automatisch nach bestem Wissen und Gewissen in eine geeignete Alternative. Sie müssen die Gerätefunktionen für jedes Primitive nicht manuell prüfen, bevor Sie mitVibrationEffect.Builderzusammengesetzte Effekte abspielen.- Ausnahme:Erweiterte Wellenform-Umschläge, die mit
WaveformEnvelopeBuildererstellt wurden, unterstützen kein automatisches Fallback, da sie von bestimmten Hardware-Frequenzzuordnungen (FOAM) abhängen. Wenn sie nicht unterstützt werden, werden sie nicht wiedergegeben.
- Ausnahme:Erweiterte Wellenform-Umschläge, die mit
VibrationEffect.Composition:Für Kompositionen, die mit derstartComposition()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 mitvibrator.arePrimitivesSupported()prüfen.- Wellenformen mit Amplitudensteuerung:Nicht null betragende Amplituden werden auf Geräten ohne Amplitudensteuerung auf 100% aufgerundet. Prüfe
vibrator.hasAmplitudeControl()und greife bei Bedarf auf ein explizit entworfenes AN/AUS-Muster zurück.
Verwendung von haptischen Primitiven
Android enthält mehrere Haptik-Primitive, die sich in Amplitude und Frequenz unterscheiden. Sie können ein einzelnes Primitiv oder mehrere Primitive in Kombination verwenden, um komplexe haptische Effekte zu erzielen.
- Verwenden Sie Verzögerungen von mindestens 50 ms, um erkennbare Lücken zwischen zwei Primitiven zu erzeugen. Berücksichtigen Sie dabei nach Möglichkeit auch die Dauer des Primitivs.
- Verwenden Sie Skalen, die sich um das 1,4-Fache 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 für haptische Benachrichtigungen wie Benachrichtigungen und Klingeltöne verwendet. 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 in der Regel wahrnehmbar, aber plötzliche lange Vibrationen können den Nutzer in einer ruhigen Umgebung erschrecken. Wenn die Zielamplitude zu schnell erreicht wird, kann das auch zu hörbaren Summtönen führen. Entwerfen Sie Wellenformmuster, um die Amplitudenübergänge zu glätten und Ramp-up- und Ramp-down-Effekte zu erzielen.
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:
- Timings:Ein Array mit Dauern in Millisekunden für jedes Wellenformsegment.
- Amplituden:Die gewünschte Vibrationsamplitude für jede im ersten Argument angegebene Dauer, dargestellt als Ganzzahlwert zwischen 0 und 255. Dabei steht 0 für den „Aus“-Zustand des Vibrators und 255 für die maximale Amplitude des Geräts.
- Wiederholungsindex:Der Index im Array, der im ersten Argument angegeben ist, ab dem die Wellenform wiederholt werden soll, oder -1, wenn das Muster nur einmal wiedergegeben werden soll.
Hier ist ein Beispiel für eine Wellenform, die zweimal pulsiert, mit einer Pause von 350 ms zwischen den Pulsen. Der erste Impuls ist ein sanfter Anstieg zur maximalen Amplitude und der zweite ein schneller Anstieg zur maximalen Amplitude. Das Ende wird durch den negativen Wiederholungsindexwert definiert.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't 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; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
Sich wiederholendes Muster
Wellenformen können auch wiederholt abgespielt werden, bis sie abgebrochen werden. Um eine sich wiederholende Wellenform zu erstellen, müssen Sie einen nicht negativen repeat-Parameter festlegen. Wenn Sie eine sich wiederholende Wellenform abspielen, vibriert das Gerät so lange, bis die Vibration im Dienst explizit beendet 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 zeitweilige Ereignisse, die eine Nutzeraktion erfordern, um sie zu bestätigen. Beispiele für solche Ereignisse sind eingehende Anrufe und ausgelöste Alarme.
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 Ihre App solche Geräte unterstützen muss, verwenden Sie entweder ein Muster, das unter diesen Bedingungen keinen summenden Effekt erzeugt, oder entwerfen Sie ein einfacheres EIN/AUS-Muster, das stattdessen als Fallback verwendet 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));
}
Zeitachsenbasierte Kompositionen mit VibrationEffect.Builder
Ab Android 16 (4. Quartal 2026) ist VibrationEffect.Builder die bevorzugte API zum Erstellen komplexer Vibrationseffekte und ‑kompositionen. Damit können Sie ausdrucksstarke haptische Empfindungen erzeugen, indem Sie diskrete haptische Elemente mit startTimeMillis entlang einer absoluten Zeitachse anordnen.
VibrationEffect.Builder unterstützt die Kombination mehrerer Vibrationstypen:
- Voreinstellungen:Vordefinierte haptische Impulse (
VibrationEffect.Preset), z. B. Klicks und Ticks. - Hüllkurven:Dynamische kontinuierliche Wellenformen, einschließlich hardwareunabhängiger einfacher Hüllkurven (
BasicEnvelopeBuilder) und erweiterter frequenzmodulierter Hüllkurven (WaveformEnvelopeBuilder). - Vorhandene VibrationEffects:Primitive Kompositionen (
VibrationEffect.Composition), Stufenwellenformen (VibrationEffect.createWaveform), vordefinierte Effekte (VibrationEffect.createPredefined) und One-Shots (VibrationEffect.createOneShot), die mitaddEvents()oder durch Kopieren des Konstruktors importiert wurden. - Wiederholende Sequenzen:Kontinuierliche Muster, die mit
setRepeatingEffect()konfiguriert wurden.
Vibrations, die mit VibrationEffect.Builder erstellt wurden, haben eine integrierte automatische Fallback-Funktion auf Framework-Ebene für alle Elemente (einschließlich Voreinstellungen, einfachen 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 können Sie einer Komposition häufig verwendete, vordefinierte kurze haptische Impulse wie PRESET_CLICK, PRESET_TICK oder PRESET_LOW_TICK hinzufügen.
Voreinstellungen werden vom Gerätehersteller implementiert, um eine klare, kurze und angenehme Vibration zu erzeugen, die den Haptikprinzipien für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie unter Vibrationsaktoren – Grundlagen.
Voreinstellungen ersetzen die kurzen Primitiven aus der VibrationEffect.Composition API.
Für längere oder kontinuierliche haptische Empfindungen, z. B. das Hoch- und Herunterfahren, verwenden Sie stattdessen Hüllkurven-Wellenformen (PWLE).
Jedem Preset kann optional ein Skalierungswert zwischen 0.0f und 1.0f zugewiesen werden. Außerdem wird es zu einer bestimmten 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 Presets komponieren
Sie können VibrationEffect.Envelope-Instanzen (erstellt mit BasicEnvelopeBuilder oder WaveformEnvelopeBuilder) nahtlos mit Voreinstellungen kombinieren, um umfangreiche, mehrsegmentige haptische Muster zu erstellen.
Hier ist ein Beispiel, in dem zuerst ein sanfter Ramp-up- und Fade-out-Umschlag und dann ein scharfes Klick-Preset wiedergegeben wird:
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 eine vorhandene 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 Primitives automatisch in Voreinstellungen, was die Unterstützung von Laufzeit-Fallbacks ermöglicht.
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);
Wiederholende Kompositionen erstellen
Mit setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) können Sie einer Komposition ein sich wiederholendes Muster hinzufü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);
Zeitplanung, Validierung und Drift-Management
Beachten Sie beim Erstellen von Kompositionen mit VibrationEffect.Builder die folgenden Zeit- und Validierungsregeln:
- Streng aufsteigende Startzeiten:Jedes Element, das dem Builder hinzugefügt wird, muss eine
startTimeMillishaben, die größer oder gleich der Startzeit des vorherigen Elements ist. - Validierung zur Build-Zeit:Der Builder führt zur
build()-Zeit eine bestmögliche Validierung mit bekannten Elementdauern (oder einem Mindestwert von 1 ms für Presets) durch. Wenn eine unmögliche Überschneidung erkannt wird, wird eineIllegalArgumentExceptionausgelöst. - Sequenzielle Wiedergabeverschiebung: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 den frühesten verfügbaren Zeitraum. So wird sichergestellt, dass sich Ereignisse nicht überschneiden und keine Vibrationen verloren gehen. Allerdings kann es zu leichten Zeitabweichungen kommen, wenn Ereignisse zu dicht beieinander geplant werden. Um Abweichungen zu minimieren, sollte zwischen aufeinanderfolgenden haptischen Ereignissen genügend Zeit (z.B. 50 ms oder mehr) liegen.
Kompositionen aus Vibrationsprimitiven erstellen
In diesem Abschnitt wird beschrieben, wie du mit VibrationEffect.Composition Vibrationen erstellst. Die Kompositions-Primitive, die weiter oben auf dieser Seite beschrieben werden, werden vom Gerätehersteller implementiert. Sie bieten eine kurze, angenehme Vibration, die den Haptik-Grundsätzen für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie unter Vibrationsaktoren – Grundlagen.
Im Gegensatz zu VibrationEffect.Builder bietet die VibrationEffect.Composition API keine automatischen Fallbacks für nicht unterstützte Primitiven. Dementsprechend gilt:
Bevor du die erweiterten Haptikfunktionen aktivierst, solltest du prüfen, ob ein bestimmtes Gerät alle verwendeten Primitiven unterstützt.
Deaktiviere die einheitliche Gruppe von Varianten, die nicht unterstützt werden, und nicht nur die Effekte, bei denen 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, werden die Kompositions-Primitiven in Voreinstellungen umgewandelt und es wird automatisch ein Laufzeit-Fallback bereitgestellt, falls ein Primitiv auf dem Gerät des Nutzers nicht unterstützt wird.
Zusammengesetzte Vibrationseffekte
Mit VibrationEffect.Composition können Sie zusammengesetzte 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 erstellt, indem Primitives hinzugefügt werden, die nacheinander abgespielt werden. Jedes Primitive ist außerdem skalierbar, sodass Sie die Amplitude der Vibration steuern können, die von jedem Primitive erzeugt wird. Die Skala ist als Wert zwischen 0 und 1 definiert, wobei 0 einer minimalen Amplitude entspricht, bei der dieses Primitive vom Nutzer (kaum) wahrgenommen werden kann.
Varianten in Vibrationsprimitiven erstellen
Wenn Sie eine schwache und eine starke Version desselben Primitivs erstellen möchten, sollten Sie Stärkeverhältnisse von mindestens 1,4 verwenden, damit der Unterschied in der Intensität deutlich wahrgenommen werden kann. Versuchen Sie nicht, mehr als drei Intensitätsstufen desselben Primitivs zu erstellen, da sie sich nicht wahrnehmbar unterscheiden. 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.
Lücken zwischen Vibrationsprimitiven einfügen
In der Komposition können auch Verzögerungen zwischen aufeinanderfolgenden Primitiven angegeben werden. Diese Verzögerung wird in Millisekunden seit dem Ende des vorherigen Primitivs 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 eine erkennbare Lücke zwischen zwei Primitiven erstellen 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());
Unterstützte Primitives prüfen
Mit den folgenden APIs lässt sich die Geräteunterstützung für bestimmte Primitives überprü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 Primitives zu prüfen und dann basierend auf der Unterstützung des Geräts zu entscheiden, welche zusammengesetzt 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);
Beispiele für Vibrationsmuster
In den folgenden Abschnitten finden Sie mehrere Beispiele für Vibrationskompositionen aus der Haptik-Beispiel-App auf GitHub.
Widerstand (mit wenigen Ticks)
Sie können 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 sanfter Crescendo-Effekt eines Primitivs erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch auf Grundlage der Nutzerinteraktion festgelegt werden. Das wird im folgenden Beispiel einer Ansichtsanimation veranschaulicht, die durch eine Ziehbewegung gesteuert und durch Haptik ergänzt wird.
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);
}
}
Erweitern (mit Anstieg und Abfall)
Es gibt zwei Grundelemente, um die wahrgenommene Vibrationsintensität zu steigern: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, aber mit unterschiedlichen Laufzeiten. Es gibt nur ein Primitiv für das Herunterfahren, PRIMITIVE_QUICK_FALL. Diese Primitives funktionieren besser zusammen, um ein Wellenformsegment zu erstellen, das an Intensität zunimmt und dann abklingt. Sie können skalierte Primitives ausrichten, um plötzliche Amplitudensprünge zwischen ihnen zu vermeiden. Das ist auch eine gute Möglichkeit, die Gesamtdauer des Effekts zu verlängern.
Wahrnehmungstechnisch fällt der steigende Teil immer mehr auf als der fallende. Wenn Sie den steigenden Teil kürzer als den fallenden gestalten, können Sie den Schwerpunkt auf den fallenden Teil verlagern.
Hier sehen Sie ein Beispiel für die Anwendung dieser Komposition zum Ein- und Ausblenden eines Kreises. Der Rise-Effekt kann das Gefühl der Expansion während der Animation verstärken. Die Kombination aus Ein- und Ausblendeffekten trägt dazu bei, das Einblenden am Ende der Animation zu betonen.
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;
}
}
Wobble (mit Spins)
Eines der wichtigsten Haptikprinzipien ist es, Nutzer zu begeistern. Eine unterhaltsame Möglichkeit, einen angenehmen, unerwarteten Vibrationseffekt einzuführen, ist die Verwendung von PRIMITIVE_SPIN. Dieses Primitive ist am effektivsten, wenn es mehrmals aufgerufen wird. Durch das Verketten mehrerer Drehungen kann ein wackeliger und instabiler Effekt erzeugt werden, der durch das Anwenden einer zufälligen Skalierung auf jedes Primitive noch verstärkt werden kann. Sie können auch mit dem Abstand zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Drehungen ohne Lücke (0 ms dazwischen) erzeugen ein Gefühl der schnellen Drehung. Wenn Sie den Abstand zwischen den Drehungen von 10 auf 50 ms erhöhen, fühlt sich das Drehen weniger schnell an. So können Sie die Dauer an die eines Videos oder einer Animation anpassen.
Verwenden Sie keine Lücke, die länger als 100 ms ist, da die aufeinanderfolgenden Drehungen nicht mehr gut integriert werden und sich wie einzelne Effekte anfühlen.
Hier sehen Sie ein Beispiel für eine elastische Form, die nach dem Herunterziehen und Loslassen zurückspringt. Die Animation wird durch zwei Dreheffekte verstärkt, die mit unterschiedlichen Intensitäten abgespielt werden, die proportional zur Sprungverschiebung sind.
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 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)
}
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 the range [-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)
}
}
Hüpfen (mit dumpfen Geräuschen)
Eine weitere anspruchsvolle Anwendung von Vibrationseffekten ist die Simulation physischer Interaktionen. PRIMITIVE_THUD kann einen starken und nachhaltigen Effekt erzeugen, der mit der Visualisierung einer Wirkung kombiniert werden kann, z. B. in einem Video oder einer Animation, um das Gesamterlebnis zu verbessern.
Hier ist ein Beispiel für eine Animation, bei der ein Ball herunterfällt. Bei jedem Aufprall des Balls am unteren Bildschirmrand wird ein dumpfer Aufprall-Effekt abgespielt:
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;
}
}
});
}
}
Vibrationswellenform mit Hüllkurven
Beim Erstellen benutzerdefinierter Vibrationsmuster können Sie die Vibrationsamplitude steuern, um sanfte Effekte beim Hoch- und Herunterfahren zu erzielen. In diesem Abschnitt wird beschrieben, wie Sie dynamische haptische Effekte mit Wellenform-Umschlägen erstellen, die eine präzise Steuerung der Vibrationsamplitude und -frequenz im Zeitverlauf ermöglichen. So können Sie komplexere und differenziertere haptische Erlebnisse schaffen.
Ab Android 16 (API-Level 36) bietet das System die folgenden APIs zum Erstellen einer Vibrationswellenform-Hüllkurve, indem eine Sequenz von Kontrollpunkten definiert wird:
BasicEnvelopeBuilder:Ein barrierefreier Ansatz zum Erstellen von hardwareunabhängigen haptischen Effekten.WaveformEnvelopeBuilder:Eine komplexere Methode zum Erstellen haptischer Effekte, die Kenntnisse der Haptik-Hardware erfordert.
Android bietet keine Fallbacks für Hüllkurven-Effekte. Wenn Sie diese Unterstützung benötigen, führen Sie die folgenden Schritte aus:
- Mit
Vibrator.areEnvelopeEffectsSupported()können Sie prüfen, ob ein bestimmtes Gerät Envelope-Effekte unterstützt. - Deaktiviere die einheitliche Gruppe von Funktionen, die nicht unterstützt werden, oder verwende benutzerdefinierte Vibrationsmuster oder Kompositionen als Fallback-Alternativen.
Wenn Sie einfachere Hüllkurveneffekte erstellen möchten, verwenden Sie BasicEnvelopeBuilder mit den folgenden Parametern:
- Ein intensity-Wert im Bereich \( [0, 1] \), der die wahrgenommene Stärke der Vibration darstellt. Ein Wert von \( 0.5 \)entspricht beispielsweise der Hälfte der maximalen globalen Intensität, die das Gerät erreichen kann.
Ein Schärfe-Wert im Bereich \( [0, 1] \), der die Schärfe der Vibration darstellt. Niedrigere Werte führen zu sanfteren Vibrationen, während höhere Werte ein schärferes Gefühl erzeugen.
Ein duration-Wert, der die Zeit in Millisekunden angibt, die für den Übergang vom letzten Kontrollpunkt (ein Paar aus Intensität und Schärfe) zum neuen Kontrollpunkt benötigt wird.
Das folgende Beispiel zeigt eine Wellenform, bei der die Intensität über 500 ms von einer Vibration mit niedriger Tonhöhe zu einer Vibration mit hoher Tonhöhe und 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 Sie über fortgeschrittene Kenntnisse im Bereich Haptik verfügen, können Sie mit WaveformEnvelopeBuilder Hüllkurveneffekte definieren. Wenn Sie dieses Objekt verwenden, können Sie über VibratorFrequencyProfile auf die Zuordnung von Häufigkeit zu Output-Steigerung (Frequency-to-Output-Acceleration Mapping, FOAM) zugreifen.
- Ein Amplitudenwert im Bereich \( [0, 1] \), der die erreichbare Vibrationsstärke bei einer bestimmten Frequenz darstellt, wie sie vom Geräte-FOAM bestimmt wird. Ein Wert von \( 0.5 \) erzeugt beispielsweise die Hälfte der maximalen Ausgabebeschleunigung, die bei der angegebenen Frequenz erreicht werden kann.
Ein frequency-Wert, der in Hertz angegeben wird.
Ein duration-Wert, der die Zeit in Millisekunden angibt, die für den Übergang vom letzten zum neuen Kontrollpunkt benötigt wird.
Der folgende Code zeigt ein Beispiel für eine Wellenform, die einen 400 ms langen Vibrationseffekt definiert. Zuerst wird die Amplitude über 50 ms von „Aus“ auf „Voll“ gesteigert, bei einer konstanten Frequenz von 60 Hz. Dann wird die Frequenz über die nächsten 100 ms auf 120 Hz gesteigert und bleibt 200 ms lang auf diesem Niveau. Schließlich wird die Amplitude über die letzten 50 ms auf \( 0 \)gesenkt und die Frequenz kehrt zu 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 Schwingungsformen mit Hüllkurven.
Springende Feder
In einem früheren Beispiel wird PRIMITIVE_THUD verwendet, um Interaktionen mit physischen Sprungbewegungen zu simulieren. Die Basic Envelope API bietet eine deutlich feinere Steuerung, mit der Sie die Vibrationsintensität und -schärfe präzise anpassen können.
Das führt zu einem haptischen Feedback, das animierten Ereignissen genauer folgt.
Hier ein Beispiel für eine ungehindert fallende Feder, bei der die Animation durch einen einfachen Hüllkurveneffekt verstärkt wird, der jedes Mal abgespielt wird, wenn die Feder vom unteren Bildschirmrand abprallt:
Abbildung 5: Ein Diagramm mit der Beschleunigung als Ausgabe für eine Schwingung, die eine hüpfende Feder simuliert.
@Composable
fun BouncingSpringAnimation() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
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(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
Raketenstart
In einem früheren Beispiel wurde gezeigt, wie die Envelope API verwendet wird, um eine Reaktion wie bei einer Feder zu simulieren. Die WaveformEnvelopeBuilder ermöglicht eine präzise Steuerung des gesamten Frequenzbereichs des Geräts und damit hochgradig angepasste haptische Effekte. Wenn Sie diese Daten mit FOAM-Daten kombinieren, können Sie Vibrationen an bestimmte Frequenzfunktionen anpassen.
Hier ist ein Beispiel für eine Raketenstartsimulation mit einem dynamischen Vibrationsmuster. Der Effekt reicht von der minimal unterstützten Frequenzbeschleunigungsausgabe (0,1 G) bis zur Resonanzfrequenz, wobei immer eine Amplitude von 10 % beibehalten wird. So kann der Effekt mit einer relativ starken Ausgabe beginnen und die wahrgenommene Intensität und Schärfe erhöhen, obwohl die treibende Amplitude gleich ist. Bei Erreichen der Resonanz 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 Freigabe, die einem Start ins Weltall ähnelt.
Dieser Effekt ist mit der Basic Envelope API nicht möglich, da sie gerätespezifische Informationen zur Resonanzfrequenz und zur Ausgabebeschleunigungskurve abstrahiert. Wenn Sie die Schärfe erhöhen, kann die entsprechende Frequenz über die Resonanz hinausgehen, was möglicherweise zu einem unbeabsichtigten Einbruch der Beschleunigung führt.
Abbildung 6. Ein Diagramm mit der Beschleunigungsausgabe für eine Schwingung, die einen Raketenstart simuliert.
@Composable
fun RocketLaunchAnimation() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
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)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
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, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
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 - rampUpDuration - 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, da 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“.
Dies kann erreicht werden, indem Biomarker eines typischen Elektrokardiogramm-Signals (EKG) durch Vibrationssegmente mit bestimmten Amplituden und Frequenzen dargestellt werden. 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 scharfer Peak mit hoher Amplitude und kurzer Dauer dargestellt 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 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 über einen kurzen Zeitraum mit einer niedrigen Frequenz beginnt und mit 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 Verzögerung wiederholen, wobei wir uns an einer typischen Beats-per-Minute-Rate (bpm) orientieren. Das Ergebnis ist ein haptischer Effekt, der einem schlagenden Herzen ähnelt.
Sie können LavaBeats in unserer Haptik-Beispiel-App auf GitHub ausprobieren und den Effekt zusammen mit einer Lavalampe-Visualisierung erleben, die im gleichen Rhythmus wie der haptische Effekt pulsiert. Sie können auch die Einstellungen des Effekts ändern, um durch Modifizieren der Amplituden, Frequenzen, Dauern und Verzögerungen der beiden Impulse unterschiedliche Schwebungsempfindungen zu erzeugen.
Abbildung 7. Ein Segment einer EKG-Aufzeichnung mit dem QRS-Komplex und der T-Welle
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
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 */
@Stable
data class BeatParameter(
val description: String = "",
val value: Float = 0f,
val range: ClosedFloatingPointRange<Float> = 0f..1f,
val steps: Int = 0,
val isFrequencyType: Boolean = false,
)