מגבילי אנימציה ותכנים קומפוזביליים

Compose כולל רכיבים מורכבים ומשתני מודיפיקטור מובנים לטיפול בתרחישי שימוש נפוצים של אנימציה.

רכיבים מורכבים עם אנימציה מובנית

אנימציה של הופעה והיעלמות באמצעות AnimatedVisibility

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

הרכיב המודד AnimatedVisibility מאפשר להציג אנימציה של הופעה והיעלמות של התוכן שלו.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

כברירת מחדל, התוכן מופיע בהדרגה ומתרחב, והוא נעלם בהדרגה ומתכווץ. אפשר להתאים אישית את המעבר על ידי ציון הערכים EnterTransition ו-ExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(
        "Hello",
        Modifier
            .fillMaxWidth()
            .height(200.dp)
    )
}

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

EnterTransition ו-ExitTransition דוגמאות

EnterTransition ExitTransition
fadeIn
אנימציה של מעבר מסמוי להצגה
fadeOut
אנימציה של דהייה
slideIn
אנימציה של החלקה פנימה
slideOut
אנימציה של החלקה החוצה
slideInHorizontally
אנימציה של הזזה אופקית
slideOutHorizontally
אנימציה של החלקה החוצה אופקית
slideInVertically
אנימציה של החלקה לאורך
slideOutVertically
אנימציה של החלקה החוצה בכיוון אנכי
scaleIn
אנימציה של צמצום
scaleOut
אנימציה של סילומיות אופקית (scale out)
expandIn
הרחבה באנימציה
shrinkOut
אנימציה של התכווצות
expandHorizontally
אנימציה של הרחבה אופקית
shrinkHorizontally
אנימציה של התכווצות אופקית
expandVertically
אנימציה של הרחבה אנכית
shrinkVertically
אנימציה של התכווצות אנכית

AnimatedVisibility מציע גם וריאנט שמקבל MutableTransitionState. כך תוכלו להפעיל אנימציה ברגע ש-AnimatedVisibility יתווסף לעץ הקומפוזיציה. הוא גם שימושי למעקב אחרי מצב האנימציה.

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

אנימציה של כניסה ויציאה לילדים

תוכן ב-AnimatedVisibility (צאצאים ישירים או עקיפים) יכול להשתמש במשתנה animateEnterExit כדי לציין התנהגות אנימציה שונה לכל אחד מהם. האפקט החזותי של כל אחד מהצאצאים האלה הוא שילוב של האנימציות שצוינו ב-composable של AnimatedVisibility והאנימציות של הכניסה והיציאה של הצאצא עצמו.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(
        Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

במקרים מסוימים, יכול להיות שתרצו ש-AnimatedVisibility לא יחיל אנימציות בכלל, כדי שכל ילד או ילדה יוכלו להגדיר אנימציות ייחודיות משלהם באמצעות animateEnterExit. כדי לעשות זאת, מציינים את EnterTransition.None ו-ExitTransition.None ב-composable של AnimatedVisibility.

הוספת אנימציה בהתאמה אישית

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

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

פרטים על Transition זמינים במאמר updateTransition.

אנימציה על סמך מצב היעד באמצעות AnimatedContent

הרכיב הניתן לקיבוץ AnimatedContent מפעיל אנימציה של התוכן שלו כשהוא משתנה על סמך מצב יעד.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

חשוב לזכור שצריך תמיד להשתמש בפרמטר lambda ולהציג אותו בתוכן. ה-API משתמש בערך הזה כמפתח לזיהוי התוכן שמוצג כרגע.

כברירת מחדל, התוכן הראשוני נעלם בהדרגה ואז התוכן היעד מופיע בהדרגה (ההתנהגות הזו נקראת העברה בהדרגה). אפשר להתאים אישית את התנהגות האנימציה הזו על ידי ציון אובייקט ContentTransform לפרמטר transitionSpec. אפשר ליצור את ContentTransform על ידי שילוב של EnterTransition עם ExitTransition באמצעות פונקציית הביניים with. אפשר להחיל את SizeTransform על ContentTransform באמצעות הצמדה שלה באמצעות פונקציית הביניים using.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition מגדיר איך תוכן היעד אמור להופיע, ו-ExitTransition מגדיר איך התוכן הראשוני אמור להיעלם. בנוסף לכל הפונקציות של EnterTransition ו-ExitTransition שזמינות ב-AnimatedVisibility, ב-AnimatedContent יש את הפונקציות slideIntoContainer ו-slideOutOfContainer. אלה חלופות נוחות ל-slideInHorizontally/Vertically ול-slideOutHorizontally/Vertically, שמחשבות את מרחק ההזזה על סמך הגדלים של התוכן הראשוני ושל תוכן היעד של התוכן AnimatedContent.

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

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

אנימציה של מעברים של ילדים לכניסה וליציאה

בדומה ל-AnimatedVisibility, המשתנה animateEnterExit זמין בתוך פונקציית הלמהדה של התוכן ב-AnimatedContent. אפשר להשתמש באפשרות הזו כדי להחיל את EnterAnimation ו-ExitAnimation על כל אחד מהצאצאים הישירים או העקיפים בנפרד.

הוספת אנימציה בהתאמה אישית

בדומה לשדה AnimatedVisibility, השדה transition זמין בתוך פונקציית הלמהדה של התוכן של AnimatedContent. אפשר להשתמש בו כדי ליצור אפקט אנימציה בהתאמה אישית שפועל בו-זמנית עם המעבר AnimatedContent. פרטים נוספים זמינים במאמר updateTransition.

אנימציה בין שתי פריסות באמצעות Crossfade

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

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

משתני אנימציה מובנים

אנימציה של שינויים בגודל של רכיבים שאפשר ליצור מהם קומפוזיציות באמצעות animateContentSize

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

המשתנה המשנה animateContentSize מפעיל אנימציה של שינוי גודל.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

הנפשות של פריטים ברשימה

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