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

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

vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));

Образец: повторяющийся узор

Формы сигналов также можно воспроизводить повторно, пока они не будут отменены. Способ создания повторяющегося сигнала — установить неотрицательный параметр «повторение». Когда вы воспроизводите повторяющийся сигнал, вибрация продолжается до тех пор, пока она не будет явно отменена в сервисе:

Котлин

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

Примитивы композиции, описанные ранее на этой странице, реализованы производителем устройства. Они обеспечивают четкую, короткую и приятную вибрацию, соответствующую принципам 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);

Пример: Resist (с низкими тиками)

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

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

Котлин

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

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

Анимация расширяющегося круга
График формы входного сигнала вибрации

Котлин

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

Пример: Wobble (с вращениями)

Один из ключевых принципов тактильности — доставлять удовольствие пользователям. Интересный способ создать приятный неожиданный эффект вибрации — использовать PRIMITIVE_SPIN . Этот примитив наиболее эффективен, когда он вызывается более одного раза. Объединение нескольких вращений может создать эффект раскачивания и нестабильности, который можно еще больше усилить, применив к каждому примитиву несколько случайное масштабирование. Вы также можете поэкспериментировать с зазором между последовательными примитивами вращения. Два вращения без перерыва (0 мс между ними) создают ощущение сильного вращения. Увеличение интервала между вращениями с 10 до 50 мс приводит к более слабому ощущению вращения и может использоваться для согласования продолжительности видео или анимации.

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

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

Анимация подпрыгивания упругой фигуры
График формы входного сигнала вибрации

Котлин

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

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

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

Котлин

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