התאמה אישית של אנימציות

הרבה ממשקי API של אנימציה מקבלים בדרך כלל פרמטרים להתאמה אישית של ההתנהגות שלהם.

התאמה אישית של אנימציות באמצעות הפרמטר AnimationSpec

רוב ממשקי ה-API של אנימציות מאפשרים למפתחים להתאים אישית את מפרטי האנימציה באמצעות AnimationSpecפרמטר אופציונלי.

val alpha: Float by animateFloatAsState(
    targetValue = if (enabled) 1f else 0.5f,
    // Configure the animation duration and easing.
    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
    label = "alpha"
)

יש סוגים שונים של AnimationSpec ליצירת סוגים שונים של אנימציות.

יצירת אנימציה שמבוססת על פיזיקה באמצעות spring

spring יוצרת אנימציה שמבוססת על פיזיקה בין ערכי ההתחלה והסיום. היא מקבלת 2 פרמטרים: dampingRatio ו-stiffness.

dampingRatio מגדיר את מידת הקפיציות של הקפיץ. ערך ברירת המחדל הוא Spring.DampingRatioNoBouncy.

איור 1. הגדרת יחסי שיכוך שונים של קפיצים.

stiffness מגדיר את מהירות התנועה של הקפיץ לעבר ערך הסיום. ערך ברירת המחדל הוא Spring.StiffnessMedium.

איור 2. הגדרת קשיחות שונה של הקפיץ.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,
        stiffness = Spring.StiffnessMedium
    ),
    label = "spring spec"
)

spring יכול להתמודד עם הפרעות בצורה חלקה יותר מאשר סוגים של AnimationSpec שמבוססים על משך זמן, כי הוא מבטיח את הרציפות של המהירות כשערך היעד משתנה באמצע האנימציות. ‫spring משמש כ-AnimationSpec כברירת מחדל בהרבה ממשקי API של אנימציה, כמו animate*AsState ו-updateTransition.

לדוגמה, אם נחיל הגדרת spring על האנימציה הבאה שמופעלת על ידי מגע המשתמש, כשמפריעים לאנימציה במהלך ההתקדמות שלה, אפשר לראות שהשימוש ב-tween לא מגיב בצורה חלקה כמו השימוש ב-spring.

איור 3. הגדרת מפרטים של אנימציה tween לעומת spring והפסקת האנימציה.

אנימציה בין ערכי התחלה וסיום עם עקומת שיכוך עם tween

tween יוצר אנימציה בין ערכי ההתחלה והסיום לאורך durationMillis שצוין באמצעות עקומת שיכוך. הערך tween הוא קיצור של המילה between (בין) – כי הוא מופיע בין שני ערכים.

אפשר גם לציין delayMillis כדי לדחות את תחילת האנימציה.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
        durationMillis = 300,
        delayMillis = 50,
        easing = LinearOutSlowInEasing
    ),
    label = "tween delay"
)

מידע נוסף זמין במאמר בנושא האצה והאטה.

אנימציה לערכים ספציפיים בתזמונים מסוימים באמצעות keyframes

keyframes מונפש על סמך ערכי תמונת המצב שצוינו בחותמות זמן שונות במהלך האנימציה. בכל רגע נתון, ערך האנימציה יהיה אינטרפולציה בין שני ערכים של מסגרות מפתח. לכל אחד מפריימי המפתח האלה אפשר לציין Easing כדי לקבוע את עקומת האינטרפולציה.

אפשר לציין את הערכים ב-0 אלפיות השנייה ובמשך הזמן. אם לא מציינים את הערכים האלה, ברירת המחדל היא ערכי ההתחלה והסיום של האנימציה, בהתאמה.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
        durationMillis = 375
        0.0f at 0 using LinearOutSlowInEasing // for 0-15 ms
        0.2f at 15 using FastOutLinearInEasing // for 15-75 ms
        0.4f at 75 // ms
        0.4f at 225 // ms
    },
    label = "keyframe"
)

יצירת אנימציה חלקה בין תמונות מפתח באמצעות keyframesWithSplines

כדי ליצור אנימציה שמתבססת על מעבר חלק בין ערכים, אפשר להשתמש במפרטי אנימציה של keyframesWithSplines במקום keyframes.

val offset by animateOffsetAsState(
    targetValue = Offset(300f, 300f),
    animationSpec = keyframesWithSpline {
        durationMillis = 6000
        Offset(0f, 0f) at 0
        Offset(150f, 200f) atFraction 0.5f
        Offset(0f, 100f) atFraction 0.7f
    }
)

פריימים מרכזיים מבוססי-ספלין שימושיים במיוחד לתנועה דו-ממדית של פריטים במסך.

הסרטונים הבאים ממחישים את ההבדלים בין keyframes לבין keyframesWithSpline בהינתן אותה קבוצה של קואורדינטות x ו-y שעיגול צריך לנוע לפיהן.

keyframes keyframesWithSplines

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

איך חוזרים על אנימציה עם repeatable

repeatable מפעיל אנימציה שמבוססת על משך זמן (כמו tween או keyframes) שוב ושוב עד שמגיעים למספר האיטרציות שצוין. אפשר להעביר את הפרמטר repeatMode כדי לציין אם האנימציה צריכה לחזור על עצמה על ידי התחלה מההתחלה (RepeatMode.Restart) או מהסוף (RepeatMode.Reverse).

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = repeatable(
        iterations = 3,
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    ),
    label = "repeatable spec"
)

חזרה אינסופית על אנימציה באמצעות infiniteRepeatable

הפונקציה infiniteRepeatable דומה ל-repeatable, אבל היא חוזרת על עצמה מספר אינסופי של פעמים.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    ),
    label = "infinite repeatable"
)

בבדיקות שבהן נעשה שימוש ב-ComposeTestRule, אנימציות שמשתמשות ב-infiniteRepeatable לא מופעלות. הרכיב יעבור רינדור באמצעות הערך ההתחלתי של כל ערך מונפש.

מעבר מיידי לערך הסופי באמצעות snap

snap הוא AnimationSpec מיוחד שמעביר את הערך באופן מיידי לערך הסופי. אפשר לציין delayMillis כדי לעכב את תחילת האנימציה.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = snap(delayMillis = 50),
    label = "snap spec"
)

הגדרת פונקציית easing מותאמת אישית

פעולות מבוססות-משך (כמו tween או keyframes) משתמשות ב-Easing כדי להתאים את החלק של האנימציה.AnimationSpec כך ערך האנימציה יכול להאיץ ולהאט, במקום לנוע בקצב קבוע. השבר הוא ערך בין 0 (התחלה) ל-1.0 (סוף) שמציין את הנקודה הנוכחית באנימציה.

ההאצה היא למעשה פונקציה שמקבלת ערך שבר בין 0 ל-1.0 ומחזירה מספר עשרוני. הערך שמוחזר יכול להיות מחוץ לגבול כדי לייצג חריגה או חוסר. אפשר ליצור מעבר הדרגתי מותאם אישית כמו בקוד שבהמשך.

val CustomEasing = Easing { fraction -> fraction * fraction }

@Composable
fun EasingUsage() {
    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 300,
            easing = CustomEasing
        ),
        label = "custom easing"
    )
    // ……
}

ב-Compose יש כמה פונקציות מובנות של Easing שמתאימות לרוב תרחישי השימוש. מידע נוסף על סוגי ה-Easing שמתאימים לתרחישים שונים זמין במאמר Speed - Material Design.

  • FastOutSlowInEasing
  • LinearOutSlowInEasing
  • FastOutLinearEasing
  • LinearEasing
  • CubicBezierEasing
  • מידע נוסף

הנפשת סוגי נתונים מותאמים אישית על ידי המרה אל AnimationVector וממנו

רוב ממשקי ה-API של אנימציות ב-Compose תומכים ב-Float, ‏ Color, ‏ Dp ובסוגי נתונים בסיסיים אחרים כערכי אנימציה כברירת מחדל, אבל לפעמים צריך להנפיש סוגי נתונים אחרים, כולל סוגי נתונים מותאמים אישית. במהלך האנימציה, כל ערך של אנימציה מיוצג כ-AnimationVector. הערך מומר ל-AnimationVector ולהפך על ידי TwoWayConverter תואם, כדי שמערכת האנימציה המרכזית תוכל לטפל בהם באופן אחיד. לדוגמה, Int מיוצג כ-AnimationVector1D שמכיל ערך נקודה צפה יחיד. ‫TwoWayConverter for Int נראה כך:

val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })

הערך Color הוא למעשה קבוצה של 4 ערכים: אדום, ירוק, כחול ואלפא, ולכן הערך Color מומר לערך AnimationVector4D שמכיל 4 ערכים מסוג float. בדרך הזו, כל סוג נתונים שמשמש באנימציות מומר ל-AnimationVector1D, ‏ AnimationVector2D, ‏ AnimationVector3D או AnimationVector4D, בהתאם לממדיות שלו. כך אפשר להנפיש רכיבים שונים של האובייקט באופן עצמאי, כל אחד עם מעקב מהירות משלו. אפשר לגשת לממירים מובנים לסוגי נתונים בסיסיים באמצעות ממירים כמו Color.VectorConverter או Dp.VectorConverter.

אם רוצים להוסיף תמיכה בסוג נתונים חדש כערך של אנימציה, אפשר ליצור TwoWayConverter משלכם ולספק אותו ל-API. לדוגמה, אתם יכולים להשתמש ב-animateValueAsState כדי להנפיש את סוג הנתונים המותאם אישית שלכם כך:

data class MySize(val width: Dp, val height: Dp)

@Composable
fun MyAnimation(targetSize: MySize) {
    val animSize: MySize by animateValueAsState(
        targetSize,
        TwoWayConverter(
            convertToVector = { size: MySize ->
                // Extract a float value from each of the `Dp` fields.
                AnimationVector2D(size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector2D ->
                MySize(vector.v1.dp, vector.v2.dp)
            }
        ),
        label = "size"
    )
}

הרשימה הבאה כוללת כמה VectorConverters מובנים: