ایجاد جلوه های لمسی سفارشی

این صفحه نمونه هایی از نحوه استفاده از API های لمسی مختلف برای ایجاد جلوه های سفارشی در یک برنامه اندروید را پوشش می دهد. از آنجایی که بسیاری از اطلاعات موجود در این صفحه متکی به دانش خوبی از عملکرد یک محرک ارتعاشی است، توصیه می کنیم پرایمر محرک لرزش را بخوانید.

این صفحه شامل نمونه های زیر می باشد.

برای مثال‌های بیشتر، به افزودن بازخورد لمسی به رویدادها مراجعه کنید و همیشه از اصول طراحی لمسی پیروی کنید.

برای رسیدگی به سازگاری دستگاه از بک گراند استفاده کنید

هنگام اجرای هر افکت سفارشی، موارد زیر را در نظر بگیرید:

  • کدام قابلیت های دستگاه برای اثر مورد نیاز است
  • وقتی دستگاه قادر به پخش افکت نیست چه باید کرد

مرجع Android haptics API جزئیاتی را در مورد نحوه بررسی پشتیبانی از مؤلفه‌های درگیر در هاپتیک شما ارائه می‌کند تا برنامه شما بتواند یک تجربه کلی ثابت را ارائه دهد.

بسته به مورد استفاده شما، ممکن است بخواهید جلوه های سفارشی را غیرفعال کنید یا افکت های سفارشی جایگزین را بر اساس قابلیت های بالقوه مختلف ارائه دهید.

برای کلاس های سطح بالای قابلیت دستگاه زیر برنامه ریزی کنید:

  • اگر از اولیه‌های لمسی استفاده می‌کنید: دستگاه‌هایی که از آن اولیه‌های مورد نیاز افکت‌های سفارشی پشتیبانی می‌کنند. (برای جزئیات بیشتر در مورد بدوی به بخش بعدی مراجعه کنید.)

  • دستگاه هایی با کنترل دامنه

  • دستگاه‌هایی با پشتیبانی ارتعاش اولیه (روشن/خاموش) - به عبارت دیگر، دستگاه‌هایی که فاقد کنترل دامنه هستند.

اگر انتخاب اثر لمسی برنامه شما برای این دسته بندی ها باشد، تجربه کاربری لمسی آن باید برای هر دستگاه جداگانه قابل پیش بینی باقی بماند.

استفاده از اولیه های لمسی

اندروید شامل چندین نسخه اولیه لمسی است که هم از نظر دامنه و هم از نظر فرکانس متفاوت هستند. برای دستیابی به اثرات لمسی غنی، می توانید از یک اولیه به تنهایی یا چند نمونه اولیه در ترکیب استفاده کنید.

  • از تأخیرهای 50 میلی‌ثانیه یا بیشتر برای شکاف‌های قابل تشخیص بین دو نمونه اولیه استفاده کنید، همچنین در صورت امکان ، مدت زمان اولیه را نیز در نظر بگیرید.
  • از مقیاس هایی استفاده کنید که با نسبت 1.4 یا بیشتر تفاوت دارند تا تفاوت در شدت بهتر درک شود.
  • از مقیاس های 0.5، 0.7 و 1.0 برای ایجاد یک نسخه با شدت کم، متوسط ​​و بالا از یک اولیه استفاده کنید.

الگوهای ارتعاشی سفارشی ایجاد کنید

الگوهای ارتعاشی اغلب در لمس های توجهی مانند اعلان ها و آهنگ های زنگ استفاده می شوند. سرویس Vibrator می تواند الگوهای ارتعاشی طولانی را پخش کند که دامنه ارتعاش را در طول زمان تغییر می دهد. چنین اثراتی شکل موج نامیده می شوند.

جلوه های شکل موج را می توان به راحتی درک کرد، اما ارتعاشات طولانی ناگهانی می تواند کاربر را مبهوت کند، اگر در یک محیط آرام پخش شود. شیب دار به دامنه هدف بسیار سریع نیز ممکن است صداهای وزوز قابل شنیدن تولید کند. توصیه برای طراحی الگوهای شکل موج، هموارسازی انتقال دامنه برای ایجاد افکت های شیب دار بالا و پایین است.

نمونه: الگوی شیب دار

شکل موج به صورت VibrationEffect با سه پارامتر نشان داده می شود:

  1. زمان بندی: آرایه ای از مدت زمان، بر حسب میلی ثانیه، برای هر بخش شکل موج.
  2. دامنه: دامنه ارتعاش مورد نظر برای هر مدت زمان مشخص شده در آرگومان اول، که با یک عدد صحیح از 0 تا 255 نشان داده شده است، با 0 نشان دهنده ویبراتور "خاموش" و 255 حداکثر دامنه دستگاه است.
  3. Repeat index: شاخصی در آرایه مشخص شده در آرگومان اول برای شروع تکرار شکل موج، یا -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);

نمونه: مقاومت (با تیک کم)

شما می توانید دامنه ارتعاش اولیه را کنترل کنید تا بازخورد مفیدی را به یک اقدام در حال انجام انتقال دهید. مقادیر مقیاس نزدیک به فاصله را می توان برای ایجاد یک اثر کرشندوی صاف از یک اولیه استفاده کرد. تأخیر بین اولیه های متوالی نیز می تواند به صورت پویا بر اساس تعامل کاربر تنظیم شود. این در مثال زیر از یک انیمیشن نمای کنترل شده توسط یک حرکت کشیدن و تقویت شده با لمسی نشان داده شده است.

انیمیشن یک دایره که به پایین کشیده می شود
نمودار شکل موج ارتعاش ورودی

کاتلین

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

نمونه: لرزش (با چرخش)

یکی از اصول کلیدی لمسی ، خوشحال کردن کاربران است. یک راه سرگرم کننده برای معرفی یک اثر لرزش غیرمنتظره دلپذیر، استفاده از 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;
          }
        }
      });
  }
}