Tạo hiệu ứng xúc giác tuỳ chỉnh

Trang này trình bày các ví dụ về cách sử dụng nhiều API xúc giác để tạo hiệu ứng tuỳ chỉnh trong một ứng dụng Android. Nhiều thông tin trên trang này dựa trên kiến thức tốt về hoạt động của bộ truyền động rung, bạn nên đọc Bài viết về bộ truyền động rung.

Trang này bao gồm các ví dụ sau.

Để biết thêm ví dụ, hãy xem phần Thêm phản hồi xúc giác vào sự kiện và luôn tuân theo các nguyên tắc thiết kế theo xúc giác.

Sử dụng tính năng dự phòng để xử lý khả năng tương thích của thiết bị

Khi triển khai bất kỳ hiệu ứng tuỳ chỉnh nào, hãy cân nhắc những điều sau:

  • Cần có các chức năng của thiết bị để tạo hiệu ứng
  • Việc cần làm khi thiết bị không thể phát hiệu ứng

Tài liệu tham khảo về API xúc giác của Android cung cấp thông tin chi tiết về cách kiểm tra khả năng hỗ trợ các thành phần liên quan đến xúc giác để ứng dụng của bạn có thể mang lại trải nghiệm tổng thể nhất quán.

Tuỳ thuộc vào trường hợp sử dụng, bạn nên tắt hiệu ứng tuỳ chỉnh hoặc cung cấp hiệu ứng tuỳ chỉnh thay thế dựa trên nhiều chức năng tiềm năng.

Lên kế hoạch cho các cấp tính năng cấp cao của thiết bị sau đây:

  • Nếu bạn đang sử dụng dữ liệu gốc xúc giác: các thiết bị hỗ trợ các dữ liệu gốc đó mà hiệu ứng tuỳ chỉnh cần. (Xem phần tiếp theo để biết thông tin chi tiết về dữ liệu gốc.)

  • Thiết bị có điều khiển biên độ.

  • Các thiết bị có hỗ trợ rung cơ bản (bật/tắt), nói cách khác là những thiết bị thiếu khả năng kiểm soát biên độ.

Nếu lựa chọn hiệu ứng xúc giác của ứng dụng có tính đến các danh mục này, thì trải nghiệm người dùng xúc giác của ứng dụng vẫn có thể dự đoán được trên mọi thiết bị.

Sử dụng dữ liệu gốc xúc giác

Android bao gồm một số dữ liệu nguyên gốc xúc giác khác nhau về cả biên độ và tần số. Bạn có thể sử dụng riêng một hoặc nhiều dữ liệu gốc kết hợp để đạt được hiệu ứng xúc giác phong phú.

  • Sử dụng độ trễ 50 mili giây trở lên cho các khoảng trống rõ ràng giữa hai dữ liệu nguyên gốc, đồng thời tính đến thời lượng gốc nếu có thể.
  • Hãy sử dụng các thang đo có tỷ lệ từ 1,4 trở lên để nhận biết rõ hơn sự khác biệt về cường độ.
  • Sử dụng các thang điểm 0,5, 0,7 và 1,0 để tạo phiên bản cường độ thấp, trung bình và cao của một dữ liệu nguyên gốc.

Tạo mẫu rung tuỳ chỉnh

Dạng rung thường được dùng trong các phản hồi xúc giác chú ý, chẳng hạn như thông báo và nhạc chuông. Dịch vụ Vibrator có thể phát các mẫu rung lâu thay đổi biên độ rung theo thời gian. Các hiệu ứng đó được đặt tên là dạng sóng.

Hiệu ứng dạng sóng có thể dễ nhận thấy, nhưng các rung đột ngột dài có thể khiến người dùng giật mình nếu chơi trong môi trường yên tĩnh. Việc tăng dần đến biên độ mục tiêu quá nhanh cũng có thể tạo ra tiếng ồn ù. Đề xuất thiết kế mẫu dạng sóng là làm mượt các chuyển đổi biên độ để tạo hiệu ứng dốc lên và xuống.

Mẫu: Mẫu tăng dần số lượng

Dạng sóng được biểu thị dưới dạng VibrationEffect với 3 tham số:

  1. Timings (Thời gian): một mảng thời lượng (tính bằng mili giây) cho mỗi phân đoạn dạng sóng.
  2. Biên độ rung: biên độ rung mong muốn cho từng thời lượng được chỉ định trong đối số đầu tiên, được biểu thị bằng một giá trị số nguyên từ 0 đến 255, trong đó 0 đại diện cho bộ rung "tắt" và 255 là biên độ tối đa của thiết bị.
  3. Chỉ mục lặp lại: chỉ mục trong mảng được chỉ định trong đối số đầu tiên để bắt đầu lặp lại dạng sóng hoặc -1 nếu chỉ nên phát mẫu một lần.

Dưới đây là ví dụ về dạng sóng xung 2 lần với khoảng dừng 350 mili giây giữa các xung. Xung đầu tiên là một đường tăng mượt mà lên đến biên độ tối đa, và xung thứ hai là một đường dốc nhanh để giữ biên độ tối đa. Việc dừng ở cuối được xác định bằng giá trị chỉ mục lặp lại âm.

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

Java

long[] timings = new long[] { 50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] { 33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Do not repeat.

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

Mẫu: Mẫu lặp lại

Bạn cũng có thể phát dạng sóng nhiều lần cho đến khi huỷ. Cách để tạo dạng sóng lặp lại là đặt tham số "repeat" không âm. Khi bạn phát một dạng sóng lặp lại, chế độ rung sẽ tiếp tục cho đến khi bị huỷ một cách rõ ràng trong dịch vụ:

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

Java

void startVibrating() {
  long[] timings = new long[] { 50, 50, 100, 50, 50 };
  int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
  int repeat = 1; // Repeat from the second entry, index = 1.
  VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat);
  // repeatingEffect can be used in multiple places.

  vibrator.vibrate(repeatingEffect);
}

void stopVibrating() {
  vibrator.cancel();
}

Điều này rất hữu ích đối với các sự kiện không liên tục đòi hỏi người dùng hành động để xác nhận. Ví dụ về những sự kiện như vậy bao gồm cuộc gọi điện thoại đến và chuông báo được kích hoạt.

Mẫu: Mẫu có dự phòng

Việc kiểm soát biên độ rung là tính năng phụ thuộc vào phần cứng. Khi phát một dạng sóng trên thiết bị cấp thấp mà không có tính năng này, dạng sóng sẽ bị rung ở biên độ tối đa đối với mỗi giá trị nhập dương trong mảng biên độ. Nếu ứng dụng của bạn cần hỗ trợ các thiết bị như vậy, thì bạn nên đảm bảo mẫu của bạn không tạo ra hiệu ứng rung khi phát ở điều kiện đó hoặc thiết kế một mẫu BẬT/TẮT đơn giản hơn có thể phát dưới dạng dự phòng.

Kotlin

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

Java

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

Tạo bố cục rung

Phần này trình bày các cách để kết hợp chúng thành các hiệu ứng tuỳ chỉnh dài hơn và phức tạp hơn, ngoài ra còn có thể khám phá các hiệu ứng xúc giác phong phú bằng cách sử dụng các tính năng phần cứng nâng cao hơn. Bạn có thể sử dụng kết hợp các hiệu ứng thay đổi biên độ và tần suất để tạo ra các hiệu ứng xúc giác phức tạp hơn trên các thiết bị có bộ truyền động xúc giác có băng thông tần số rộng hơn.

Quy trình tạo mẫu rung tuỳ chỉnh, như mô tả trên trang này trước đó, giải thích cách kiểm soát biên độ rung để tạo ra các hiệu ứng vuốt lên và xuống một cách mượt mà. Chế độ phản hồi xúc giác phong phú giúp cải thiện khái niệm này bằng cách khám phá dải tần số rộng hơn của bộ rung trên thiết bị để mang lại hiệu ứng mượt mà hơn nữa. Các dạng sóng này đặc biệt hiệu quả trong việc tạo ra hiệu ứng cao dần hoặc nhỏ nhất.

Gốc của thành phần kết hợp, được mô tả ở phần trên của trang này, do nhà sản xuất thiết bị triển khai. Cảm biến cung cấp độ rung sắc nét, ngắn và dễ chịu, phù hợp với nguyên tắc xúc giác để tạo phản hồi xúc giác rõ ràng. Để biết thêm thông tin chi tiết về các chức năng này và cách hoạt động của chúng, hãy xem bài viết Đầu phát động truyền động rung.

Android không cung cấp tính năng dự phòng cho các cấu trúc có các dữ liệu gốc không được hỗ trợ. Bạn nên thực hiện các bước sau:

  1. Trước khi kích hoạt công nghệ xúc giác nâng cao, hãy kiểm tra để đảm bảo rằng một thiết bị nhất định có hỗ trợ tất cả các dữ liệu gốc mà bạn đang sử dụng.

  2. Tắt tập hợp các trải nghiệm nhất quán không được hỗ trợ, chứ không chỉ các hiệu ứng thiếu dữ liệu nguyên gốc. Sau đây là thông tin chi tiết về cách kiểm tra khả năng hỗ trợ của thiết bị.

Bạn có thể tạo các hiệu ứng rung khi soạn bằng VibrationEffect.Composition. Dưới đây là ví dụ về hiệu ứng tăng dần, theo sau là hiệu ứng nhấp chuột mạnh:

Kotlin

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

Java

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

Thành phần được tạo bằng cách thêm các dữ liệu gốc để phát theo trình tự. Mỗi gốc cũng có thể mở rộng. Vì vậy, bạn có thể kiểm soát biên độ rung do từng nguyên gốc tạo ra. Tỷ lệ được định nghĩa là một giá trị nằm trong khoảng từ 0 đến 1, trong đó giá trị 0 thực sự ánh xạ đến biên độ tối thiểu mà người dùng có thể cảm nhận được (hiếm khi).

Nếu muốn tạo phiên bản yếu và mạnh của cùng một dữ liệu nguyên gốc, bạn nên tạo các thang đo khác nhau theo tỷ lệ từ 1, 4 trở lên để có thể dễ dàng nhận thấy sự khác biệt về cường độ. Đừng cố tạo nhiều hơn 3 mức cường độ cho cùng một độ nguyên gốc vì các mức cường độ này không khác biệt về mặt nhận thức. Ví dụ: sử dụng các thang điểm 0, 5, 0, 7 và 1, 0 để tạo phiên bản có cường độ thấp, trung bình và cao của một dữ liệu nguyên gốc.

Thành phần kết hợp này cũng có thể chỉ định độ trễ cần thêm giữa các dữ liệu gốc liên tiếp. Độ trễ này được biểu thị bằng mili giây kể từ khi kết thúc quá trình gốc trước đó. Nhìn chung, khoảng cách giữa hai dữ liệu gốc từ 5 đến 10 mili giây là quá ngắn nên không thể phát hiện được. Hãy cân nhắc sử dụng một khoảng trống theo thứ tự 50 mili giây trở lên nếu bạn muốn tạo khoảng cách rõ ràng giữa hai dữ liệu gốc. Dưới đây là ví dụ về một thành phần có độ trễ:

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

Java

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

Bạn có thể dùng các API sau đây để xác minh khả năng hỗ trợ thiết bị cho các dữ liệu nguyên gốc cụ thể:

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

Java

int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;

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

Bạn cũng có thể kiểm tra nhiều dữ liệu gốc rồi quyết định xem dữ liệu nào cần kết hợp dựa trên cấp độ hỗ trợ thiết bị:

Kotlin

val effects: IntArray = intArrayOf(
  VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
  VibrationEffect.Composition.PRIMITIVE_TICK,
  VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives);

Java

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

Mẫu: Chống lại (có ít đánh dấu)

Bạn có thể kiểm soát biên độ của rung ban đầu để truyền đạt ý kiến phản hồi hữu ích cho một hành động đang diễn ra. Bạn có thể sử dụng các giá trị tỷ lệ có khoảng cách gần nhau để tạo hiệu ứng cao trào mượt mà của dữ liệu nguyên gốc. Độ trễ giữa các gốc liên tiếp cũng có thể được đặt linh động dựa trên tương tác của người dùng. Điều này được minh hoạ trong ví dụ sau đây về ảnh động dạng khung hiển thị được điều khiển bằng cử chỉ kéo và được tăng cường bằng xúc giác.

Ảnh động của một vòng tròn đang được kéo xuống
Biểu đồ dạng sóng rung đầu vào

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

Java

class DragListener implements View.OnTouchListener {
  // Control variables for the dragging of the indicator.
  private int startY;
  private int vibrationInterval;
  private float vibrationScale;

  @Override
  public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        startY = event.getRawY();
        vibrationInterval = calculateVibrationInterval(0);
        vibrationScale = calculateVibrationScale(0);
        startVibration();
        break;
      case MotionEvent.ACTION_MOVE:
        float dragOffset = event.getRawY() - startY;
        // Calculate the interval inversely proportional to the drag offset.
        vibrationInterval = calculateVibrationInterval(dragOffset);
        // Calculate the scale directly proportional to the drag offset.
        vibrationScale = calculateVibrationScale(dragOffset);
        // Build the indicator UI based on how much the user has dragged it.
        updateIndicator(dragOffset);
        break;
      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
        // Only vibrates while the user is dragging
        cancelVibration();
        break;
    }
    return true;
  }

  private void startVibration() {
    vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, vibrationScale)
            .compose());

    // Continuously run the effect for vibration to occur even when the view
    // is not being drawn, when user stops dragging midway through gesture.
    handler.postDelayed(this::startVibration, vibrationInterval);
  }

  private void cancelVibration() {
    handler.removeCallbacksAndMessages(null);
  }
}

Mẫu: Mở rộng (theo chiều tăng và mùa thu)

Có hai gốc để tăng cường độ rung cảm nhận được: PRIMITIVE_QUICK_RISEPRIMITIVE_SLOW_RISE. Cả hai đều đạt được cùng một mục tiêu, nhưng với thời lượng khác nhau. Chỉ có một dữ liệu gốc để giảm dần, đó là PRIMITIVE_QUICK_FALL. Các dữ liệu gốc này hoạt động hiệu quả hơn cùng nhau để tạo một phân đoạn dạng sóng tăng dần cường độ rồi biến mất. Bạn có thể căn chỉnh các dữ liệu gốc được điều chỉnh theo tỷ lệ để ngăn hiện tượng nhảy đột ngột về biên độ giữa các dữ liệu gốc, điều này cũng phù hợp để kéo dài thời lượng hiệu ứng tổng thể. Về mặt lý thuyết, mọi người luôn chú ý đến phần đang tăng nhiều hơn phần giảm, vì vậy, bạn có thể sử dụng cách làm cho phần đang tăng ngắn hơn phần giảm xuống để chuyển mức độ nhấn mạnh về phần giảm.

Dưới đây là ví dụ về ứng dụng cấu trúc này để mở rộng và thu gọn một vòng tròn. Hiệu ứng nổi có thể cải thiện cảm giác mở rộng trong khi chuyển động. Sự kết hợp giữa các hiệu ứng lên và giảm giúp làm nổi bật phần thu gọn ở cuối ảnh động.

Ảnh động của một vòng tròn mở rộng
Biểu đồ dạng sóng rung đầu vào

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

Java

class ClickListener implements View.OnClickListener {
  private final Animation expandAnimation;
  private final Animation collapseAnimation;
  private boolean isExpanded;

  ClickListener(Context context) {
    expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
    expandAnimation.setAnimationListener(new Animation.AnimationListener() {

      @Override
      public void onAnimationStart(Animation animation) {
        vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
            .compose());
      }
    });

    collapseAnimation = AnimationUtils.loadAnimation(context, R.anim.collapse);
    collapseAnimation.setAnimationListener(new Animation.AnimationListener() {

      @Override
      public void onAnimationStart(Animation animation) {
        vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
            .compose());
      }
    });
  }

  @Override
  public void onClick(View view) {
    view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
    isExpanded = !isExpanded;
  }
}

Mẫu: Rung (với vòng tròn)

Một trong những nguyên tắc xúc giác chính là làm hài lòng người dùng. Một cách thú vị để giới thiệu một hiệu ứng rung bất ngờ dễ chịu là sử dụng PRIMITIVE_SPIN. Dữ liệu gốc này hoạt động hiệu quả nhất khi được gọi nhiều lần. Nhiều vòng quay được nối với nhau có thể tạo ra hiệu ứng rung lắc và không ổn định, điều này có thể được tăng cường hơn nữa bằng cách áp dụng tỷ lệ hơi ngẫu nhiên trên từng hiệu ứng nguyên gốc. Bạn cũng có thể thử nghiệm khoảng cách giữa các gốc quay liên tiếp. Hai vòng xoay không có bất kỳ khoảng cách nào (0 mili giây ở giữa) tạo ra cảm giác quay tròn chặt. Việc tăng khoảng cách giữa các thành phần xoay từ 10 lên 50 mili giây sẽ dẫn đến cảm giác quay chậm hơn và có thể dùng để khớp với thời lượng của video hoặc ảnh động.

Bạn không nên sử dụng khoảng cách dài hơn 100 mili giây, vì các vòng quay liên tiếp không còn tích hợp tốt và bắt đầu tạo ra cảm giác giống như các hiệu ứng riêng lẻ.

Dưới đây là ví dụ về một hình dạng đàn hồi sẽ bật trở lại sau khi bị kéo xuống rồi thả ra. Ảnh động này được tăng cường bằng một cặp hiệu ứng xoay tròn, phát ở nhiều cường độ tương ứng với độ dịch chuyển thoát.

Ảnh động về một hình đàn hồi nảy lên
Biểu đồ dạng sóng rung đầu vào

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

Java

class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
  private final Random vibrationRandom = new Random(seed);
  private final long lastVibrationUptime;

  @Override
  public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
    // Delay the next check for a sufficient duration until the current
    // composition finishes. Note that you can use
    // Vibrator.getPrimitiveDurations API to calculcate the delay.
    if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
      return;
    }

    float displacement = calculateRelativeDisplacement(value);

    // Use some sort of minimum displacement so the final few frames
    // of animation don't generate a vibration.
    if (displacement < SPIN_MIN_DISPLACEMENT) {
      return;
    }

    lastVibrationUptime = SystemClock.uptimeMillis();
    vibrator.vibrate(
      VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement))
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement))
        .compose());
  }

  // Calculate a random scale for each spin to vary the full effect.
  float nextSpinScale(float displacement) {
    // Generate a random offset in [-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)
  }
}

Mẫu: Số trang không truy cập (có âm thanh)

Một ứng dụng nâng cao khác của hiệu ứng rung là mô phỏng các tương tác vật lý. PRIMITIVE_THUD có thể tạo ra hiệu ứng mạnh và vang dội, có thể kết hợp với hình ảnh trực quan về tác động, chẳng hạn như trong video hoặc ảnh động, để tăng trải nghiệm tổng thể.

Dưới đây là ví dụ về một ảnh động thả bóng đơn giản được cải tiến bằng hiệu ứng thu được phát mỗi khi bóng bật ra ở cuối màn hình:

Ảnh động về một quả bóng bị rơi ra khỏi phần cuối màn hình
Biểu đồ dạng sóng rung đầu vào

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