Twórz własne efekty haptyczne

Na tej stronie znajdziesz przykłady korzystania z różnych interfejsów API dotykowych w celu tworzenia niestandardowych efektów poza standardowymi falami wibracji w aplikacji na Androida.

Na tej stronie znajdziesz te przykłady:

Dodatkowe przykłady znajdziesz w artykule Dodawanie wibracji do zdarzeń. Pamiętaj, aby zawsze przestrzegać zasad projektowania haptycznego.

Zastosowanie rozwiązań zastępczych do obsługi zgodności urządzeń

Podczas wdrażania efektu niestandardowego weź pod uwagę te kwestie:

  • Funkcje urządzenia wymagane do działania efektu
  • Co zrobić, gdy urządzenie nie może odtworzyć efektu

dokumentacji interfejsu API haptycznych funkcji Androida znajdziesz szczegółowe informacje o tym, jak sprawdzić, czy komponenty używane w haptycznych funkcjach są obsługiwane, aby aplikacja mogła zapewnić spójne wrażenia.

W zależności od przypadku użycia możesz wyłączyć efekty niestandardowe lub udostępnić alternatywne efekty niestandardowe na podstawie różnych potencjalnych możliwości.

Zaplanuj te ogólne klasy możliwości urządzeń:

  • Jeśli używasz haptycznych elementów: urządzeń obsługujących te elementy potrzebne do efektów niestandardowych. (szczegółowe informacje o elementach znajdziesz w następnej sekcji).

  • Urządzenia z kontrolą amplitudy.

  • Urządzenia z podstawową obsługą wibracji (włącz/wyłącz) – innymi słowy, urządzenia bez kontroli amplitudy.

Jeśli w aplikacji uwzględniono te kategorie, użytkownik powinien mieć przewidywalne wrażenia haptyczne na każdym urządzeniu.

Korzystanie z elementów haptycznych

Android zawiera kilka prostych efektów haptycznych, które różnią się amplitudą i częstotliwością. Aby uzyskać bogate efekty haptyczne, możesz użyć jednego prymitywu lub kilku prymitywów w połączeniu.

  • W przypadku zauważalnych przerw między dwoma elementami należy stosować opóźnienia co najmniej 50 ms, biorąc pod uwagę czas trwania elementu (jeśli to możliwe).
  • Używaj skal, które różnią się o współczynnik 1,4 lub więcej, aby lepiej odróżnić intensywność.
  • Użyj skali 0,5, 0,7 i 1,0, aby utworzyć wersję prymitywu o niskiej, średniej i wysokiej intensywności.

Tworzenie niestandardowych wzorców wibracji

W reakcji haptycznej na zwrócenie uwagi często stosuje się wzorce wibracji, np. w przypadku powiadomień i dzwonów. Usługa Vibrator może odtwarzać długie wzorce wibracji, które zmieniają amplitudę wibracji w czasie. Takie efekty nazywamy przebiegami.

Efekty fali są zwykle wyczuwalne, ale nagłe, długie wibracje mogą przestraszyć użytkownika, jeśli odtwarzanie odbywa się w cichym otoczeniu. Zbyt szybkie zwiększanie amplitudy docelowej może też powodować słyszalne brzęczenie. Projektowanie wzorów fali w celu wygładzania przejść między amplitudami i tworzenia efektów wzmacniania i ściszania.

Przykłady wzorów wibracji

W poniższych sekcjach znajdziesz kilka przykładów wzorów wibracji:

Wzór zwiększania wyświetlania

Faloformy są reprezentowane jako VibrationEffect z 3 parametrami:

  1. Timings: tablica z czasem trwania (w milisekundach) każdego segmentu fali dźwiękowej.
  2. Amplitudy: żądana amplituda wibracji dla każdego czasu określonego w pierwszym argumencie, reprezentowana przez wartość całkowitą z zakresu 0–255, przy czym 0 oznacza „stan wyłączony”, a 255 to maksymalna amplituda urządzenia.
  3. Indeks powtórzeń: indeks w tablicy określony w pierwszym argumencie, aby rozpocząć powtarzanie fali dźwiękowej lub -1, jeśli ma ona być odtworzona tylko raz.

Oto przykład fali, która pulsuje dwukrotnie z przerwą 350 ms. Pierwszy impuls to płynne zwiększanie amplitudy do maksimum, a drugi to szybkie zwiększanie amplitudy do jej maksymalnej wartości. Zatrzymanie na końcu jest zdefiniowane przez ujemną wartość indeksu powtórzenia.

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

Falowniki można też odtwarzać wielokrotnie, dopóki nie zostaną anulowane. Aby utworzyć powtarzającą się krzywą, ustaw nieujemny parametr repeat. Podczas odtwarzania powtarzającej się fali wibracyjnej wibracje będą trwać, 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 sporadycznych zdarzeń, które wymagają potwierdzenia przez użytkownika. Przykładami takich zdarzeń są przychodzące połączenia telefoniczne i wyzwalane alarmy.

Wzór z zastępczym

Regulowanie amplitudy wibracji to funkcja zależna od sprzętu. Odtwarzanie fali na urządzeniu niskiego poziomu bez tej funkcji powoduje wibrowanie urządzenia z maksymalną amplitudą dla każdego dodatniego wpisu w tablicy amplitudy. Jeśli Twoja aplikacja musi obsługiwać takie urządzenia, użyj wzoru, który nie generuje efektu brzęczenia podczas odtwarzania w takich warunkach, lub zaprojektuj prostszy wzór włączania/wyłączania, który można odtwarzać 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));
}

Tworzenie kompozycji wibracji

W tej sekcji znajdziesz sposoby na tworzenie wibracji w dłuższe i bardziej złożone efekty niestandardowe. Oprócz tego dowiesz się, jak korzystać z zaawansowanych funkcji sprzętowych, aby tworzyć bogate wibracje. Możesz używać kombinacji efektów, które zmieniają amplitudę i częstotliwość, aby tworzyć bardziej złożone efekty haptyczne na urządzeniach z silnikami haptycznymi o szerszym paśmie częstotliwości.

Proces tworzenia niestandardowych wzorów wibracji, opisany wcześniej na tej stronie, wyjaśnia, jak kontrolować amplitudę wibracji, aby uzyskać płynne efekty zwiększania i zmniejszania intensywności wibracji. Ulepszona haptyka udoskonala tę koncepcję, wykorzystując szerszy zakres częstotliwości wibratora urządzenia, aby efekt był jeszcze bardziej płynny. Te przebiegi fali są szczególnie skuteczne w tworzeniu efektu crescendo lub diminuendo.

Primitive’y kompozycji, opisane wcześniej na tej stronie, są implementowane przez producenta urządzenia. Zapewniają wyraźne, krótkie i przyjemne wibracje, które są zgodne z zasadami haptyki. Więcej informacji o tych funkcjach i o sposobie ich działania znajdziesz w artykule Wprowadzenie do siłowników wibracyjnych.

Android nie zapewnia alternatywnych rozwiązań dla kompozycji z nieobsługiwanymi prymitywami. Dlatego wykonaj te czynności:

  1. Zanim aktywujesz zaawansowane funkcje haptyczne, sprawdź, czy dane urządzenie obsługuje wszystkie używane przez Ciebie prymitywy.

  2. Wyłącz spójny zestaw funkcji, które nie są obsługiwane, a nie tylko efekty, w których brakuje prymitywu.

W kolejnych sekcjach znajdziesz więcej informacji o tym, jak sprawdzić, czy urządzenie jest obsługiwane.

Tworzenie złożonych efektów wibracji

Za pomocą VibrationEffect.Composition możesz tworzyć złożone efekty wibracji. Oto przykład efektu powolnego wzrostu, a następnie nagłego kliknięcia:

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 jest tworzona przez dodawanie prymitywów, które mają być odtwarzane sekwencyjnie. Każdy obiekt prosty jest też skalowalny, więc możesz kontrolować amplitudę wibracji generowanych przez każdy z nich. Skala jest zdefiniowana jako wartość od 0 do 1, gdzie 0 odpowiada minimalnej amplitudzie, przy której użytkownik może (ledwie) wyczuć ten prymityw.

Tworzenie wariantów w przypadku wibracji prostych

Jeśli chcesz utworzyć słabszą i silniejszą wersję tego samego prymitywu, utwórz współczynniki siły 1,4 i wyższe, aby różnica w intensywności była łatwo zauważalna. Nie próbuj tworzyć więcej niż 3 poziomów intensywności tego samego prymitywu, ponieważ nie są one od siebie wyraźnie odróżnialne. Użyj na przykład skali 0,5, 0,7 i 1,0, aby utworzyć wersje prymitywu o niskiej, średniej i wysokiej intensywności.

Dodawanie przerw między wibracjami

Kompozycja może też określać opóźnienia dodawane między kolejnymi elementami. To opóźnienie jest wyrażone w milisekundach od końca poprzedniego prymitywu. Zazwyczaj odstęp 5–10 ms między dwoma prymitywami jest zbyt krótki, aby można go było wykryć. Jeśli chcesz utworzyć wyraźną przerwę między dwoma prymitywami, użyj przerwy o długości co najmniej 50 ms. 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 obsługiwanych prymitywów

Aby sprawdzić, czy dane urządzenie obsługuje określone prymitywy, możesz 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 prymitywów, a potem zdecydować, które z nich użyć na podstawie poziomu obsługi przez urządzenie:

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 następnych sekcjach znajdziesz kilka przykładów kompozycji wibracji pochodzących z przykładowej aplikacji haptycznej na GitHubie.

Resist (with low ticks)

Możesz kontrolować amplitudę wibracji prymitywnych, aby przekazywać przydatne informacje o działaniu w toku. Wartości skali o zbliżonych wartościach można stosować do tworzenia płynnego efektu crescendo prymity. Opóźnienie między kolejnymi elementami prostymi można też ustawiać dynamicznie na podstawie interakcji użytkownika. Przykład animacji widoku sterowanej gestem przeciągania i wzbogaconej funkcją haptyczną przedstawia poniższy obraz.

Animacja przedstawiająca przeciąganie koła w dół.
Wykres fali drgań wejściowych.

Rysunek 1. Ten przebieg falowy przedstawia 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);
    }
}

Rozwiń (z podnoszeniem i opadaniem)

Aby zwiększyć odczuwaną intensywność wibracji, możesz użyć 2 elementów prymitywnych: PRIMITIVE_QUICK_RISEPRIMITIVE_SLOW_RISE. Oba mają ten sam cel, ale różnią się długością. Jest tylko jeden prymityw do łagodnego zwalniania, PRIMITIVE_QUICK_FALL. Te prymity lepiej współpracują ze sobą, tworząc segment fali, który narasta, a potem zanika. Możesz wyrównywać skalowane prymity, aby zapobiec nagłym skokom amplitudy między nimi. Takie działanie pozwala też wydłużyć czas trwania efektu. Z punktu widzenia percepcji ludzie zawsze bardziej zauważają część rosnącą niż opadającą, więc skrócenie części rosnącej w stosunku do części opadającej może służyć do przesunięcia akcentu na część opadającą.

Oto przykład zastosowania tej kompozycji do rozszerzania i zwijania koła. Efekt wzrostu może wzmocnić wrażenie rozszerzania się podczas animacji. Połączenie efektów wzrostu i spadku pomaga podkreślić zwężanie się na końcu animacji.

Animacja rozszerzającego się koła.
Wykres fali drgań wejściowych.

Rysunek 2. Ten przebieg fali 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)

Jednym z kluczowych zasadniczych zasad haptycznych jest zadowolenie użytkowników. Ciekawym sposobem na wprowadzenie przyjemnego, nieoczekiwanego efektu wibracji jest użycie PRIMITIVE_SPIN. Ten typ jest najbardziej efektywny, gdy jest wywoływany więcej niż raz. Połączenie wielu obrotów może stworzyć efekt kołysania i niestabilności, który można dodatkowo wzmocnić, stosując nieco losowego skalowania dla każdego prymitywu. Możesz też eksperymentować z przestrzenią między kolejnymi pierwotnymi. Dwa obroty bez przerwy (0 ms między nimi) dają wrażenie szybkiego obrotu. Zwiększenie przerwy między obrotami z 10 do 50 ms powoduje wrażenie płynniejszego obracania. Można to wykorzystać, aby dopasować czas trwania filmu lub animacji.

Nie używaj przerwy dłuższej niż 100 ms, ponieważ kolejne obroty nie będą się dobrze łączyć i zaczną przypominać osobne efekty.

Oto przykład elastycznej figury, która odskakuje po przeciągnięciu w dół i puszczeniu. Animacja jest wzbogacona o parę efektów obrotu, które są odtwarzane z różną intensywnością proporcjonalną do przesunięcia odbicia.

Animacja pokazująca odbijanie się sprężystej figury
Wykres fali wibracji wejściowych

Rysunek 3. Ten przebieg falowy przedstawia 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 uderzeniami)

Innym zaawansowanym zastosowaniem efektów wibracji jest symulowanie fizycznych interakcji. PRIMITIVE_THUD może tworzyć silny i pogłoszysty efekt, który można połączyć z wizualizacją uderzenia, na przykład w filmie lub animacji, aby wzbogacić ogólne wrażenia.

Oto przykład animacji spadania piłki wzbogaconej o efekt uderzenia, który jest odtwarzany za każdym razem, gdy piłka odbija się od dołu ekranu:

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

Rysunek 4. Ten przebieg falowy przedstawia 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;
            }
            }
        });
    }
}