יצירת אפקטים פיזיים בהתאמה אישית

בדף הזה מפורטות דוגמאות לשימוש ב-haptics API שונים כדי ליצור אפקטים מותאמים אישית באפליקציה ל-Android. כל המידע על הדף הזה מסתמך על ידע טוב לגבי אופן הפעולה של אקטואטור רטט, אנחנו ממליצים לקרוא את גרסת ה-Primer של מפעיל הרטט.

הדף הזה כולל את הדוגמאות הבאות.

לדוגמאות נוספות, ראו הוספת משוב פיזי לאירועים. תמיד צריך לפעול בהתאם לעקרונות של עיצוב פיזי.

שימוש בחלופות כדי לטפל בתאימות מכשירים

כשמיישמים אפקט מותאם אישית, כדאי להביא בחשבון את הנקודות הבאות:

  • אילו יכולות של המכשיר נדרשות כדי ליצור את האפקט הזה
  • מה לעשות כשהמכשיר לא מסוגל להפעיל את האפקט

בחומרי העזר של 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());

אפשר להשתמש בממשקי ה-API הבאים כדי לאמת את התמיכה במכשירים פרימיטיביים:

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

דוגמה: Wbble (עם סיבובים)

אחד מהעקרונות המרכזיים של המשוב הפיזי הוא לספק שביעות רצון למשתמשים. דרך כיפית כדי ליצור אפקט רטט נעים בלתי צפוי הוא להשתמש 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;
          }
        }
      });
  }
}