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 các API xúc giác khác nhau để tạo hiệu ứng tuỳ chỉnh trong ứng dụng Android. Hầu hết 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 Giới thiệu 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 nguyên tắc thiết kế về xúc giác.

Sử dụng phương án 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ó những khả năng nào của thiết bị để có hiệu ứng
  • Việc cần làm khi thiết bị không phát được 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 tính năng hỗ trợ các thành phần liên quan đến phản hồi xúc giác để ứng dụng của bạn có thể cung cấp mang lại trải nghiệm nhất quán chung.

Tuỳ thuộc vào trường hợp sử dụng của mình, bạn có thể 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 các khả năng tiềm năng khác nhau.

Hãy lập kế hoạch cho các lớp chức năng cấp cao sau đây của thiết bị:

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

  • Thiết bị có tính năng kiểm soát 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à thiếu 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 những danh mục này, thì trải nghiệm xúc giác người dùng vẫn có thể dự đoán được đối với mọi thiết bị riêng lẻ.

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

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

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

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

Các mẫu rung thường được dùng trong hoạt động 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 trong thời gian dài thay đổi biên độ rung theo thời gian. Những hiệu ứng như vậy được gọi là dạng sóng.

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

Mẫu: Mẫu tăng dần

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

  1. Thời gian: một mảng thời lượng, tính bằng mili giây, cho mỗi dạng sóng phân khúc.
  2. Khuếch đại: biên độ rung mong muốn trong mỗi khoảng thời gian 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, với 0 biểu thị bộ rung "tắt" và 255 là mức tối đa của thiết bị biên độ.
  3. Chỉ mục lặp lại: chỉ mục trong mảng được chỉ định trong đối số đầu tiên cho bắt đầu lặp lại dạng sóng hoặc -1 nếu chỉ phát mẫu một lần.

Dưới đây là ví dụ về dạng sóng phát xung hai lần với thời gian tạm dừng 350 mili giây ở giữa xung. Xung đầu tiên là tăng dần lên đến biên độ tối đa và giây là đoạn đường nối nhanh để duy trì biên độ tối đa. Dừng ở cuối được xác định theo giá trị chỉ số 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 các dạng sóng nhiều lần cho đến khi bị huỷ. Cách tạo dạng sóng lặp lại là đặt tham số "lặp lại" không âm. Khi bạn chơi một dạng sóng lặp lại, rung sẽ tiếp tục cho đến khi bị huỷ một cách rõ ràng sau 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 phải thao tác để xác nhận điều đó. Các ví dụ về những sự kiện này bao gồm cuộc gọi điện thoại đến và đã kích hoạt chuông báo.

Mẫu: Mẫu có tính năng dự phòng

Điều khiển biên độ của rung là khả năng phụ thuộc vào phần cứng. Phát dạng sóng trên thiết bị cấp thấp không có khả năng này khiến thiết bị rung ở mức tối đa biên độ cho mỗi mục dương trong mảng biên độ. Nếu ứng dụng của bạn cần phù hợp với các thiết bị như vậy, thì bạn nên đảm bảo rằng không tạo ra hiệu ứng rung khi phát trong đ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ản phối rung

Phần này trình bày các cách sắp xếp chúng các hiệu ứng tuỳ chỉnh dài hơn và phức tạp hơn, cũng như vượt ra ngoài phạm vi đó để khám phá các hiệu ứng xúc giác 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 số để tạo ra 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 rung tuỳ chỉnh , như được mô tả trước đó trên trang này, giải thích cách kiểm soát biên độ rung để tạo ra hiệu ứng mượt mà của tăng và giảm. Công nghệ xúc giác phong phú cải thiện khái niệm này bằng cách khám phá dải tần số của bộ rung thiết bị rộng hơn để hiệu ứng còn 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 sự cao trào hoặc nhỏ bé hiệu ứng.

Nguyên tắc gốc kết hợp, được mô tả trước đó trên trang này, được triển khai bằng nhà sản xuất thiết bị. Loa có khả năng rung mạnh, ngắn và dễ chịu phù hợp với nguyên tắc về xúc giác để mang lại phản hồi xúc giác rõ ràng. Để biết thêm thông tin chi tiết về những chức năng này và cách hoạt động, hãy xem phần Bộ truyền động rung lót.

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

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

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

Bạn có thể tạo hiệu ứng rung tổng hợp 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 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());

Một cấu trúc được tạo bằng cách thêm các dữ liệu nguyên gốc được phát theo trình tự. Một nguyên gốc cũng có thể mở rộng, vì vậy, bạn có thể kiểm soát biên độ rung tạo ra. Thang đo được định nghĩa là một giá trị từ 0 đến 1, trong đó 0 thực sự ánh xạ tới biên độ tối thiểu mà tại đó giá trị gốc có thể có (hầu như không) cảm nhận được.

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

Bố cục cũng có thể chỉ định độ trễ cần thêm vào giữa các liên tiếp dữ liệu gốc. Độ trễ này được biểu thị bằng mili giây kể từ khi kết thúc dữ liệu gốc trước đó. Nói chung, khoảng cách từ 5 đến 10 mili giây giữa hai dữ liệu gốc là quá ngắn gọn sao cho dễ phát hiện được. Cân nhắc sử dụng một khoảng trống trong khoảng 50 mili giây trở lên nếu bạn muốn tạo một khoảng cách rõ ràng giữa hai dữ liệu gốc. Dưới đây là một ví dụ về bản sáng tác 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());

Các API sau đây có thể được dùng để xác minh khả năng hỗ trợ của thiết bị cho một số dữ liệu gốc:

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 nên chọn dữ liệu gốc nào Compose dựa trên mức độ hỗ trợ của 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: Từ chối (có ít kim đánh dấu nhịp độ khung hình)

Bạn có thể kiểm soát biên độ của rung động ban đầu để truyền tải thông tin phản hồi hữu ích cho một hành động đang diễn ra. Giá trị tỷ lệ có khoảng cách gần nhau có thể là dùng để tạo ra hiệu ứng cao trào mượt mà của thời nguyên thuỷ. Độ trễ giữa các dữ liệu nguyên gốc liên tiếp cũng có thể được đặt một cách linh động dựa trên người dùng tương tác. Điều này được minh hoạ trong ví dụ sau 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 về 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 (tăng và giảm)

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

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

Ảnh động về 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: Lắc lư (có vòng quay)

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

Ảnh động có hình dạng đàn hồi đang nảy
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ó tiếng sấm)

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

Dưới đây là ví dụ về một ảnh động thả bóng đơn giản được tăng cường bằng hiệu ứng tiếng sấm được phát mỗi khi bóng bật ra khỏi mép màn hình:

Ảnh động về một quả bóng rơi đang bật ra khỏi 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;
          }
        }
      });
  }
}