Создавайте собственные тактильные эффекты

На этой странице приведены примеры использования различных API тактильных ощущений для создания пользовательских эффектов, выходящих за рамки стандартных форм вибрационных волн в приложении Android.

На этой странице приведены следующие примеры:

Дополнительные примеры см. в разделе Добавление тактильной обратной связи к событиям и всегда следуйте принципам проектирования тактильной обратной связи .

Используйте резервные варианты для обеспечения совместимости устройств

При реализации любого пользовательского эффекта учитывайте следующее:

  • Какие возможности устройства необходимы для эффекта
  • Что делать, если устройство не может воспроизвести эффект

Справочник по API тактильных эффектов Android содержит подробную информацию о том, как проверить поддержку компонентов, задействованных в тактильных эффектах, чтобы ваше приложение могло обеспечивать единообразный общий опыт.

В зависимости от вашего варианта использования вы можете захотеть отключить пользовательские эффекты или предоставить альтернативные пользовательские эффекты на основе различных потенциальных возможностей.

Планируйте следующие высокоуровневые классы возможностей устройств:

  • Если вы используете тактильные примитивы : устройства, поддерживающие эти примитивы, необходимые для пользовательских эффектов. (Подробнее о примитивах см. в следующем разделе.)

  • Устройства с амплитудным управлением .

  • Устройства с базовой поддержкой вибрации (вкл./выкл.) — другими словами, те, в которых отсутствует управление амплитудой.

Если выбор тактильного эффекта вашего приложения учитывает эти категории, то его тактильный опыт пользователя должен оставаться предсказуемым для любого отдельного устройства.

Использование тактильных примитивов

Android включает в себя несколько примитивов тактильного восприятия, которые различаются как по амплитуде, так и по частоте. Вы можете использовать один примитив в отдельности или несколько примитивов в сочетании для достижения богатых тактильных эффектов.

  • Используйте задержки в 50 мс или более для заметных промежутков между двумя примитивами, также принимая во внимание длительность примитива, если это возможно.
  • Используйте шкалы, отличающиеся в соотношении 1,4 или более, чтобы лучше ощущать разницу в интенсивности.
  • Используйте масштабы 0,5, 0,7 и 1,0 для создания версии примитива с низкой, средней и высокой интенсивностью.

Создавайте собственные шаблоны вибрации

Шаблоны вибрации часто используются в тактильных элементах внимания, таких как уведомления и рингтоны. Служба Vibrator может воспроизводить длинные шаблоны вибрации, которые со временем меняют амплитуду вибрации. Такие эффекты называются волновыми формами.

Эффекты формы волны обычно воспринимаются, но внезапные длинные вибрации могут напугать пользователя, если играть в тихой обстановке. Слишком быстрое нарастание до целевой амплитуды также может привести к слышимым жужжащим шумам. Разрабатывайте шаблоны формы волны, чтобы сгладить переходы амплитуды для создания эффектов нарастания и спада.

Примеры моделей вибрации

В следующих разделах приведено несколько примеров схем вибрации:

Модель нарастания нагрузки

Формы волн представлены как VibrationEffect с тремя параметрами:

  1. Временные параметры: массив длительностей в миллисекундах для каждого сегмента формы волны.
  2. Амплитуды: желаемая амплитуда вибрации для каждой длительности, указанной в первом аргументе, представленная целым числом от 0 до 255, где 0 представляет собой «выключенное» состояние вибратора, а 255 — максимальную амплитуду устройства.
  3. Индекс повтора: индекс в массиве, указанный в первом аргументе, для начала повторения формы волны или -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))

Ява

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()
}

Ява

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

Ява

if (vibrator.hasAmplitudeControl()) {
  vibrator.vibrate(VibrationEffect.createWaveform(
    smoothTimings, amplitudes, smoothRepeatIdx));
} else {
  vibrator.vibrate(VibrationEffect.createWaveform(
    onOffTimings, onOffRepeatIdx));
}

Создание вибрационных композиций

В этом разделе представлены способы объединения вибраций в более длинные и сложные пользовательские эффекты, а также более того, чтобы исследовать богатые тактильные эффекты с использованием более продвинутых аппаратных возможностей. Вы можете использовать комбинации эффектов, которые изменяют амплитуду и частоту, чтобы создавать более сложные тактильные эффекты на устройствах с тактильными приводами, имеющими более широкую полосу частот.

Процесс создания пользовательских шаблонов вибрации , описанный ранее на этой странице, объясняет, как управлять амплитудой вибрации для создания плавных эффектов нарастания и спада. Rich haptics улучшает эту концепцию, исследуя более широкий диапазон частот вибратора устройства, чтобы сделать эффект еще более плавным. Эти формы волн особенно эффективны для создания эффекта крещендо или диминуэндо.

Примитивы композиции, описанные ранее на этой странице, реализованы производителем устройства. Они обеспечивают четкую, короткую и приятную вибрацию, которая соответствует принципам тактильных ощущений для четких тактильных ощущений. Для получения более подробной информации об этих возможностях и о том, как они работают, см. Вибрационные приводы .

Android не предоставляет резервных вариантов для композиций с неподдерживаемыми примитивами. Поэтому выполните следующие шаги:

  1. Перед активацией расширенных тактильных функций убедитесь, что данное устройство поддерживает все используемые вами примитивы.

  2. Отключите согласованный набор неподдерживаемых эффектов, а не только те, в которых отсутствует примитив.

Более подробная информация о том, как проверить поддержку устройства, приведена в следующих разделах.

Создавайте сложные вибрационные эффекты

Вы можете создавать комбинированные эффекты вибрации с помощью VibrationEffect.Composition . Вот пример медленно нарастающего эффекта, за которым следует резкий щелчок:

Котлин

vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_CLICK
    ).compose()
)

Ява

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

Ява

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.
}

Ява

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)

Ява

int[] primitives = new int[] {
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);

Примеры вибрационных композиций

В следующих разделах приведено несколько примеров композиций вибрации, взятых из примера приложения haptics на 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)
        }
    }
}

Ява

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

Ява

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

Ява

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

Ява

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;
            }
            }
        });
    }
}