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

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

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

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

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

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

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

يوفر مرجع واجهة برمجة التطبيقات Android haptics API تفاصيل حول كيفية التحقق من دعمًا للمكونات المتضمنة في تقنية اللمس، بحيث يمكن أن يوفر تطبيقك تجربة شاملة متسقة.

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

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

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

  • الأجهزة التي تتضمّن إمكانية التحكّم في الاتساع

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

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

استخدام وحدات حسّية للّمس

يشتمل 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;
          }
        }
      });
  }
}