Przykłady i wskazówki dotyczące tworzenia niestandardowych efektów haptycznych na Androidzie, w tym kompozycji zakotwiczonych na osi czasu z użyciem klasy VibrationEffect.Builder, niestandardowych wzorców wibracji i zaawansowanych obwiedni fal.keywords_public: > Android, haptyka, efekty niestandardowe, wibracje, interfejsy API haptyki, wzorce wibracji, VibrationEffect.Builder, kompozycje, elementy haptyczne, obwiednie fal, interfejs
Na tej stronie znajdziesz przykłady użycia różnych interfejsów API haptyki do tworzenia w aplikacji na Androida niestandardowych efektów wykraczających poza standardowe kształty fal wibracji.
Na tej stronie znajdziesz te przykłady:
- Kompozycje zakotwiczone na osi czasu z
VibrationEffect.Builder- Tworzenie z gotowych ustawień: sekwencjonowanie z góry określonych wrażeń dotykowych.
- Komponowanie za pomocą obwiedni i gotowych ustawień: łącz obwiednie i gotowe ustawienia na osi czasu.
- Ponowne wykorzystywanie i przesuwanie zdarzeń: przesuwaj i ponownie wykorzystuj istniejące zdarzenia kompozycji.
- Powtarzające się kompozycje: twórz powtarzające się efekty na osi czasu.
- Niestandardowe wzory wibracji
- Wzrostowy: wzór, który zaczyna się łagodnie.
- Wzorzec powtarzający się: wzorzec bez końca.
- Wzór z wartością zastępczą: demonstracja wartości zastępczej.
- Kompozycje elementów wibracji
- Opór: efekt przeciągania o dynamicznej intensywności.
- Rozszerzanie: efekt wzrostu, a następnie spadku.
- Wobble: efekt drgania wykorzystujący element
SPIN. - Odbijanie: efekt odbijania wykorzystujący element pierwotny
THUD.
- Kształt fali wibracji z obwiedniami
- Sprężynowe odbicie: efekt sprężynowego odbicia z wykorzystaniem podstawowych efektów obwiedni.
- Start rakiety: efekt startu rakiety z użyciem efektów obwiedni fali.
Więcej przykładów znajdziesz w artykule Dodawanie do zdarzeń informacji o wibracjach. Zawsze postępuj zgodnie z zasadami projektowania wibracji.
Używanie funkcji rezerwowych do obsługi zgodności urządzeń
Podczas wdrażania niestandardowych efektów haptycznych zgodność urządzenia i zachowanie rezerwowe zależą od wybranego interfejsu API:
VibrationEffect.Builder(zalecane): od Androida 16 (IV kwartał 2026 r.) efekty utworzone za pomocąVibrationEffect.Builderobejmują automatyczne wycofywanie na poziomie platformy. Jeśli urządzenie nie obsługuje natywniePresetlub podstawowegoEnvelope, platforma automatycznie przetłumaczy je na odpowiednią alternatywę w czasie odtwarzania, dokładając wszelkich starań. Nie musisz ręcznie sprawdzać możliwości poszczególnych urządzeń przed odtwarzaniem efektów utworzonych za pomocąVibrationEffect.Builder.- Wyjątek: zaawansowane obwiednie kształtu fali utworzone za pomocą narzędzia
WaveformEnvelopeBuildernie obsługują automatycznego powrotu do poprzedniej wersji, ponieważ zależą od konkretnych mapowań częstotliwości sprzętu (FOAM). Jeśli nie są obsługiwane, nie będą odtwarzane.
- Wyjątek: zaawansowane obwiednie kształtu fali utworzone za pomocą narzędzia
VibrationEffect.Composition: kompozycje utworzone za pomocą interfejsustartComposition()API nie mają automatycznych wersji zapasowych. Jeśli kompozycja zawiera choć jeden nieobsługiwany element, cała wibracja nie zostanie odtworzona. Możliwości musisz sprawdzić ręcznie za pomocą poleceniavibrator.arePrimitivesSupported().- Kształty fal z regulacją amplitudy: na urządzeniach bez regulacji amplitudy wartości amplitudy inne niż zero są zaokrąglane w górę do 100%. Sprawdź
vibrator.hasAmplitudeControl()i w razie potrzeby wróć do wyraźnie zaprojektowanego wzorca WŁ./WYŁ.
Używanie podstawowych elementów haptycznych
Android zawiera kilka podstawowych efektów dotykowych, które różnią się amplitudą i częstotliwością. Możesz użyć jednego elementu podstawowego lub kilku elementów podstawowych w połączeniu, aby uzyskać bogate efekty haptyczne.
- Używaj opóźnień wynoszących co najmniej 50 ms, aby uzyskać zauważalne przerwy między dwoma elementami podstawowymi. W miarę możliwości uwzględniaj też czas trwania elementu podstawowego.
- Używaj skal, które różnią się o współczynnik 1,4 lub większy, aby różnica w intensywności była lepiej widoczna.
Użyj skal 0,5, 0,7 i 1,0, aby utworzyć wersję kształtu podstawowego o niskiej, średniej i wysokiej intensywności.
Tworzenie niestandardowych wzorów wibracji
Wzorce wibracji są często używane w przypadku reakcji haptycznych związanych z uwagą, takich jak powiadomienia i dzwonki. Usługa Vibrator może odtwarzać długie wzorce wibracji, które zmieniają amplitudę wibracji w czasie. Takie efekty nazywamy kształtami fali.
Efekty wibracji są zwykle zauważalne, ale nagłe długie wibracje mogą zaskoczyć użytkownika, jeśli są odtwarzane w cichym otoczeniu. Zbyt szybkie osiąganie docelowej amplitudy może też powodować słyszalne brzęczenie. Projektuj wzorce kształtu fali, aby wygładzać przejścia amplitudy i tworzyć efekty narastania i opadania.
Przykłady wzorów wibracji
W sekcjach poniżej znajdziesz kilka przykładów wzorców wibracji:
Wzór zwiększania wyświetlania
Kształty fal są reprezentowane jako VibrationEffect z 3 parametrami:
- Timings: tablica czasów trwania (w milisekundach) poszczególnych segmentów fali.
- Amplitudy: żądana amplituda wibracji dla każdego czasu trwania określonego w pierwszym argumencie, reprezentowana przez liczbę całkowitą z zakresu od 0 do 255, gdzie 0 oznacza „wyłączony” wibrator, a 255 – maksymalną amplitudę urządzenia.
- Indeks powtórzenia: indeks w tablicy określonej w pierwszym argumencie, od którego ma się rozpocząć powtarzanie fali dźwiękowej, lub -1, jeśli wzorzec ma być odtwarzany tylko raz.
Oto przykład przebiegu fali, który pulsuje 2 razy z przerwą 350 ms między impulsami. Pierwszy impuls to płynne zwiększanie amplitudy do maksymalnej wartości, a drugi to szybkie zwiększanie amplitudy do maksymalnej wartości. Zatrzymanie na końcu jest określone przez ujemną wartość indeksu powtórzeń.
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));
Powtarzający się wzór
Kształty fal można też odtwarzać wielokrotnie, dopóki nie zostaną anulowane. Aby utworzyć powtarzający się kształt fali, ustaw nieujemny parametr repeat. Gdy odtwarzasz powtarzający się kształt fali, wibracje trwają, dopóki nie zostaną wyraźnie anulowane w usłudze:
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();
}
Jest to bardzo przydatne w przypadku zdarzeń przerywanych, które wymagają potwierdzenia przez użytkownika. Przykłady takich zdarzeń to połączenia przychodzące i włączone alarmy.
Wzorzec z wersją zastępczą
Kontrolowanie amplitudy wibracji jest funkcją zależną od sprzętu. Odtwarzanie fali na urządzeniu z niższej półki bez tej funkcji powoduje wibracje urządzenia z maksymalną amplitudą dla każdego dodatniego wpisu w tablicy amplitud. Jeśli Twoja aplikacja musi obsługiwać takie urządzenia, użyj wzorca, który nie generuje efektu brzęczenia w takich warunkach, lub zaprojektuj prostszy wzorzec włączania/wyłączania, który można odtworzyć jako alternatywę.
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));
}
Kompozycje zakotwiczone na osi czasu z VibrationEffect.Builder
Od Androida 16 (czwarty kwartał 2026 r.) VibrationEffect.Builder jest preferowanym interfejsem API do tworzenia złożonych efektów wibracji i kompozycji. Umożliwia tworzenie ekspresywnych wrażeń haptycznych poprzez sekwencjonowanie dyskretnych elementów haptycznych wzdłuż bezwzględnej osi czasu z użyciem startTimeMillis.
VibrationEffect.Builder obsługuje łączenie wielu typów wibracji:
- Ustawienia wstępne: wstępnie zdefiniowane impulsy haptyczne (
VibrationEffect.Preset), takie jak kliknięcia i tyknięcia. - Obwiednie: dynamiczne ciągłe przebiegi, w tym podstawowe obwiednie niezależne od sprzętu (
BasicEnvelopeBuilder) i zaawansowane obwiednie modulowane częstotliwością (WaveformEnvelopeBuilder). - Istniejące efekty wibracji: proste kompozycje (
VibrationEffect.Composition), przebiegi schodkowe (VibrationEffect.createWaveform), predefiniowane efekty (VibrationEffect.createPredefined) i jednorazowe efekty (VibrationEffect.createOneShot) zaimportowane za pomocąaddEvents()lub kopiowania konstruktora. - Powtarzające się sekwencje: ciągłe wzorce skonfigurowane za pomocą symbolu
setRepeatingEffect().
Wibracje utworzone za pomocą VibrationEffect.Builder mają wbudowane automatyczne zastępowanie na poziomie platformy we wszystkich elementach (w tym w ustawieniach wstępnych, podstawowych obwiedniach i połączonych efektach wibracji), co zapewnia spójne wrażenia użytkowników na różnych urządzeniach bez konieczności ręcznego sprawdzania możliwości. (Zaawansowane obwiednie kształtu fali utworzone za pomocą
WaveformEnvelopeBuilder wymagają obsługi sprzętowej i nie mają automatycznego
mechanizmu rezerwowego).
Tworzenie za pomocą gotowych ustawień
Użyj VibrationEffect.Preset, aby dodać do kompozycji typowe, wstępnie zdefiniowane krótkie impulsy haptyczne (np. PRESET_CLICK, PRESET_TICK lub PRESET_LOW_TICK).
Gotowe ustawienia są wdrażane przez producenta urządzenia, aby zapewnić wyraźne, krótkie i przyjemne wibracje, które są zgodne z zasadami haptyki. Więcej informacji o tych możliwościach i sposobie ich działania znajdziesz w artykule Wprowadzenie do siłowników wibracyjnych.
Gotowe ustawienia zastępują krótkie elementy podstawowe z interfejsu VibrationEffect.Composition API.
W przypadku dłuższych lub ciągłych wibracji, takich jak stopniowe zwiększanie i zmniejszanie intensywności, używaj fal obwiedni (PWLE).
Każdemu ustawieniu wstępnemu można przypisać opcjonalną skalę od 0.0f do 1.0f i umieścić je w określonym czasie rozpoczęcia (w milisekundach) od początku kompozycji.
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);
Tworzenie kompozycji za pomocą obwiedni i gotowych ustawień
Możesz płynnie łączyć instancje VibrationEffect.Envelope (utworzone za pomocą BasicEnvelopeBuilder lub WaveformEnvelopeBuilder) z ustawieniami wstępnymi, aby tworzyć bogate, wielosegmentowe wzorce haptyczne.
Oto przykład, w którym odtwarzana jest płynna obwiednia narastania i zanikania, a następnie ostry klik:
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);
Ponowne wykorzystywanie i przesuwanie istniejących wydarzeń
Aby ponownie użyć lub połączyć istniejący obiekt VibrationEffect (w tym VibrationEffect.Composition), pobierz listę obiektów VibrationEffect.Event za pomocą funkcji getEvents() i dołącz je z przesunięciem za pomocą funkcji addEvents(startTimeShiftMillis, events) (lub przekaż efekt bezpośrednio do konstruktora VibrationEffect.Builder(effect)). Podczas importowania w ten sposób instancji VibrationEffect.Composition framework automatycznie przekształca ich typy proste w ustawienia wstępne, co umożliwia obsługę rezerwowego działania w czasie działania.
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);
Tworzenie powtarzających się kompozycji
Użyj setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis), aby dodać powtarzający się wzór do kompozycji:
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);
Czas, weryfikacja i zarządzanie dryfem
Podczas projektowania kompozycji z VibrationEffect.Builder pamiętaj o tych regułach dotyczących czasu i weryfikacji:
- Ściśle rosnące czasy rozpoczęcia: każdy element dodany do kreatora musi mieć wartość
startTimeMillis, która jest ściśle większa lub równa czasowi rozpoczęcia poprzedniego elementu. - Weryfikacja w czasie kompilacji: kompilator przeprowadza weryfikację w czasie
build()kompilacji, korzystając ze znanych czasów trwania elementów (lub z minimalnego czasu trwania 1 ms w przypadku gotowych ustawień). Jeśli wykryta zostanie niemożliwa zbieżność, funkcja zwraca wartośćIllegalArgumentException. - Sekwencyjne przesuwanie odtwarzania: jeśli poprzedni element wibracji jest nadal fizycznie odtwarzany, gdy nadejdzie czas rozpoczęcia następnego elementu, platforma automatycznie przesunie następny element do najwcześniejszego dostępnego przedziału czasu. Dzięki temu zdarzenia nie nakładają się na siebie i nie są pomijane wibracje, ale może to powodować niewielkie odchylenia czasowe, jeśli zdarzenia są zaplanowane zbyt blisko siebie. Aby zminimalizować dryfowanie, odczekaj wystarczająco długo (np. 50 ms lub więcej) między kolejnymi zdarzeniami haptycznymi.
Tworzenie kompozycji z podstawowych elementów wibracji
W tej sekcji dowiesz się, jak tworzyć wibracje za pomocą VibrationEffect.Composition. Podstawowe elementy kompozycji, opisane wcześniej na tej stronie, są wdrażane przez producenta urządzenia. Zapewniają one wyraźne wibracje, które są krótkie i przyjemne w odbiorze, a także zgodne z zasadami haptyki. Więcej informacji o tych możliwościach i sposobie ich działania znajdziesz w tym artykule.
W przeciwieństwie do VibrationEffect.Builder interfejs VibrationEffect.Composition API nie ma automatycznych rozwiązań zastępczych w przypadku nieobsługiwanych typów prostych. Dlatego:
Zanim aktywujesz zaawansowane wibracje, sprawdź, czy dane urządzenie obsługuje wszystkie używane przez Ciebie elementy podstawowe.
Wyłącz spójny zestaw nieobsługiwanych funkcji, a nie tylko efekty, w których brakuje elementu podstawowego.
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(lub.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(lub.addEvents(0L, compositionEffect.getEvents()))
Gdy jest tworzony za pomocą VibrationEffect.Builder, framework przekształca podstawowe elementy kompozycji w ustawienia wstępne i automatycznie zapewnia rezerwowe działanie w czasie działania, jeśli którykolwiek z elementów podstawowych nie jest obsługiwany na urządzeniu użytkownika.
Złożone efekty wibracji
Możesz tworzyć złożone efekty wibracji za pomocą funkcji VibrationEffect.Composition. Oto przykład efektu powolnego wzrostu, po którym następuje gwałtowne kliknięcie:
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());
Kompozycja powstaje przez dodanie elementów podstawowych, które mają być odtwarzane po kolei. Każdy z nich jest też skalowalny, więc możesz kontrolować amplitudę wibracji generowanych przez poszczególne elementy. Skala jest zdefiniowana jako wartość z zakresu od 0 do 1, przy czym 0 odpowiada minimalnej amplitudzie, przy której użytkownik może (ledwo) odczuwać to działanie.
Tworzenie wariantów w podstawowych elementach wibracji
Jeśli chcesz utworzyć słabszą i mocniejszą wersję tego samego elementu pierwotnego, utwórz współczynniki siły wynoszące co najmniej 1,4, aby różnica w intensywności była łatwo dostrzegalna. Nie próbuj tworzyć więcej niż 3 poziomów intensywności tego samego kształtu podstawowego, ponieważ nie są one postrzegane jako odrębne. Możesz na przykład użyć skal 0,5, 0,7 i 1,0, aby utworzyć wersje elementu pierwotnego o niskiej, średniej i wysokiej intensywności.
Dodawanie przerw między podstawowymi elementami wibracji
Kompozycja może też określać opóźnienia, które mają być dodawane między kolejnymi elementami podstawowymi. Opóźnienie jest wyrażone w milisekundach od końca poprzedniego elementu. Ogólnie rzecz biorąc, odstęp od 5 do 10 ms między 2 elementami jest zbyt krótki, aby można go było wykryć. Jeśli chcesz utworzyć zauważalną przerwę między 2 elementami, użyj przerwy o długości około 50 ms lub dłuższej. Oto przykład kompozycji z opóźnieniami:
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());
Sprawdzanie, które typy proste są obsługiwane
Do weryfikacji obsługi konkretnych elementów pierwotnych przez urządzenie można użyć tych interfejsów API:
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.
}
Możesz też sprawdzić wiele elementów podstawowych, a potem zdecydować, które z nich chcesz skomponować, na podstawie poziomu obsługi urządzenia:
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);
Przykłady kompozycji wibracji
W kolejnych sekcjach znajdziesz kilka przykładów kompozycji wibracji zaczerpniętych z przykładowej aplikacji haptycznej na GitHubie.
Odporność (z niskimi wartościami)
Możesz kontrolować amplitudę wibracji podstawowych, aby przekazywać przydatne informacje zwrotne o trwającym działaniu. Wartości skali umieszczone blisko siebie mogą służyć do tworzenia płynnego efektu crescendo elementu podstawowego. Opóźnienie między kolejnymi elementami może być też dynamicznie ustawiane na podstawie interakcji użytkownika. Ilustruje to poniższy przykład animacji widoku sterowanej gestem przeciągnięcia i wzbogaconej o haptykę.
Rysunek 1. Ta fala reprezentuje przyspieszenie wyjściowe wibracji na urządzeniu.
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);
}
}
Rozwijanie (z wzrostem i spadkiem)
Istnieją 2 podstawowe elementy zwiększające odczuwalną intensywność wibracji: PRIMITIVE_QUICK_RISE i PRIMITIVE_SLOW_RISE. Oba osiągają ten sam cel, ale w różnym czasie. Istnieje tylko 1 element podstawowy do zmniejszania wartości, PRIMITIVE_QUICK_FALL. Te elementy działają lepiej razem, tworząc segment fali, który rośnie w intensywności, a potem zanika. Możesz wyrównać skalowane elementy, aby zapobiec nagłym skokom amplitudy między nimi, co sprawdza się również w przypadku wydłużania ogólnego czasu trwania efektu.
Ludzie zawsze bardziej zauważają część rosnącą niż malejącą, więc skrócenie części rosnącej w stosunku do malejącej może przesunąć nacisk na część malejącą.
Oto przykład zastosowania tej kompozycji do rozwijania i zwijania okręgu. Efekt wznoszenia może wzmocnić wrażenie ekspansji podczas animacji. Połączenie efektów wznoszenia i opadania pomaga podkreślić zwijanie na końcu animacji.
Ilustracja 2. Ten przebieg przedstawia przyspieszenie wyjściowe wibracji na urządzeniu.
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 (z obrotami)
Jedną z głównych zasad dotyczących haptyki jest sprawianie użytkownikom przyjemności. Zabawnym sposobem na wprowadzenie przyjemnego, nieoczekiwanego efektu wibracji jest użycie PRIMITIVE_SPIN. Ten element jest najbardziej skuteczny, gdy jest wywoływany więcej niż raz. Połączenie wielu obrotów może wywołać efekt chwiejności i niestabilności, który można dodatkowo wzmocnić, stosując nieco losowe skalowanie każdego elementu. Możesz też eksperymentować z odstępem między kolejnymi elementami obrotu. Dwa obroty bez przerwy (0 ms między nimi) wywołują silne wrażenie wirowania. Zwiększenie odstępu między obrotami z 10 ms do 50 ms powoduje luźniejsze obracanie i może być używane do dopasowania czasu trwania filmu lub animacji.
Nie używaj przerw dłuższych niż 100 ms, ponieważ kolejne obroty nie będą już dobrze się łączyć i zaczną przypominać oddzielne efekty.
Oto przykład elastycznego kształtu, który po przeciągnięciu w dół i zwolnieniu wraca do pierwotnego stanu. Animacja jest wzbogacona o 2 efekty obrotu odtwarzane z różną intensywnością, która jest proporcjonalna do odległości odbicia.
Rysunek 3. Ta fala reprezentuje przyspieszenie wyjściowe wibracji na urządzeniu.
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)
}
}
Odbijanie (z głośnymi uderzeniami)
Innym zaawansowanym zastosowaniem efektów wibracji jest symulowanie interakcji fizycznych. PRIMITIVE_THUD może stworzyć silny i rozbrzmiewający efekt, który można połączyć z wizualizacją uderzenia, np. w filmie lub animacji, aby wzmocnić ogólne wrażenia.
Oto przykład animacji spadającej piłki z efektem uderzenia odtwarzanym za każdym razem, gdy piłka odbija się od dolnej krawędzi ekranu:
Rysunek 4. Ta fala reprezentuje przyspieszenie wyjściowe wibracji na urządzeniu.
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;
}
}
});
}
}
Przebieg wibracji z obwiedniami
Proces tworzenia niestandardowych wzorów wibracji umożliwia kontrolowanie amplitudy wibracji, aby tworzyć płynne efekty narastania i zanikania. W tej sekcji dowiesz się, jak tworzyć dynamiczne efekty haptyczne za pomocą obwiedni kształtu fali, które umożliwiają precyzyjne sterowanie amplitudą i częstotliwością wibracji w czasie. Dzięki temu możesz tworzyć bogatsze i bardziej zniuansowane wrażenia dotykowe.
Od Androida 16 (poziom API 36) system udostępnia te interfejsy API do tworzenia obwiedni przebiegu drgań przez zdefiniowanie sekwencji punktów kontrolnych:
BasicEnvelopeBuilder: przystępne podejście do tworzenia efektów haptycznych niezależnych od sprzętu.WaveformEnvelopeBuilder: bardziej zaawansowane podejście do tworzenia efektów haptycznych; wymaga znajomości sprzętu haptycznego.
Android nie zapewnia efektów obwiedni. Jeśli potrzebujesz tej pomocy, wykonaj te czynności:
- Sprawdź, czy dane urządzenie obsługuje efekty obwiedni, za pomocą funkcji
Vibrator.areEnvelopeEffectsSupported(). - Wyłącz spójny zestaw funkcji, które nie są obsługiwane, lub użyj niestandardowych wzorców wibracji albo kompozycji jako alternatywnych rozwiązań.
Aby utworzyć więcej podstawowych efektów obwiedni, użyj ikony BasicEnvelopeBuilder z tymi parametrami:
- Wartość intensity w zakresie \( [0, 1] \), która reprezentuje odczuwalną siłę wibracji. Na przykład wartość \( 0.5 \) jest postrzegana jako połowa maksymalnej intensywności, jaką może osiągnąć urządzenie.
Wartość ostrości w zakresie \( [0, 1] \), która reprezentuje wyrazistość wibracji. Niższe wartości oznaczają łagodniejsze wibracje, a wyższe – bardziej intensywne.
Wartość duration, która reprezentuje czas (w milisekundach) potrzebny na przejście od ostatniego punktu kontrolnego, czyli pary intensywności i ostrości, do nowego.
Oto przykład przebiegu, w którym intensywność wibracji rośnie od niskiej do wysokiej, maksymalnej siły wibracji w ciągu 500 ms, a następnie spada do\( 0 \) (wyłączone) w ciągu 100 ms.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
Jeśli masz bardziej zaawansowaną wiedzę na temat haptyki, możesz zdefiniować efekty obwiedni za pomocą WaveformEnvelopeBuilder. Korzystając z tego obiektu, możesz uzyskać dostęp do mapowania częstotliwości na przyspieszenie wyjściowe (FOAM) za pomocą VibratorFrequencyProfile.
- Wartość amplitudy w zakresie \( [0, 1] \), która reprezentuje osiągalną siłę wibracji przy danej częstotliwości, określoną przez FOAM urządzenia. Na przykład wartość \( 0.5 \) generuje połowę maksymalnego przyspieszenia wyjściowego, które można osiągnąć przy danej częstotliwości.
Wartość częstotliwości podana w hercach.
Wartość czasu trwania, która reprezentuje czas w milisekundach potrzebny na przejście od ostatniego punktu kontrolnego do nowego.
Poniższy kod przedstawia przykładowy kształt fali, który definiuje 400-milisekundowy efekt wibracji. Zaczyna się od 50-milisekundowego wzrostu amplitudy od zera do pełnej wartości przy stałej częstotliwości 60 Hz. Następnie w ciągu kolejnych 100 ms częstotliwość wzrasta do 120 Hz i utrzymuje się na tym poziomie przez 200 ms. Na koniec w ciągu ostatnich 50 ms amplituda spada do \( 0 \), a częstotliwość wraca do 60 Hz:
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()
)
W kolejnych sekcjach znajdziesz kilka przykładów przebiegów drgań z obwiedniami.
Sprężyna odbijająca
W poprzednim przykładzie użyto PRIMITIVE_THUD do symulowania interakcji z odbiciem fizycznym. Podstawowy interfejs API obwiedni zapewnia znacznie większą kontrolę, umożliwiając precyzyjne dostosowanie intensywności i ostrości wibracji.
Dzięki temu reakcja haptyczna lepiej odzwierciedla animowane zdarzenia.
Oto przykład spadającej sprężyny z animacją wzbogaconą o podstawowy efekt obwiedni, który jest odtwarzany za każdym razem, gdy sprężyna odbija się od dołu ekranu:
Rysunek 5. Wykres przebiegu przyspieszenia wyjściowego dla wibracji, która symuluje odbijającą się sprężynę.
@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")
}
}
}
Start rakiety
W poprzednim przykładzie pokazaliśmy, jak używać podstawowego interfejsu API obwiedni do symulowania reakcji sprężystej. WaveformEnvelopeBuilder umożliwia precyzyjne sterowanie pełnym zakresem częstotliwości urządzenia, co pozwala na uzyskanie wysoce spersonalizowanych efektów haptycznych. Łącząc te dane z danymi FOAM, możesz dostosować wibracje do określonych możliwości częstotliwości.
Oto przykład symulacji startu rakiety z użyciem dynamicznego wzorca wibracji. Efekt przechodzi od minimalnej obsługiwanej częstotliwości wyjściowej przyspieszenia, 0,1 G, do częstotliwości rezonansowej, zawsze przy zachowaniu 10% amplitudy wejściowej. Dzięki temu efekt zaczyna się od dość silnego sygnału wyjściowego i zwiększa postrzeganą intensywność i ostrość, mimo że amplituda sterująca jest taka sama. Po osiągnięciu rezonansu częstotliwość efektu spada z powrotem do minimum, co jest odbierane jako zmniejszenie intensywności i ostrości. Daje to wrażenie początkowego oporu, a następnie zwolnienia, co przypomina start w kosmos.
Nie jest to możliwe w przypadku podstawowego interfejsu API obwiedni, ponieważ ukrywa on informacje o częstotliwości rezonansowej i krzywej przyspieszenia wyjściowego urządzenia. Zwiększenie ostrości może spowodować przesunięcie równoważnej częstotliwości poza rezonans, co może spowodować niezamierzony spadek przyspieszenia.
Rysunek 6. Wykres fali przyspieszenia wyjściowego dla wibracji, która symuluje start rakiety.
@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
Podobnie jak w przykładzie Rocket Launch, interfejs WaveformEnvelopeBuilder API umożliwia projektowanie wielu złożonych efektów haptycznych poprzez sterowanie segmentami amplitudy i częstotliwości wibracji. Innym przykładem takiego projektu jest emulacja bardziej abstrakcyjnych doznań fizycznych, takich jak „żywotność”.
Można to osiągnąć, przedstawiając biomarkery typowego sygnału elektrokardiogramu (EKG) za pomocą segmentów wibracji o określonych amplitudach i częstotliwościach. LavaBeats to przykład, w którym 2 charakterystyczne segmenty zapisu EKG są reprezentowane jako 2 impulsy oddzielone opóźnieniem czasowym. Pierwszym charakterystycznym impulsem jest zespół QRS, który jest widoczny jako ostry pik o wysokiej amplitudzie i krótkim czasie trwania. Drugi impuls to fala T, która ma mniejszą amplitudę, dłuższą długość i bardziej płynny kształt (patrz rysunek 7).
Użyj WaveformEnvelopeBuilder, aby utworzyć różne powtórzenia tych 2 impulsów oddzielonych stałym opóźnieniem między pierwszym a drugim impulsem. Pierwszy impuls może być sygnałem typu chirp, który zaczyna się od niskiej częstotliwości i kończy na wyższej częstotliwości w krótkim czasie. Drugi impuls można przedstawić jako pojedynczy okres sinusoidy o niskiej częstotliwości. Możemy połączyć te 2 impulsy w rytm i powtórzyć go kilka razy z opóźnieniem, zgodnie z typową liczbą uderzeń na minutę (bpm). Efektem jest wibracja, która
przypomina bicie serca.
Możesz wypróbować LavaBeats w naszej przykładowej aplikacji haptycznej w GitHubie i poczuć efekt wibracji, któremu towarzyszy wizualizacja lampy lawowej pulsująca w tym samym rytmie co efekt haptyczny. Możesz też zmienić ustawienia efektu, aby uzyskać różne odczucia drgań, modyfikując amplitudy, częstotliwości, czasy trwania i opóźnienia dwóch impulsów.
Rysunek 7. Segment zapisu EKG z zespołem QRS i falą T
@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,
)