אנימציות מבוססות-ערך

הוספת אנימציה של ערך יחיד באמצעות animate*AsState

הפונקציות animate*AsState הן ממשקי ה-API הפשוטים ביותר ליצירת אנימציה ב-Compose, שמאפשרים ליצור אנימציה של ערך יחיד. אתם מספקים רק את ערך היעד (או ערך הסיום), וה-API מתחיל את האנימציה מהערך הנוכחי לערך שצוין.

בהמשך מופיעה דוגמה ליצירת אנימציה של אלפא באמצעות ה-API הזה. פשוט עוטפים את ערך היעד ב-animateFloatAsState, וערך האלפא הוא עכשיו ערך אנימציה בין הערכים שסופקו (1f או 0.5f במקרה הזה).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

שימו לב שאתם לא צריכים ליצור מופע של שיעור אנימציה כלשהו, או לטפל בהפרעות. מתחת לפני השטח, ייווצר אובייקט אנימציה (כלומר, מופע של Animatable) ויישמר באתר הקריאה, עם ערך היעד הראשון בתור הערך הראשוני שלו. מעכשיו ואילך, בכל פעם שתספקו לערך המורכב הזה ערך יעד שונה, תופעל באופן אוטומטי אנימציה לכיוון הערך הזה. אם כבר קיימת אנימציה בטיסה, האנימציה מתחילה מהערך הנוכחי (והמהירות) שלה וממשיכה לכיוון ערך היעד. במהלך האנימציה, ה-Composable הזה עובר קומפוזיציה מחדש ומחזיר ערך אנימציה מעודכן בכל פריים.

כברירת מחדל, Compose מספק פונקציות animate*AsState עבור Float,‏ Color,‏ Dp,‏ Size,‏ Offset,‏ Rect,‏ Int,‏ IntOffset ו-IntSize. כדי להוסיף תמיכה בסוגי נתונים אחרים, אפשר לספק ל-animateValueAsState את הערך TwoWayConverter שמקבל סוג כללי.

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

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

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

המצבים יכולים להיות מכל סוג של נתונים. במקרים רבים תוכלו להשתמש בסוג enum בהתאמה אישית כדי לשמור על בטיחות הסוג, כמו בדוגמה הבאה:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition יוצרת מכונה של Transition, שומרת אותה בזיכרון ומעדכנת את המצב שלה.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

לאחר מכן תוכלו להשתמש באחת מפונקציות ההרחבה animate* כדי להגדיר אנימציה של הצאצא במעבר הזה. מציינים את ערכי היעד לכל אחת מהמדינות. הפונקציות animate* מחזירות ערך אנימציה שמתעדכן בכל פריים במהלך האנימציה, כשמצב המעבר מתעדכן באמצעות updateTransition.

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

לחלופין, אפשר להעביר פרמטר transitionSpec כדי לציין AnimationSpec שונה לכל אחד מהשילובים של השינויים במצב המעבר. למידע נוסף, ראו AnimationSpec.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

לאחר שהמעבר יגיע למצב היעד, הערך של Transition.currentState יהיה זהה לערך של Transition.targetState. כך אפשר לדעת אם המעבר הסתיים.

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

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

שימוש במעבר עם AnimatedVisibility ו-AnimatedContent

AnimatedVisibility ו-AnimatedContent זמינים כפונקציות של תוספים של Transition. הערכים targetState של Transition.AnimatedVisibility ו-Transition.AnimatedContent נגזרים מה-Transition, ומפעילים מעברים בין כניסה/יציאה לפי הצורך כשה-targetState של Transition משתנה. פונקציות התוספים האלה מאפשרות להעביר את כל האנימציות של הכניסה/היציאה/השינוי בגודל, שהיו אמורות להיות פנימיות ב-AnimatedVisibility/AnimatedContent, אל Transition. באמצעות פונקציות התוסף האלה, אפשר לצפות בשינוי המצב של AnimatedVisibility/AnimatedContent מבחוץ. במקום פרמטר visible בוליאני, בגרסה הזו של AnimatedVisibility מופיע פונקציית lambda שממירה את מצב היעד של המעבר ההורה לבוליאני.

פרטים נוספים זמינים במאמרים AnimatedVisibility ו-AnimatedContent.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

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

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

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

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

יצירת אנימציה שחוזרת על עצמה ללא הגבלה באמצעות rememberInfiniteTransition

InfiniteTransition מכיל אנימציית צאצא אחת או יותר, כמו Transition, אבל האנימציות מתחילות לפעול ברגע שהן נכנסות ליצירה ולא מפסיקות עד שהן יוסרו. אפשר ליצור מכונה של InfiniteTransition עם rememberInfiniteTransition. אפשר להוסיף אנימציות צאצא באמצעות animateColor,‏ animatedFloat או animatedValue. צריך גם לציין את הערך infiniteRepeatable כדי לציין את מפרטי האנימציה.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

ממשקי API של אנימציה ברמה נמוכה

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

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

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

תרשים שבו מוצג הקשר בין ממשקי ה-API השונים ליצירת אנימציות ברמה נמוכה

Animatable: אנימציה של ערך יחיד שמבוססת על שגרת המשך (coroutine)

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

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

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

בדוגמה שלמעלה, אנחנו יוצרים ושומרים מופע של Animatable עם הערך הראשוני של Color.Gray. בהתאם לערך של הדגל הבוליאני ok, הצבע מוסיף אנימציה ל-Color.Green או ל-Color.Red. כל שינוי נוסף בערך הבוליאני יגרום להפעלת אנימציה לצבע השני. אם יש אנימציה מתמשכת כשהערך משתנה, האנימציה מבוטלת והאנימציה החדשה מתחילה מערך קובץ ה-snapshot הנוכחי עם המהירות הנוכחית.

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

כברירת מחדל, Animatable תומך ב-Float וב-Color, אבל אפשר להשתמש בכל סוג נתונים על ידי הצגת TwoWayConverter. מידע נוסף זמין במאמר AnimationVector.

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

Animation: אנימציה בשליטה ידנית

Animation הוא רמת Animation API הנמוכה ביותר שזמינה. רבות מהאנימציות שראינו עד עכשיו מבוססות על Animation. יש שני סוגי משנה של Animation: TargetBasedAnimation ו-DecayAnimation.

יש להשתמש ב-Animation רק כדי לשלוט באופן ידני בזמן של האנימציה. Animation הוא ללא מצב (stateless), ואין לו מושג של מחזור חיים. הוא משמש כמנוע חישוב של אנימציה שמשמש את ממשקי ה-API ברמה גבוהה יותר.

TargetBasedAnimation

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

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

בניגוד ל-TargetBasedAnimation, לא צריך לספק targetValue כשמשתמשים ב-DecayAnimation. במקום זאת, הוא מחשב את הערך של targetValue על סמך תנאי ההתחלה שמוגדרים על ידי initialVelocity ו-initialValue ועל סמך הערך של DecayAnimationSpec שסופק.

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