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

На этой странице приведены примеры использования различных 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;
            }
            }
        });
    }
}

Форма волны вибрации с огибающими

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

Начиная с Android 16 (уровень API 36), система предоставляет следующие API для создания огибающей формы волны вибрации путем определения последовательности контрольных точек:

  • BasicEnvelopeBuilder : доступный подход к созданию тактильных эффектов, не зависящих от оборудования.
  • WaveformEnvelopeBuilder : более продвинутый подход к созданию тактильных эффектов; требует знакомства с аппаратным обеспечением тактильных эффектов.

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

  1. Проверьте, поддерживает ли данное устройство эффекты огибающей, используя Vibrator.areEnvelopeEffectsSupported() .
  2. Отключите согласованный набор неподдерживаемых возможностей или используйте пользовательские шаблоны вибрации или композиции в качестве запасных альтернатив.

Для создания более простых эффектов огибающей используйте 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 огибающей обеспечивает значительно более тонкий контроль, позволяя вам точно настраивать интенсивность и резкость вибрации. Это приводит к тактильной обратной связи, которая более точно следует анимированным событиям.

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

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

@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 огибающей, поскольку он абстрагируется от специфической для устройства информации о его резонансной частоте и выходной кривой ускорения. Увеличение резкости может вывести эквивалентную частоту за пределы резонанса, что может привести к непреднамеренному провалу ускорения.

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

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