На этой странице приведены примеры использования различных API тактильной обратной связи для создания пользовательских эффектов, выходящих за рамки стандартных вибрационных сигналов в приложении Android.
На этой странице приведены следующие примеры:
- Пользовательские режимы вибрации
- Нарастающий паттерн : паттерн, который начинается плавно.
- Повторяющийся узор : узор, не имеющий конца.
- Шаблон с резервным вариантом : демонстрация резервного варианта.
- Вибрационные композиции
- Сопротивление : эффект сопротивления с динамической интенсивностью.
- Расширение : Эффект подъема, а затем спада.
- Wobble : Эффект колебания, создаваемый с помощью примитива
SPIN. - Bounce : Эффект отскока, создаваемый с помощью примитива
THUD.
- Форма вибрационного сигнала с огибающей
- «Пружинный отскок» : эффект пружинистого отскока, создаваемый с помощью базовых эффектов огибающей.
- Запуск ракеты : Эффект запуска ракеты с использованием эффектов огибающей волновой формы.
Дополнительные примеры см. в разделе «Добавление тактильной обратной связи к событиям» и «Всегда следуйте принципам проектирования тактильной обратной связи» .
Используйте резервные варианты для обеспечения совместимости устройств.
При реализации любых пользовательских эффектов следует учитывать следующее:
- Какие возможности устройства необходимы для достижения этого эффекта?
- Что делать, если устройство не способно воспроизводить эффект?
Справочник по API тактильной обратной связи Android содержит подробную информацию о том, как проверить поддержку компонентов, участвующих в тактильной обратной связи, чтобы ваше приложение могло обеспечивать единообразный пользовательский опыт.
В зависимости от ваших задач, вы можете отключить пользовательские эффекты или предложить альтернативные пользовательские эффекты, основанные на различных потенциальных возможностях.
Планируйте использование устройств следующих основных классов возможностей:
Если вы используете тактильные примитивы : устройства, поддерживающие эти примитивы, необходимые для пользовательских эффектов. (Подробнее о примитивах см. в следующем разделе.)
Устройства с регулировкой амплитуды .
Устройства с базовой поддержкой вибрации (вкл/выкл) — иными словами, те, которые не имеют регулировки амплитуды.
Если при выборе тактильных эффектов для вашего приложения учитываются эти категории, то тактильное взаимодействие с пользователем должно оставаться предсказуемым для любого отдельного устройства.
Использование тактильных примитивов
Android включает в себя несколько примитивов тактильной обратной связи, различающихся как амплитудой, так и частотой. Вы можете использовать один примитив отдельно или несколько примитивов в комбинации для достижения разнообразных тактильных эффектов.
- Для создания заметных промежутков между двумя примитивами используйте задержки в 50 мс или более, по возможности также учитывая длительность примитива .
- Используйте шкалы, разница между которыми составляет 1,4 или более, чтобы разница в интенсивности воспринималась лучше.
Используйте шкалы 0,5, 0,7 и 1,0 для создания версий примитива с низкой, средней и высокой интенсивностью.
Создавайте собственные шаблоны вибрации
Вибрационные паттерны часто используются в тактильной обратной связи, например, в уведомлениях и мелодиях звонка. Сервис Vibrator может воспроизводить длинные вибрационные паттерны, амплитуда которых изменяется со временем. Такие эффекты называются волновыми формами.
Волновые эффекты обычно ощутимы, но внезапные продолжительные вибрации могут напугать пользователя, если воспроизводятся в тихой обстановке. Слишком быстрое нарастание амплитуды до целевого значения также может вызывать слышимые жужжащие звуки. Разрабатывайте волновые паттерны таким образом, чтобы сглаживать переходы амплитуды и создавать эффекты нарастания и спада.
Примеры вибрационных паттернов
В следующих разделах приведены несколько примеров колебательных паттернов:
Схема наращивания мощности
Формы волн представлены в виде параметра VibrationEffect с тремя параметрами:
- Временные параметры: массив длительностей в миллисекундах для каждого сегмента волновой формы.
- Амплитуды: желаемая амплитуда вибрации для каждой длительности, указанной в первом аргументе, представленная целочисленным значением от 0 до 255, где 0 означает «выключенное» состояние вибратора, а 255 — максимальную амплитуду устройства.
- Индекс повтора: индекс в массиве, указанном в первом аргументе, с которого начинается повторение волновой формы, или -1, если паттерн должен воспроизводиться только один раз.
Вот пример формы сигнала, который повторяется дважды с паузой в 350 мс между импульсами. Первый импульс представляет собой плавное нарастание до максимальной амплитуды, а второй — быстрое нарастание для удержания максимальной амплитуды. Остановка в конце определяется отрицательным значением индекса повтора.
Котлин
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));
Повторяющийся узор
Волновые формы также могут воспроизводиться многократно до тех пор, пока не будут отменены. Для создания повторяющейся волновой формы необходимо установить неотрицательный параметр repeat . При воспроизведении повторяющейся волновой формы вибрация продолжается до тех пор, пока она не будет явно отменена в сервисе:
Котлин
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();
}
Это очень полезно для периодически возникающих событий, требующих подтверждения со стороны пользователя. Примерами таких событий являются входящие телефонные звонки и срабатывание сигнализации.
Шаблон с резервным вариантом
Управление амплитудой вибрации — это возможность, зависящая от аппаратного обеспечения . Воспроизведение сигнала на недорогом устройстве без этой возможности приводит к тому, что устройство вибрирует с максимальной амплитудой для каждого положительного значения в массиве амплитуд. Если вашему приложению необходимо работать с такими устройствами, используйте либо шаблон, который не создает жужжащего эффекта при воспроизведении в этом режиме, либо разработайте более простой шаблон включения/выключения, который можно воспроизводить в качестве резервного варианта.
Котлин
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));
}
Создавайте вибрационные композиции.
В этом разделе представлены способы объединения вибраций в более длинные и сложные пользовательские эффекты, а также рассматриваются возможности тактильной обратной связи с использованием более продвинутых аппаратных средств. Вы можете использовать комбинации эффектов, изменяющих амплитуду и частоту, для создания более сложных тактильных эффектов на устройствах с тактильными актуаторами, имеющими более широкий частотный диапазон.
Процесс создания пользовательских вибрационных паттернов , описанный ранее на этой странице, объясняет, как управлять амплитудой вибрации для создания плавных эффектов нарастания и спада. Технология Rich Haptics улучшает эту концепцию, используя более широкий частотный диапазон вибратора устройства, чтобы сделать эффект еще более плавным. Эти волновые формы особенно эффективны для создания эффекта крещендо или диминуэндо.
Примитивы композиции, описанные ранее на этой странице, реализуются производителем устройства. Они обеспечивают четкую, короткую и приятную вибрацию, соответствующую принципам тактильной обратной связи для обеспечения четкого тактильного восприятия. Более подробную информацию об этих возможностях и принципах их работы см. в разделе «Введение в вибрационные актуаторы» .
Android не предоставляет резервных вариантов для композиций с неподдерживаемыми примитивами. Поэтому выполните следующие шаги:
Перед активацией расширенной тактильной обратной связи убедитесь, что данное устройство поддерживает все используемые вами примитивы.
Отключите не только те эффекты, у которых отсутствует базовый параметр, но и весь набор неподдерживаемых функций.
Более подробная информация о том, как проверить совместимость устройства, приведена в следующих разделах.
Создавайте сложные вибрационные эффекты.
С помощью VibrationEffect.Composition можно создавать составные вибрационные эффекты. Вот пример эффекта медленного нарастания, за которым следует резкий щелчок:
Котлин
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());
Композиция создается путем добавления примитивов, которые воспроизводятся последовательно. Каждый примитив также масштабируем, поэтому вы можете контролировать амплитуду вибрации, генерируемой каждым из них. Масштаб определяется значением от 0 до 1, где 0 соответствует минимальной амплитуде, при которой пользователь может (едва) ощутить этот примитив.
Создание вариантов в примитивах вибрации
Если вы хотите создать слабую и сильную версии одного и того же примитива, используйте коэффициенты интенсивности 1,4 или выше, чтобы разница в интенсивности была легко различима. Не пытайтесь создать более трех уровней интенсивности одного и того же примитива, поскольку они не различаются на уровне восприятия. Например, используйте шкалы 0,5, 0,7 и 1,0 для создания версий примитива с низкой, средней и высокой интенсивностью.
Добавьте промежутки между примитивами вибрации.
Композиция также может задавать задержки, добавляемые между последовательными примитивами. Эта задержка выражается в миллисекундах с момента окончания предыдущего примитива. Как правило, промежуток в 5–10 мс между двумя примитивами слишком мал, чтобы его можно было заметить. Используйте промежуток порядка 50 мс или больше, если хотите создать заметный промежуток между двумя примитивами. Вот пример композиции с задержками:
Котлин
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());
Проверьте, какие примитивы поддерживаются.
Для проверки поддержки устройством определенных примитивов можно использовать следующие API:
Котлин
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.
}
Также можно проверить несколько примитивов, а затем решить, какие из них следует скомпоновать в зависимости от уровня поддержки устройства:
Котлин
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);
Примеры вибрационных композиций
В следующих разделах приведены несколько примеров вибрационных композиций, взятых из демонстрационного приложения тактильной обратной связи на GitHub.
Сопротивление (с низкими значениями)
Вы можете управлять амплитудой вибрации примитива, чтобы передавать полезную обратную связь выполняемому действию. Близко расположенные значения масштаба можно использовать для создания плавного эффекта нарастания вибрации примитива. Задержка между последовательными примитивами также может динамически устанавливаться в зависимости от взаимодействия пользователя. Это показано в следующем примере анимации представления, управляемой жестом перетаскивания и дополненной тактильной обратной связью.

Рисунок 1. Эта осциллограмма представляет собой выходное ускорение вибрации устройства.
Котлин
@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);
}
}
Расширение (с подъемом и спадом)
Для постепенного увеличения воспринимаемой интенсивности вибрации используются два примитива : PRIMITIVE_QUICK_RISE и PRIMITIVE_SLOW_RISE . Оба достигают одной и той же цели, но с разной длительностью. Для постепенного уменьшения интенсивности вибрации используется только один примитив — PRIMITIVE_QUICK_FALL . Эти примитивы лучше работают вместе, создавая сегмент волновой формы, который нарастает по интенсивности, а затем затухает. Вы можете выравнивать масштабированные примитивы, чтобы предотвратить резкие скачки амплитуды между ними, что также хорошо работает для увеличения общей длительности эффекта. С точки зрения восприятия, люди всегда больше замечают нарастающую часть, чем спадающую, поэтому сокращение нарастающей части по сравнению со спадающей может использоваться для смещения акцента в сторону спада.
Вот пример применения этой композиции для расширения и сужения круга. Эффект подъема может усилить ощущение расширения во время анимации. Сочетание эффектов подъема и сужения помогает подчеркнуть сужение в конце анимации.

Рисунок 2. Эта осциллограмма представляет собой выходное ускорение вибрации устройства.
Котлин
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;
}
}
Покачивание (с вращениями)
Один из ключевых принципов тактильной обратной связи — доставлять удовольствие пользователям. Интересный способ создать приятный неожиданный эффект вибрации — использовать PRIMITIVE_SPIN . Этот примитив наиболее эффективен при многократном вызове. Множественные вращения, объединенные вместе, могут создать эффект дрожания и нестабильности, который можно дополнительно усилить, применив к каждому примитиву несколько случайное масштабирование. Также можно поэкспериментировать с интервалом между последовательными вращениями . Два вращения без интервала (0 мс между ними) создают ощущение сильного вращения. Увеличение интервала между вращениями с 10 до 50 мс приводит к более свободному вращению и может использоваться для согласования длительности видео или анимации.
Не используйте паузу более 100 мс, так как последовательные вращения перестают быть хорошо интегрированными и начинают ощущаться как отдельные эффекты.
Вот пример упругой фигуры, которая отскакивает назад после того, как её потянули вниз, а затем отпустили. Анимация дополнена двумя эффектами вращения, интенсивность которых варьируется в зависимости от смещения при отскоке.

Рисунок 3. Эта осциллограмма представляет собой выходное ускорение вибрации устройства.
Котлин
@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)
}
}
Отскок (с глухими ударами)
Еще одно продвинутое применение вибрационных эффектов — имитация физических взаимодействий. PRIMITIVE_THUD может создавать сильный и реверберирующий эффект, который можно сочетать с визуализацией удара, например, в видео или анимации, чтобы усилить общее впечатление.
Вот пример анимации падения мяча, дополненной звуковым эффектом, воспроизводимым каждый раз, когда мяч отскакивает от нижней части экрана:

Рисунок 4. Эта осциллограмма представляет собой выходное ускорение вибрации устройства.
Котлин
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;
}
}
});
}
}
Форма вибрационного сигнала с огибающей
Процесс создания пользовательских вибрационных паттернов позволяет контролировать амплитуду вибрации для создания плавных эффектов нарастания и спада. В этом разделе объясняется, как создавать динамические тактильные эффекты с помощью огибающих волновых форм, которые позволяют точно контролировать амплитуду и частоту вибрации во времени. Это позволяет создавать более насыщенные и тонкие тактильные ощущения.
Начиная с Android 16 (уровень API 36), система предоставляет следующие API для создания огибающей вибрационной волны путем определения последовательности контрольных точек:
-
BasicEnvelopeBuilder: Доступный подход к созданию тактильных эффектов, не зависящих от аппаратного обеспечения. -
WaveformEnvelopeBuilder: Более продвинутый подход к созданию тактильных эффектов; требует знакомства с оборудованием для тактильной обратной связи.
В Android отсутствуют резервные варианты для эффектов огибающей. Если вам необходима такая поддержка, выполните следующие шаги:
- Проверьте, поддерживает ли данное устройство эффекты огибающей, используя
Vibrator.areEnvelopeEffectsSupported(). - Отключите стандартный набор неподдерживаемых режимов работы или используйте пользовательские шаблоны или композиции вибрации в качестве резервных вариантов.
Для создания более простых эффектов огибающей используйте BasicEnvelopeBuilder со следующими параметрами:
- Значение интенсивности в диапазоне \( [0, 1] \), которая отражает воспринимаемую силу вибрации. Например, значение \( 0.5 \)воспринимается как половина максимально возможной интенсивности, которую может достичь устройство.
Значение резкости в диапазоне \( [0, 1] \), которая отражает четкость вибрации. Более низкие значения соответствуют более плавной вибрации, а более высокие значения создают более резкое ощущение.
Значение длительности , представляющее собой время в миллисекундах, необходимое для перехода от последней контрольной точки — то есть пары «интенсивность и резкость» — к новой.
Вот пример волновой формы, в которой интенсивность колебаний плавно возрастает от низкого тона до высокого тона с максимальной силой в течение 500 мс, а затем снова плавно снижается.\( 0 \) (выкл) более 100 мс.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
Если у вас есть более глубокие знания в области тактильной обратной связи, вы можете определить эффекты огибающей с помощью WaveformEnvelopeBuilder . При использовании этого объекта вы можете получить доступ к отображению частоты на ускорение выходного сигнала (FOAM) через VibratorFrequencyProfile .
- Значение амплитуды в диапазоне \( [0, 1] \), которая представляет собой достижимую интенсивность вибрации на заданной частоте, определяемую устройством FOAM. Например, значение \( 0.5 \) генерирует половину максимального ускорения на выходе, которое может быть достигнуто при заданной частоте.
Значение частоты , выраженное в герцах.
Значение длительности , представляющее собой время в миллисекундах, необходимое для перехода от последней контрольной точки к новой.
Приведенный ниже код демонстрирует пример формы сигнала, определяющей эффект вибрации длительностью 400 мс. Он начинается с 50-миллисекундного нарастания амплитуды от выключенного состояния до максимального значения с постоянной частотой 60 Гц. Затем частота нарастает до 120 Гц в течение следующих 100 мс и остается на этом уровне в течение 200 мс. Наконец, амплитуда нарастает до... \( 0 \)и частота возвращается к 60 Гц за последние 50 мс:
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()
)
В следующих разделах приведены несколько примеров вибрационных волновых форм с огибающими.
Пружина, подпрыгивающая на месте
В предыдущем примере использовался PRIMITIVE_THUD для имитации физического взаимодействия при отскоке . Базовый API огибающей обеспечивает значительно более точное управление, позволяя точно настраивать интенсивность и резкость вибрации. Это приводит к тактильной обратной связи, которая более точно следует за анимированными событиями.
Вот пример свободно падающей пружины с анимацией, дополненной простым эффектом огибающей, который воспроизводится каждый раз, когда пружина отскакивает от нижней части экрана:

Рисунок 5. График формы выходного сигнала ускорения для вибрации, имитирующей подпрыгивающую пружину.
@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")
}
}
}
Запуск ракеты
В предыдущем примере показано, как использовать базовый API огибающей для имитации пружинной реакции. Компонент WaveformEnvelopeBuilder обеспечивает точный контроль над всем частотным диапазоном устройства, позволяя создавать высоконастраиваемые тактильные эффекты. В сочетании с данными FOAM это позволяет адаптировать вибрации к конкретным частотным параметрам.
Вот пример, демонстрирующий имитацию запуска ракеты с использованием динамического вибрационного паттерна. Эффект изменяется от минимальной поддерживаемой частоты ускорения, 0,1 G, до резонансной частоты, при этом амплитуда входного сигнала всегда поддерживается на уровне 10%. Это позволяет эффекту начинаться с достаточно сильного выходного сигнала и постепенно увеличивать воспринимаемую интенсивность и резкость, даже при неизменной амплитуде входного сигнала. При достижении резонанса частота эффекта снижается до минимума, что воспринимается как снижение интенсивности и резкости. Это создает ощущение начального сопротивления с последующим ослаблением, имитируя запуск в космос.
Этот эффект невозможен с помощью базового API огибающей, поскольку он абстрагирует специфичную для устройства информацию о его резонансной частоте и кривой ускорения выходного сигнала. Увеличение резкости может вывести эквивалентную частоту за пределы резонанса, потенциально вызывая непреднамеренное падение ускорения.

Рисунок 6. График формы выходного сигнала ускорения для вибрации, имитирующей запуск ракеты.
@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()
)
}