Twórz własne efekty haptyczne

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:

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.Builder obejmują automatyczne wycofywanie na poziomie platformy. Jeśli urządzenie nie obsługuje natywnie Preset lub podstawowego Envelope, 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 WaveformEnvelopeBuilder nie 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.
  • VibrationEffect.Composition: kompozycje utworzone za pomocą interfejsu startComposition() 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ą polecenia vibrator.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:

  1. Timings: tablica czasów trwania (w milisekundach) poszczególnych segmentów fali.
  2. 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.
  3. 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:

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

  1. Zanim aktywujesz zaawansowane wibracje, sprawdź, czy dane urządzenie obsługuje wszystkie używane przez Ciebie elementy podstawowe.

  2. 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ę.

Animacja przedstawiająca przeciąganie okręgu w dół.
Wykres wejściowego przebiegu drgań.

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.

Animacja przedstawiająca powiększający się okrąg.
Wykres wejściowego przebiegu drgań.

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.

Animacja przedstawiająca odbijający się elastyczny kształt
Wykres wejściowego przebiegu drgań

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:

Animacja przedstawiająca spadającą piłkę odbijającą się od dołu ekranu.
Wykres wejściowego przebiegu drgań.

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:

  1. Sprawdź, czy dane urządzenie obsługuje efekty obwiedni, za pomocą funkcji Vibrator.areEnvelopeEffectsSupported().
  2. 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:

Animacja przedstawiająca sprężynę, która po upuszczeniu odbija się od dołu ekranu.
Wykres wejściowego przebiegu drgań.

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.

Animacja przedstawiająca rakietę startującą z dołu ekranu.
Wykres wejściowego przebiegu drgań.

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.

Animacja lampy lawowej, która pulsuje w rytm bicia serca.
Wykres segmentu EKG i jego charakterystyczne kształty fal

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