맞춤 햅틱 효과 만들기

이 페이지에서는 다양한 햅틱 API를 사용하여 Android 애플리케이션에서 맞춤 효과를 만드는 방법을 알아봅니다. 이 주제에 대한 많은 정보는 이 페이지는 진동 액추에이터의 작동에 대한 충분한 지식이 필요합니다. 진동 액추에이터 기본 지침서를 읽어보는 것이 좋습니다.

이 페이지에는 다음 예가 포함되어 있습니다.

추가 예는 이벤트에 햅틱 반응 추가를 참고하세요. 항상 햅틱 디자인 원칙을 따라야 합니다.

대체를 사용하여 기기 호환성 처리

맞춤 효과를 구현할 때는 다음 사항을 고려하세요.

  • 효과를 내는 데 필요한 기기 기능
  • 기기에서 효과를 재생할 수 없을 때 해야 할 일

Android 햅틱 API 참조에서는 햅틱과 관련된 구성요소를 지원해야 하며 전반적으로 일관성 있는 경험을 제공합니다.

사용 사례에 따라 맞춤 효과를 사용 중지하거나 다양한 잠재적 기능에 따라 대체 맞춤 효과를 제공합니다.

다음과 같은 상위 수준의 기기 기능에 대해 계획하세요.

  • 햅틱 프리미티브를 사용하는 경우 이러한 프리미티브를 지원하는 기기 필요에 따라 수정하겠습니다. (광고 설정에 대한 자세한 내용은 다음 섹션을 있습니다.)

  • 진폭 제어가 있는 기기.

  • 기본 진동 지원 (켜짐/꺼짐) 기능, 즉 진폭 제어가 없습니다.

앱의 햅틱 효과 선택이 이러한 카테고리를 고려하는 경우 햅틱 사용자 환경은 개별 기기에서 예측 가능한 상태로 유지되어야 합니다.

햅틱 프리미티브 사용

Android에는 진폭과 진폭이 다른 여러 햅틱 프리미티브가 포함되어 있습니다. 게재 빈도에 따라 달라집니다. 하나의 프리미티브를 단독으로 사용하거나 여러 프리미티브를 조합하여 사용할 수 있습니다. 리치 햅틱 효과를 얻을 수 있습니다

  • 둘 사이의 눈에 띄는 차이에는 50ms 이상의 지연을 사용합니다. 또한 프리미티브와 시간 할 수 있습니다.
  • 다른 값의 차이를 1.4 이상으로 나누는 척도를 사용하세요. 더 잘 인식될 수 있습니다.
  • 0.5, 0.7, 1.0의 척도를 사용하여 최저, 중간, 높음으로 만듭니다. 강도입니다.

맞춤 진동 패턴 만들기

진동 패턴은 알림과 같은 주의 햅틱에 자주 사용됩니다. 벨소리를 들 수 있습니다. Vibrator 서비스는 다음과 같이 긴 진동 패턴을 재생할 수 있습니다. 시간 경과에 따라 진동 진폭을 변경합니다. 이러한 효과를 파형이라고 합니다.

파형 효과는 쉽게 인지할 수 있지만 갑작스럽고 긴 진동은 사용자를 놀라게 할 수 있습니다. 타겟 진폭으로 조정 윙윙거리는 소리가 날 수도 있습니다 이 파형 패턴을 설계하는 것은 진폭 전환을 평활화하여 효과의 상승 및 축소를 조절합니다.

샘플: 단계적 패턴

파형은 다음과 같은 세 개의 매개변수가 있는 VibrationEffect로 표현됩니다.

  1. 타이밍: 각 파형의 지속 시간 배열(밀리초) 세그먼트입니다.
  2. 진폭: 지정된 각 기간에 원하는 진동 진폭 첫 번째 인수에 0과 255 사이의 정수 값으로 표현되며 0 진동기 '꺼짐'을 나타냅니다 255는 기기의 최대값이며 진폭
  3. 반복 색인: 첫 번째 인수에 지정된 배열의 색인 파형을 반복하기 시작하거나 패턴을 한 번만 재생해야 하는 경우 -1을 누릅니다.

다음은 그 사이에 350ms의 일시중지를 두고 두 번 깜빡이는 파형의 예입니다. 알 수 있습니다. 첫 번째 펄스는 최대 진폭까지 부드럽게 점진적이며 두 번째는 최대 진폭을 유지하기 위한 빠른 램프입니다. 끝 지점에서 정차가 정의됨 음의 반복 색인 값으로 나눈 값입니다.

Kotlin

val timings: LongArray = longArrayOf(50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // 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));

샘플: 반복 패턴

파형은 취소될 때까지 반복적으로 재생될 수도 있습니다. kubectl 명령어 음이 아닌 '반복' 매개변수를 설정하는 것입니다. 게임을 할 때 파형이 반복되면 1초 후로 확실히 제거될 때까지 서비스:

Kotlin

void startVibrating() {
  val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
  val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
  val repeat = 1 // Repeat from the second entry, index = 1.
  VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat)
  // repeatingEffect can be used in multiple places.

  vibrator.vibrate(repeatingEffect)
}

void stopVibrating() {
  vibrator.cancel()
}

자바

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

이 방법은 사용자 작업이 필요한 간헐적인 이벤트에 매우 유용합니다. 있습니다. 이러한 이벤트의 예로는 수신 전화 및 경보가 작동됩니다.

샘플: 대체가 있는 패턴

진동 진폭을 제어하는 것은 하드웨어에 종속된 기능이 아닙니다. 하나의 기기에서 파형을 이 기능이 없는 저사양 기기를 사용하면 기기가 최대로 진동하기 때문에 진폭 배열에 있는 각 양수 항목의 진폭입니다. 앱이 이러한 기기를 수용하려는 경우 특정 조건에서 재생할 때 윙윙거리는 효과가 발생하지 않거나 대신 대체로 재생될 수 있는 더 간단한 ON/OFF 패턴을 설계합니다.

Kotlin

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

진동 구성 만들기

이 섹션에서는 이를 다음과 같이 구성하는 방법을 설명합니다. 더 길고 복잡한 맞춤 효과를 제공합니다. 햅틱을 사용하는 방법을 배웠습니다. 진폭과 주파수를 다양화하여 더 복잡한 햅틱 효과를 만드는 효과 주파수 대역폭이 더 넓은 햅틱 액추에이터가 있는 기기에서

맞춤 진동 생성 과정 패턴을 선택하면 에서는 부드러운 진동 효과를 생성하기 위해 진동 진폭을 제어하는 방법을 설명합니다. 점점 더 어려워지고 있습니다 리치 햅틱은 장치 바이브레이터의 더 넓은 주파수 범위를 조정하여 효과를 더 부드럽게 합니다. 이 파형은 특히 크레센도나 디미누엔도를 연주하는 데 효과적입니다. 있습니다.

이 페이지의 앞부분에서 설명한 컴포지션 프리미티브는 기기 제조업체에 문의하세요. 또한 선명하고 짧으며 기분 좋은 진동을 제공합니다. 명확한 햅틱을 위한 햅틱 원칙을 준수합니다. 자세한 내용은 자세한 내용은 진동 액추에이터를 참조하세요. 기본 지침서를 참고하세요.

Android에서는 지원되지 않는 음악작품의 대체 기능을 제공하지 않습니다. 프리미티브입니다. 다음 단계를 수행하는 것이 좋습니다.

  1. 고급 햅틱을 활성화하기 전에 특정 기기에서 사용할 수 있습니다.

  2. 사용 중지 얻을 수 있습니다. 자세한 내용은 기기의 지원은 다음과 같습니다.

VibrationEffect.Composition로 구성된 진동 효과를 만들 수 있습니다. 다음은 천천히 상승하고 날카로운 클릭 효과가 뒤따르는 효과의 예입니다.

Kotlin

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 이상 차이 나는 것이 좋으므로 그 강도를 쉽게 인지할 수 있습니다. 3개 이상의 인코더-디코더는 지각할 수 없기 때문에 있습니다. 예를 들어 0.5, 0.7, 1.0의 척도를 사용하여 낮음, 중간, 고강도 버전의 프리미티브 버전이죠.

또한 컴포지션은 연속되는 연속 재생 사이에 추가될 지연을 지정할 수 있습니다. 프리미티브입니다. 이러한 지연은 이전 프리미티브입니다. 일반적으로 두 프리미티브 간의 5~10ms 간격은 더 짧아야 합니다. 50ms 이상의 간격을 사용해 봅니다. 두 프리미티브 사이에 명확한 간격을 만들려는 경우 다음은 지연이 있는 컴포지션의 예:

Kotlin

val delayMs = 100
vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
    ).addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
    ).addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
    ).compose()
  )

자바

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를 사용하여 특정 애플리케이션에 대한 기기 지원을 확인할 수 있습니다. 프리미티브:

Kotlin

val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(primitive).compose())
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

자바

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

또한 여러 프리미티브를 선택한 다음 인코더-디코더에 적용할 프리미티브를 다음과 같이 작성할 수 있습니다.

Kotlin

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

샘플: 저항 (낮은 틱)

기본 진동의 진폭을 조절하여 유용한 피드백을 수집하는 것입니다 가까운 간격의 배율 값은 부드러운 크레센도 효과를 만드는 데 사용됩니다. 지연 시간 연속 프리미티브도 사용자에 따라 동적으로 설정될 수 있습니다. 상호작용하지 않습니다. 이 내용은 다음 뷰 애니메이션 예시에서 설명합니다. 드래그 동작으로 제어되고 햅틱으로 보강됩니다.

원을 아래로 드래그하는 애니메이션
입력 진동 파형 플롯

Kotlin

@Composable
fun ResistScreen() {
  // Control variables for the dragging of the indicator.
  var isDragging by remember { mutableStateOf(false) }
  var dragOffset by remember { mutableStateOf(0f) }

  // Only vibrates while the user is dragging
  if (isDragging) {
    LaunchedEffect(Unit) {
      // Continuously run the effect for vibration to occur even when the view
      // is not being drawn, when user stops dragging midway through gesture.
      while (true) {
        // Calculate the interval inversely proportional to the drag offset.
        val vibrationInterval = calculateVibrationInterval(dragOffset)
        // Calculate the scale directly proportional to the drag offset.
        val vibrationScale = calculateVibrationScale(dragOffset)

        delay(vibrationInterval)
        vibrator.vibrate(
          VibrationEffect.startComposition().addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
            vibrationScale
          ).compose()
        )
      }
    }
  }

  Screen() {
    Column(
      Modifier
        .draggable(
          orientation = Orientation.Vertical,
          onDragStarted = {
            isDragging = true
          },
          onDragStopped = {
            isDragging = false
          },
          state = rememberDraggableState { delta ->
            dragOffset += delta
          }
        )
    ) {
      // Build the indicator UI based on how much the user has dragged it.
      ResistIndicator(dragOffset)
    }
  }
}

자바

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_RISEPRIMITIVE_SLOW_RISE 둘 다 동일한 타겟에 도달하지만 기간은 다릅니다. 오직 하나 감소하기 위한 프리미티브지만, PRIMITIVE_QUICK_FALL 이러한 프리미티브가 함께 작동하여 음속 높이에서 증가하는 파형 세그먼트를 만듭니다. 죽어버린다. 조정된 프리미티브를 정렬하여 진폭이 달라지기 때문에 적용됩니다. 사람들이 항상 상승분이 평균보다 떨어지는 부분보다 올라오는 부분을 짧은 부분보다 짧게 만들면 하강 부분으로 강조하는 데 사용됩니다.

다음은 확장 및 축소를 위해 이 컴포지션을 적용한 예입니다. 원을 접는 모습 상승 효과는 긴장을 푸는 동안 지정할 수 있습니다. 상승 및 하락 효과를 함께 사용하면 애니메이션이 끝날 때 축소됩니다.

확장되는 원의 애니메이션
입력 진동 파형 플롯

Kotlin

enum class ExpandShapeState {
    Collapsed,
    Expanded
}

@Composable
fun ExpandScreen() {
  // Control variable for the state of the indicator.
  var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }

  // Animation between expanded and collapsed states.
  val transitionData = updateTransitionData(currentState)

  Screen() {
    Column(
      Modifier
        .clickable(
          {
            if (currentState == ExpandShapeState.Collapsed) {
              currentState = ExpandShapeState.Expanded
              vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
                  0.3f
                ).addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                  0.3f
                ).compose()
              )
            } else {
              currentState = ExpandShapeState.Collapsed
              vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
                ).compose()
              )
          }
        )
    ) {
      // Build the indicator UI based on the current state.
      ExpandIndicator(transitionData)
    }
  }
}

자바

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 이 프리미티브는 두 번 이상 호출될 때 가장 효과적입니다. 여러 항목 회전이 합쳐지면 흔들리고 불안정한 효과를 일으킬 수 있으며, 각 프리미티브에 어느 정도 임의 배율을 적용하여 더욱 향상되었습니다. 나 연속적인 스핀 프리미티브 사이의 간격을 실험할 수도 있습니다. 2회 돌리기 0ms 간격이 없으면 꽉 찬 스피닝 느낌이 듭니다. 증가 10~50ms 사이의 스핀 간 간격은 더 느슨한 스피닝 감각으로 이어집니다. 동영상 또는 애니메이션의 재생 시간을 맞추는 데 사용할 수 있습니다.

100ms보다 긴 간격은 사용하지 않는 것이 좋습니다. 스핀이 더 이상 잘 통합되지 않고 개별 효과처럼 느껴지기 시작합니다.

다음은 아래로 드래그하면 다시 튀어나오는 탄성 도형의 예입니다. 발표합니다. 애니메이션은 한 쌍의 회전 효과로 개선되며 탄성 변위에 비례하는 다양한 강도를 사용합니다.

탄성 모양이 튀는 애니메이션
입력 진동 파형 플롯

Kotlin

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableStateOf(0f) }
    var isWobbling by remember { mutableStateOf(false) }
 
    // Use drag distance to create an animated float value behaving like a spring.
    val dragDistanceAnimated by animateFloatAsState(
        targetValue = if (dragDistance > 0f) dragDistance else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioHighBouncy,
            stiffness = Spring.StiffnessMedium
        ),
    )
 
    if (isWobbling) {
        LaunchedEffect(Unit) {
            while (true) {
                val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                // Use some sort of minimum displacement so the final few frames
                // of animation don't generate a vibration.
                if (displacement > SPIN_MIN_DISPLACEMENT) {
                    vibrator.vibrate(
                        VibrationEffect.startComposition().addPrimitive(
                            VibrationEffect.Composition.PRIMITIVE_SPIN,
                            nextSpinScale(displacement)
                        ).addPrimitive(
                          VibrationEffect.Composition.PRIMITIVE_SPIN,
                          nextSpinScale(displacement)
                        ).compose()
                    )
                }
                // Delay the next check for a sufficient duration until the current
                // composition finishes. Note that you can use
                // Vibrator.getPrimitiveDurations API to calculcate the delay.
                delay(VIBRATION_DURATION)
            }
        }
    }
 
    Box(
        Modifier
            .fillMaxSize()
            .draggable(
                onDragStopped = {
                    isWobbling = true
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value.
        WobbleShape(dragDistanceAnimated)
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
  // Generate a random offset in [-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 드림 강렬한 반향 효과를 낼 수 있으며, 반향을 일으킬 수 있습니다. 효과를 시각화하여 정보를 제공하는 것을 말합니다. 개선할 수 있습니다

다음은 쿵쿵거리는 효과가 적용된 간단한 볼 드롭 애니메이션의 예입니다. 지정한 횟수만큼 플레이한 횟수를 나타냅니다.

떨어뜨린 공이 화면 하단에서 튀어 오르는 애니메이션
입력 진동 파형 플롯

Kotlin

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
  // Control variable for the state of the ball.
  var ballPosition by remember { mutableStateOf(BallPosition.Start) }
  var bounceCount by remember { mutableStateOf(0) }

  // Animation for the bouncing ball.
  var transitionData = updateTransitionData(ballPosition)
  val collisionData = updateCollisionData(transitionData)

  // Ball is about to contact floor, only vibrating once per collision.
  var hasVibratedForBallContact by remember { mutableStateOf(false) }
  if (collisionData.collisionWithFloor) {
    if (!hasVibratedForBallContact) {
      val vibrationScale = 0.7.pow(bounceCount++).toFloat()
      vibrator.vibrate(
        VibrationEffect.startComposition().addPrimitive(
          VibrationEffect.Composition.PRIMITIVE_THUD,
          vibrationScale
        ).compose()
      )
      hasVibratedForBallContact = true
    }
  } else {
    // Reset for next contact with floor.
    hasVibratedForBallContact = false
  }

  Screen() {
    Box(
      Modifier
        .fillMaxSize()
        .clickable {
          if (transitionData.isAtStart) {
            ballPosition = BallPosition.End
          } else {
            ballPosition = BallPosition.Start
            bounceCount = 0
          }
        },
    ) {
      // Build the ball UI based on the current state.
      BouncingBall(transitionData)
    }
  }
}

Java

class ClickListener implements View.OnClickListener {
  @Override
  public void onClick(View view) {
    view.animate()
      .translationY(targetY)
      .setDuration(3000)
      .setInterpolator(new BounceInterpolator())
      .setUpdateListener(new AnimatorUpdateListener() {

        boolean hasVibratedForBallContact = false;
        int bounceCount = 0;

        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
          boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
          if (valueBeyondThreshold) {
            if (!hasVibratedForBallContact) {
              float vibrationScale = (float) Math.pow(0.7, bounceCount++);
              vibrator.vibrate(
                VibrationEffect.startComposition()
                  .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale)
                  .compose());
              hasVibratedForBallContact = true;
            }
          } else {
            // Reset for next contact with floor.
            hasVibratedForBallContact = false;
          }
        }
      });
  }
}