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

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

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

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

var enabled by remember { mutableStateOf(true) }

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

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

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

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

// 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 כדי ליצור מעבר צאצא. הטכניקה הזו שימושית להפרדה בין בעיות בכמה רכיבי משנה ב-Composable מורכב. המעבר ברמת ההורה יכלול את כל ערכי האנימציה במעברים ברמת הצאצא.

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 יש למדה שממירה את מצב היעד של מעבר ההורה למצב בוליאני.

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

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

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

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

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

// 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. כל שינוי בערך הבוליאני שמתרחש לאחר מכן יפעיל אנימציה לצבע השני. אם יש אנימציה פעילה כשמשנים את הערך, האנימציה מבוטלת והאנימציה החדשה מתחילה מהערך הנוכחי של התמונה עם המהירות הנוכחית.

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

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

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

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 ומואטת עם הזמן.