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:
- Niestandardowe wzorce wibracji
- Wzrost głośności: wzór, który zaczyna się płynnie.
- Powtarzający się wzór: wzór bez końca.
- Wzór z zastępnikiem: demonstracja zastępnika.
- Kompozycje wibracyjne
- Resist (Opor): efekt oporu z dynamiczną intensywnością.
- Rozwiń: efekt wznoszenia się i opadania.
- Wobble: efekt kołysania za pomocą prymitywu
SPIN
. - Odbijanie: efekt odbijania za pomocą prymitywu
THUD
.
- Wibracje z obwlokami
- Odbijanie sprężyną: efekt odbijania sprężyną za pomocą podstawowych efektów obwiedni.
- Start rakiety: efekt startu rakiety z użyciem efektów obwiedni fali.
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:
- Które funkcje urządzenia są wymagane do działania
- Co zrobić, gdy urządzenie nie może odtworzyć efektu
W dokumentacji interfejsu API haptycznych funkcji Androida znajdziesz szczegółowe informacje o tym, jak sprawdzić, czy komponenty używane w Twoich funkcjach haptycznych 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 te, które nie mają 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:
- Timings: tablica z czasem trwania (w milisekundach) każdego segmentu fali dźwiękowej.
- 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.
- 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 reklamą zastępczą
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ę więcej o bogatych efektach haptycznych, korzystając z bardziej zaawansowanych funkcji sprzętowych. 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 udostępnia alternatywnych rozwiązań dla kompozycji z nieobsługiwanymi prymitywami. Dlatego wykonaj te czynności:
Zanim aktywujesz zaawansowane funkcje haptyczne, sprawdź, czy dane urządzenie obsługuje wszystkie używane przez Ciebie prymitywy.
Wyłącz spójny zestaw funkcji, które nie są obsługiwane, a nie tylko efekty, w których brakuje prymitywu.
Więcej informacji o sprawdzaniu obsługi urządzenia znajdziesz w następnych sekcjach.
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 więcej, 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 wykorzystać do płynnego zwiększania wartości prymitywu. 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.

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_RISE
i PRIMITIVE_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ż część malejącą, więc skrócenie części rosnącej w stosunku do części malejącej może służyć do przesunięcia akcentu na część maleją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.

Rysunek 2. Ten przebieg sygnału odpowiada przyspieszeniu wyjściowemu 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 losowe skalowanie każdego prymitywu. Możesz też eksperymentować z przedziałem 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 zwolnieniu. Animacja jest wzbogacona o parę efektów obrotu, które są odtwarzane z różną intensywnością proporcjonalną do przesunięcia odbicia.

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 odgłosu, który odtwarzany jest za każdym razem, gdy piłka odbija się od dołu ekranu:

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;
}
}
});
}
}
Wibracje w postaci przebiegu sygnału z obusłaniami
Proces tworzenia niestandardowych wzorców wibracji pozwala kontrolować amplitudę wibracji, aby uzyskać płynne efekty zwiększania i zmniejszania wibracji. Z tego sekcji dowiesz się, jak tworzyć dynamiczne efekty haptyczne za pomocą kopert przebiegu sygnału, które umożliwiają precyzyjne sterowanie amplitudą i częstotliwością wibracji w czasie. Dzięki temu możesz tworzyć bardziej złożone i wyraziste wrażenia haptyczne.
Począwszy od Androida 16 (poziom interfejsu API 36) system udostępnia te interfejsy API do tworzenia za pomocą sekwencji punktów kontrolnych zaokrąglonej ścieżki sygnału wibracji:
BasicEnvelopeBuilder
: dostę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 udostępnia alternatywnych efektów kopert. Jeśli potrzebujesz pomocy, wykonaj te czynności:
- Sprawdź, czy dane urządzenie obsługuje efekty kopertowe, korzystając z
Vibrator.areEnvelopeEffectsSupported()
. - Wyłącz spójny zestaw funkcji, które nie są obsługiwane, lub użyj niestandardowych wzorów wibracji lub kompozycji jako alternatyw.
Aby utworzyć bardziej podstawowe efekty kopertowe, użyj funkcji BasicEnvelopeBuilder
z tymi parametrami:
- Wartość intensywności w zakresie \( [0, 1] \), która reprezentuje odczuwaną siłę wibracji. Na przykład wartość \( 0.5 \)jest postrzegana jako połowa globalnej maksymalnej intensywności, której może użyć urządzenie.
Wartość ostrości z zakresu \( [0, 1] \), która określa czystość wibracji. Niższe wartości oznaczają łagodniejsze wibracje, a wyższe – bardziej ostre.
wartość czasu trwania, która określa czas w milisekundach potrzebny na przejście z ostatniego punktu kontrolnego (czyli pary intensywności i ostrości) do nowego.
Oto przykład fali, która w ciągu 500 ms zwiększa natężenie od niskiego do wysokiego, a potem w ciągu 100 ms wraca do\( 0 \) (wyłączone).
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 kopertowe za pomocą WaveformEnvelopeBuilder
. Korzystając z tego obiektu, możesz uzyskać dostęp do mapowania częstotliwości na wyjście (FOAM) za pomocą VibratorFrequencyProfile
.
- Wartość amplitudy w zakresie \( [0, 1] \), która określa osiągalną siłę wibracji przy danej częstotliwości, określoną przez FOAM urządzenia. Na przykład wartość \( 0.5 \) powoduje połowę maksymalnego przyspieszenia wyjścia, jakie można osiągnąć przy danej częstotliwości.
Wartość częstotliwości podana w hercach.
wartość duration, która określa czas w milisekundach potrzebny na przejście z ostatniego punktu kontrolnego do nowego;
Poniższy kod pokazuje przykładowy przebieg sygnału, który definiuje efekt wibracji o długości 400 ms. Najpierw przez 50 ms amplituda wzrasta od zera do pełnej wartości z częstotliwością 60 Hz. Następnie w ciągu 100 ms częstotliwość wzrasta do 120 Hz i utrzymuje się na tym poziomie przez 200 ms. W końcu amplituda maleje do wartości \( 0 \), a częstotliwość w ciągu ostatnich 50 ms powraca do wartości 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 następnych sekcjach znajdziesz kilka przykładów kształtów fali wibracji z zawijaczem.
Sprężyna odbijająca
W poprzednim przykładzie do symulowania fizycznych interakcji typu „bounce” użyto wartości PRIMITIVE_THUD
. Podstawowy interfejs API obudowy zapewnia znacznie większą kontrolę, umożliwiając dokładne dostosowanie intensywności i ostrości wibracji.
Dzięki temu wibracje będą bardziej odpowiadać animowanym zdarzeniom.
Oto przykład sprężyny swobodnie opadającej z animowanym efektem podstawowych okien, który jest odtwarzany za każdym razem, gdy sprężyna odbija się od dolnej krawędzi ekranu:
@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 desired 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 pokazano, jak za pomocą podstawowego interfejsu API Envelope symulować działanie sprężystej sprężyny. WaveformEnvelopeBuilder
umożliwia precyzyjne sterowanie pełnym zakresem częstotliwości urządzenia, co pozwala na tworzenie bardzo spersonalizowanych efektów haptycznych. Łącząc te dane z danymi FOAM, możesz dostosować wibracje do określonych częstotliwości.
Oto przykład symulacji startu rakiety z wykorzystaniem dynamicznego wzoru wibracji. Efekt przechodzi od minimalnej obsługiwanej częstotliwości wyjścia przyspieszenia, 0,1 G, do częstotliwości rezonansowej, zawsze utrzymując wejście amplitudy na poziomie 10%. Dzięki temu efekt rozpoczyna się od stosunkowo silnego sygnału wyjściowego i zwiększa postrzeganą intensywność oraz ostrość, mimo że amplituda napędowa jest taka sama. Po osiągnięciu rezonansu częstotliwość efektu spada do minimum, co jest odbierane jako spadek intensywności i ostrości. Powoduje to uczucie początkowego oporu, a następnie ustąpienia, co naśladuje start w kosmos.
Ten efekt nie jest możliwy w przypadku podstawowego interfejsu API envelope, ponieważ abstrahuje on od informacji dotyczących częstotliwości rezonansowej i krzywej przyspieszenia wyjściowego urządzenia. Zwiększanie ostrości może spowodować, że równoważna częstotliwość przekroczy rezonans, co może doprowadzić do niezamierzonego spadku przyspieszenia.
@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 desired 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()
)
}