إنشاء تأثيرات ملموسة مخصَّصة

تتناول هذه الصفحة أمثلة حول كيفية استخدام واجهات برمجة تطبيقات تعمل باللمس مختلفة لإنشاء تأثيرات مخصّصة في تطبيق Android. بقدر كبير من المعلومات الواردة في هذه الصفحة يعتمد على معرفة جيدة بعمل مشغّل الاهتزاز، ننصحك بقراءة المعلومات التمهيدية عن مشغّل الاهتزاز.

تتضمّن هذه الصفحة الأمثلة التالية.

للحصول على أمثلة إضافية، يمكنك مراجعة المقالة إضافة ملاحظات ملموسة إلى الأحداث والالتزام دائمًا بمبادئ تصميم اللمس.

استخدام العناصر الاحتياطية للتعامل مع توافق الجهاز

عند تنفيذ أي تأثير مخصّص، يجب مراعاة ما يلي:

  • إمكانات الجهاز المطلوبة لإحداث التأثير
  • الإجراءات التي يجب اتّخاذها في حال تعذّر على الجهاز تشغيل التأثير

يوفر مرجع واجهة برمجة التطبيقات التي تعمل باللمس لنظام التشغيل Android تفاصيل حول كيفية التحقق من دعم المكونات المضمّنة في تقنية اللمس، وذلك حتى يتمكن تطبيقك من تقديم تجربة شاملة متسقة.

واستنادًا إلى حالة استخدامك، قد تريد إيقاف التأثيرات المخصّصة أو توفير تأثيرات مخصّصة بديلة استنادًا إلى إمكانات محتملة مختلفة.

التخطيط للفئات التالية عالية المستوى من إمكانات الأجهزة:

  • إذا كنت تستخدم الإصدارات الأولية الملموسة: وهي أجهزة تتوافق مع هذه العناصر الأساسية التي تحتاج إليها التأثيرات المخصّصة. (راجع القسم التالي للحصول على تفاصيل حول الأساسيات).

  • الأجهزة المزوّدة بالتحكّم في السعة

  • الأجهزة التي بها دعم الاهتزاز الأساسي (تفعيل/إيقاف) - بعبارة أخرى، تلك الأجهزة تفتقر إلى التحكّم في السعة.

إذا كان اختيار التأثير الملموس لتطبيقك يراعي هذه الفئات، يجب أن تظل تجربة المستخدم الملموسة قابلة للتنبؤ بأي جهاز فردي.

استخدام التطبيقات الأولية الملموسة

يشتمل Android على العديد من المبادئ الأساسية لللمس التي تختلف في السعة والتكرار. يمكنك استخدام مجموعة أساسية واحدة فقط أو عدة عناصر أولية مع بعضها لتحقيق تأثيرات ملموسة غنية.

  • ويمكنك استخدام التأخيرات التي تبلغ 50 ملي ثانية أو أكثر للتعويض عن الفجوات الواضحة بين نموذجَين أساسيَين، مع الأخذ في الاعتبار أيضًا المدة الأوّلية إن أمكن.
  • استخدم مقاييس تختلف بنسبة 1.4 أو أكثر حتى يتم إدراك الفرق في الكثافة بشكل أفضل.
  • استخدم مقاييس 0.5 و0.7 و1.0 لإنشاء نسخة منخفضة ومتوسطة وعالية الكثافة من الوحدة الأساسية.

إنشاء أنماط اهتزاز مخصصة

غالبًا ما يتم استخدام أنماط الاهتزاز في اللمسات الانتباهية، مثل الإشعارات ونغمات الرنين. يمكن لخدمة Vibrator تشغيل أنماط اهتزاز طويلة تغيّر سعة الاهتزاز بمرور الوقت. وتُعرف هذه التأثيرات باسم الأشكال الموجية.

يمكن بسهولة إدراك تأثيرات الشكل الموجي، إلا أن الاهتزازات المفاجئة الطويلة يمكن أن تُزعج المستخدم إذا تم تشغيلها في مكان هادئ. قد ينتج أيضًا المنحدر إلى السعة المستهدفة بسرعة كبيرة جدًا ضوضاء طنينة مسموعة. والتوصية بتصميم أنماط الشكل الموجي هي تسهيل انتقالات السعة لإنشاء تأثيرات ارتفاع وانخفاض.

مثال: نمط الزيادة التدريجية

يتم تمثيل الأشكال الموجية على شكل VibrationEffect بثلاث معلَمات:

  1. التوقيتات: مصفوفة من الفترات بالمللي ثانية لكل قطاع من الشكل الموجي.
  2. سعة الاهتزاز: شدة الاهتزاز المطلوبة لكل مدة محددة في الوسيطة الأولى، ممثلة بقيمة عدد صحيح من 0 إلى 255، على أن يمثّل الرقم 0 "إيقاف" الهزاز والرقم 255 إلى أقصى اتساع للجهاز.
  3. فهرس التكرار: الفهرس في الصفيف المحدد في الوسيطة الأولى لبدء تكرار الشكل الموجي أو -1 إذا كان يجب تشغيل النمط مرة واحدة فقط.

وفي ما يلي مثال على الشكل الموجي الذي ينبض مرتين مع إيقاف مؤقت يبلغ 350 مللي ثانية بين النبضات. النبضة الأولى عبارة عن منحدر سلس يصل إلى أقصى سعة، والثانية منحدرًا سريعًا للاحتفاظ بأقصى سعات. يتم تحديد التوقف في النهاية من خلال قيمة التكرار السالب للفهرس.

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

النموذج: نمط متكرر

يمكن أيضًا تشغيل الأشكال الموجية بشكل متكرّر إلى أن يتم إلغاؤها. تتمثل طريقة إنشاء شكل موجي متكرر في تعيين معلمة "تكرار" غير سالبة. عند تشغيل شكل موجي متكرر، يستمر الاهتزاز إلى أن يتم إلغاؤه بشكل واضح في الخدمة:

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

يُعد هذا مفيدًا جدًا للأحداث المتقطعة التي تتطلب إجراء المستخدم للإقرار بها. ومن الأمثلة على هذه الأحداث المكالمات الهاتفية الواردة والمنبّهات المشغّلة.

نموذج: نمط مع عنصر احتياطي

ويعد التحكم في نطاق الاهتزاز إمكانية تعتمد على الأجهزة. يؤدي تشغيل الشكل الموجي على جهاز منخفض النهاية بدون هذه الإمكانية إلى اهتزاز الجهاز عند أقصى اتساع لكل إدخال موجب في مصفوفة السعة. إذا كان تطبيقك في حاجة إلى التوافق مع هذه الأجهزة، نقترح عليك التأكد من أنّ النمط لا يولّد أي تأثير صاخب عند تشغيله في هذه الحالة، أو تصميم نمط تشغيل/إيقاف أبسط يمكن تشغيله كنمط احتياطي بدلاً من ذلك.

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

إنشاء تركيبات اهتزازية

ويستعرض هذا القسم طرقًا لتكوين تأثيرات مخصّصة أطول وأكثر تعقيدًا، ويتجاوز ذلك استكشاف اللمسات الغنية باستخدام إمكانات أجهزة أكثر تقدمًا. يمكنك استخدام مجموعات من التأثيرات التي تختلف في السعة والتكرار لإنشاء تأثيرات ملموسة أكثر تعقيدًا على الأجهزة المزوّدة بالمشغّلات اللمسية التي لها معدّل نقل بيانات أوسع نطاقًا.

تشرح عملية إنشاء أنماط اهتزاز مخصّصة، التي تم توضيحها سابقًا في هذه الصفحة، كيفية التحكم في سعة الاهتزاز لإنشاء تأثيرات سلسة للتحرك لأعلى ولأسفل. تعمل تقنية اللمس الغنية على تحسين هذا المفهوم من خلال استكشاف نطاق التردد الأوسع لجهاز الهزّاز لجعل التأثير أكثر سلاسة. هذه الأشكال الموجية فعالة بشكل خاص في إحداث تأثير تزايدي أو مضاعف.

تتولى الشركة المصنّعة للجهاز تنفيذ التركيبة الأولية الموضحة سابقًا في هذه الصفحة. وهي توفر اهتزازًا واضحًا وقصيرًا وممتعًا يتوافق مع مبادئ تقنية اللمس للحصول على تقنية اللمس الواضحة. للحصول على مزيد من التفاصيل حول هذه الإمكانات وآلية عملها، يمكنك الاطّلاع على البيانات التمهيدية للمشغّلات الاهتزازية.

لا يوفر Android عناصر احتياطية للمقطوعات الموسيقية التي تحتوي على أولية غير متوافقة. ننصحك بتنفيذ الخطوات التالية:

  1. قبل تفعيل تقنية اللمس المتقدمة، تحقق من أنّ جهازًا معيّنًا يتوافق مع جميع الإعدادات الأساسية التي تستخدمها.

  2. أوقِف المجموعة المتسقة من التجارب غير المتوافقة، وليس فقط التأثيرات التي تفتقد إلى عنصر أساسي. يتم عرض مزيد من المعلومات حول كيفية التحقق من دعم الجهاز على النحو التالي.

يمكنك إنشاء تأثيرات اهتزاز مركبة باستخدام VibrationEffect.Composition. في ما يلي مثال على تأثير متزايد ببطء متبوعًا بتأثير نقرة حاد:

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

يتم إنشاء المقطوعة الموسيقية من خلال إضافة مواد أولية ليتم تشغيلها بالتسلسل. كما أن جميع العناصر الأساسية قابلة للتطوير، بحيث يمكنك التحكم في سعة الاهتزاز الناجم عن كل منها. يُعرَّف المقياس على أنّه قيمة بين 0 و1، حيث يشير الرقم 0 إلى الحد الأدنى من السعة التي يمكن أن يرى المستخدم عندها هذا العنصر الأساسي (نادرًا).

إذا كنت ترغب في إنشاء نسخة ضعيفة وقوية من نفس النموذج الأساسي، فمن المستحسن أن تختلف المقاييس بنسبة 1.4 أو أكثر، بحيث يمكن ملاحظة الفرق في الكثافة بسهولة. لا تحاول إنشاء أكثر من ثلاثة مستويات كثافة من نفس العنصر الأساسي، لأنها ليست مميزة بشكل حدي. على سبيل المثال، استخدم المقاييس من 0.5 و0.7 و1.0 لإنشاء نسخة أساسية منخفضة ومتوسطة وعالية الكثافة.

يمكن للتركيبة أيضًا تحديد المهلات التي تُضاف بين الوحدات الأساسية المتتالية. يتم التعبير عن هذا التأخير بالملي ثانية منذ نهاية العنصر الأساسي السابق. وبوجه عام، تكون الفجوة من 5 إلى 10 مللي ثانية بين اثنين من الأساسيات قصيرة جدًا بحيث لا يمكن الكشف عنها. ضع في اعتبارك استخدام فجوة بترتيب 50 مللي ثانية أو أكثر إذا كنت تريد إنشاء فجوة واضحة بين اثنين من الأساسيات. في ما يلي مثال على مقطوعة موسيقية تتضمن تأخيرات:

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

يمكن استخدام واجهات برمجة التطبيقات التالية للتحقق من توافق الجهاز مع تطبيقات أساسية محدّدة:

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

من الممكن أيضًا التحقق من العديد من الأساسيات ثم تحديد أيها سيتم إنشاؤها بناءً على مستوى دعم الجهاز:

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

عيّنة: مقاومة (بمؤشرات منخفضة)

يمكنك التحكم في سعة الاهتزاز الأساسي لنقل ملاحظات مفيدة إلى إجراء جارٍ. يمكن استخدام قيم المقياس المتباعدة عن المسافة لإنشاء تأثير تصاعدي سلس للعنصر الأساسي. يمكن أيضًا تحديد التأخير بين العناصر الأساسية المتتالية ديناميكيًا استنادًا إلى تفاعل المستخدم. ويتضح هذا في المثال التالي لصورة متحركة لعرض يتم التحكّم فيها بإيماءة السحب وتعزّزها تقنية اللمس.

صورة متحركة لدائرة يتم سحبها للأسفل
مخطط الشكل الموجي للاهتزاز عند الإدخال

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

النموذج: التوسيع (مع ارتفاع وانخفاض)

هناك نموذجان أساسيان لتحسين شدة الاهتزاز المدوَّنة، وهما PRIMITIVE_QUICK_RISE وPRIMITIVE_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)
    }
  }
}

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

عيّنة: تمايل (مع دوران)

يتمثّل أحد مبادئ تقنية اللمس الرئيسية في إسعاد المستخدمين. يمكن استخدام PRIMITIVE_SPIN بطريقة ممتعة لتقديم تأثير اهتزاز لطيف غير متوقع. تكون هذه المجموعة الأولية أكثر فعالية عند استدعائها أكثر من مرة. يمكن أن تؤدي عمليات الدوران المتعددة المتسلسلة إلى حدوث تأثير متذبذب وغير مستقر، والذي يمكن تحسينه من خلال تطبيق تحجيم عشوائي إلى حد ما على كل وحدة أولية. يمكنك أيضًا تجربة الفجوة بين الإصدارات الأساسية المتتالية لدورات اللعبة. وذلك لأن دورانَين بدون أي فجوة (0 ملي ثانية بينهما) يخلق إحساسًا بالدوران الشديد. إنّ زيادة الفجوة بين الدوران من 10 إلى 50 ملي ثانية تؤدي إلى شعور في الدوران أكثر سلاسة، ويمكن استخدامها لمطابقة مدة الفيديو أو الصورة المتحركة.

لا ننصح باستخدام فجوة تزيد مدتها عن 100 ملي ثانية، لأنّ الأجزاء المتتالية لم تعُد تتكامل بشكل جيد وتبدأ في الشعور كتأثيرات فردية.

فيما يلي مثال على شكل مطاطي يرتد بعد سحبه لأسفل ثم تحريره. يتم تحسين الرسوم المتحركة باستخدام مؤثرات دوران يتم تشغيلها بكثافة متفاوتة تتناسب مع إزاحة الارتداد.

صورة متحركة لشكل مطاطي يهتز
مخطط الشكل الموجي للاهتزاز عند الإدخال

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

عيّنة: الارتداد (مع صوت مرتفع)

هناك تطبيق متقدم آخر لتأثيرات الاهتزاز وهو محاكاة التفاعلات الجسدية. بإمكان 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;
          }
        }
      });
  }
}