מדריך מהיר לאנימציות בכתיבה

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

הנפשה של מאפיינים נפוצים של רכיבים קומפוזביליים

ב-Compose יש ממשקי API נוחים שמאפשרים לפתור הרבה תרחישים נפוצים של אנימציה. בקטע הזה מוסבר איך להוסיף אנימציה למאפיינים נפוצים של פונקציה קומפוזבילית.

הוספת אנימציה להופעה או להיעלמות

רכיב Composable ירוק שמופיע ונעלם
איור 1. הנפשה של הופעה והיעלמות של פריט בעמודה

משתמשים ב-AnimatedVisibility כדי להסתיר או להציג Composable. ילדים בתוך AnimatedVisibility יכולים להשתמש ב-Modifier.animateEnterExit() למעבר כניסה או יציאה משלהם.

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
    // ...
}

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

אפשרות נוספת להנפשת החשיפה של רכיב Composable היא להנפיש את האלפא לאורך זמן באמצעות animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenckGuide.kt

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

הנפשה של אלפא של רכיב קומפוזבילי
איור 2. הנפשה של אלפא של רכיב קומפוזבילי

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

רכיב Composable עם צבע רקע שמשתנה לאורך זמן כאנימציה, שבה הצבעים מתמזגים זה בזה.
איור 3. הנפשה של צבע הרקע של רכיב Composable

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composackGuide.kt

האפשרות הזו יעילה יותר משימוש ב-Modifier.background(). הפונקציה Modifier.background() מתאימה להגדרת צבע חד-פעמית, אבל כשמגדירים אנימציה של צבע לאורך זמן, היא עלולה לגרום ליותר הרכבות מחדש מהנדרש.

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

הנפשת הגודל של רכיב קומפוזבילי

רכיב קומפוזבילי ירוק שמשנה את הגודל שלו בצורה חלקה באנימציה.
איור 4. Composable smoothly animating between a small and a larger size

ב-Compose אפשר להוסיף אנימציה לגודל של רכיבים קומפוזביליים בכמה דרכים שונות. משתמשים ב-animateContentSize() לאנימציות בין שינויים בגודל של רכיבים קומפוזביליים.

לדוגמה, אם יש לכם תיבה שמכילה טקסט שיכול להתרחב משורה אחת לכמה שורות, אתם יכולים להשתמש ב-Modifier.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
        }

) {
}

אפשר גם להשתמש ב-AnimatedContent עם SizeTransform כדי לתאר איך שינויי הגודל צריכים להתבצע.

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

רכיב ירוק שניתן לשילוב עם אנימציה חלקה למטה וימינה
איור 5. ‫Composable שזז לפי היסט

כדי להוסיף אנימציה למיקום של רכיב קומפוזבילי, משתמשים ב-Modifier.offset{ } בשילוב עם animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
 ckGuide.kt

אם אתם רוצים לוודא שרכיבים קומפוזביליים לא משורטטים מעל או מתחת לרכיבים קומפוזביליים אחרים כשמפעילים אנימציה של מיקום או גודל, השתמשו ב-Modifier.layout{ }. המשתנה הזה מעביר את השינויים בגודל ובמיקום לרכיב ההורה, וכך משפיע על רכיבי הצאצא האחרים.

לדוגמה, אם אתם מעבירים Box בתוך Column, והצאצאים האחרים צריכים לזוז כשה-Box זז, צריך לכלול את פרטי ההיסט עם Modifier.layout{ } באופן הבא:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, con>straints -
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBluckGuide.kt

‫2 תיבות,כשהתיבה השנייה מנפישה את מיקום ה-X וה-Y שלה, והתיבה השלישית מגיבה בהזזה של עצמה בערך Y גם כן.
איור 6. יצירת אנימציה באמצעות Modifier.layout{ }

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

רכיב ירוק קומפוזבילי שמתכווץ ומתרחב בלחיצה, עם אנימציה של המרווח הפנימי
איור 7. ‫Composable עם אנימציה של המרווח הפנימי

כדי להוסיף אנימציה למרווח הפנימי של רכיב קומפוזבילי, משתמשים ב-animateDpAsState בשילוב עם Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
 ckGuide.kt

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

איור 8. הנפשה של גובה הקומפוזבילי בלחיצה

כדי להוסיף אנימציה לגובה של רכיב קומפוזבילי, משתמשים ב-animateDpAsState בשילוב עם Modifier.graphicsLayer{ }. כדי לשנות את הרשאות הגישה באופן חד-פעמי, משתמשים בפקודה Modifier.shadow(). אם אתם מוסיפים אנימציה לצל, שימוש ב-modifier‏ Modifier.graphicsLayer{ } הוא האפשרות היעילה יותר.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGrckGuide.kt

לחלופין, אפשר להשתמש ברכיב ה-Composable‏ Card ולהגדיר את מאפיין הגובה לערכים שונים לכל מצב.

הוספת אנימציה לשינוי הגודל, המיקום או הזווית של הטקסט

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

כשמגדירים אנימציה של שינוי קנה מידה, תרגום או סיבוב של טקסט, צריך להגדיר את הפרמטר textMotion ב-TextStyle לערך TextMotion.Animated. כך המעברים בין האנימציות של הטקסט יהיו חלקים יותר. משתמשים בModifier.graphicsLayer{ } כדי לתרגם, לסובב או לשנות את הגודל של הטקסט.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion =    )
}
AnimationQuickGuide.kt

הנפשה של צבע הטקסט

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

כדי להוסיף אנימציה לצבע הטקסט, משתמשים בביטוי הלמדא color ברכיב ה-Composable‏ BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        ani// ...
)AnimationQuickGuide.kt

מעבר בין סוגי תוכן שונים

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

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

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { tar>getState -
    when (targetState) {
        UiState>.Loading - {
            LoadingScreen()
        }
        UiStat>e.Loaded - {
            LoadedScreen()
        }
        UiSta>te.Error - {
            ErrorScreen()
       ckGuide.kt

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

הנפשה בזמן ניווט ליעדים שונים

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

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

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
        >    )
        }
    ) { backStackEntry -
        ScreenDetai
        )
    }
}AnimationQuickGuide.kt

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

חזרה על אנימציה

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

משתמשים ב-rememberInfiniteTransition עם infiniteRepeatable animationSpec כדי שהאנימציה תחזור על עצמה ברצף. משנים את RepeatMode כדי לציין איך הוא אמור לנוע קדימה ואחורה.

משתמשים ב-repeatable כדי לחזור על פעולה מספר מסוים של פעמים.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // yoimationQuickGuide.kt

הפעלת אנימציה בהפעלה של רכיב Composable

הפונקציה LaunchedEffect פועלת כשמכניסים רכיב קומפוזבילי לקומפוזיציה. היא מפעילה אנימציה בהפעלה של פונקציה קומפוזבילית, ואפשר להשתמש בה כדי להניע את שינוי המצב של האנימציה. שימוש ב-Animatable עם ה-method‏ animateTo כדי להפעיל את האנימציה בזמן הטעינה:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

יצירת אנימציות ברצף

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

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

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tweckGuide.kt

יצירת אנימציות בו-זמניות

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

כדי ליצור אנימציות בו-זמניות, משתמשים בממשקי ה-API של פונקציות ה-coroutine‏ (Animatable#animateTo() או animate) או ב-API‏ Transition. אם משתמשים בכמה פונקציות הפעלה בהקשר של שגרת המשך (coroutine), האנימציות מופעלות בו-זמנית:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100ckGuide.kt

אתם יכולים להשתמש ב-API של updateTransition כדי להשתמש באותו מצב כדי להפעיל הרבה אנימציות שונות של מאפיינים בו-זמנית. בדוגמה הבאה, שני מאפיינים מונפשים באמצעות שינוי מצב, rect ו-borderWidth:

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

val rect by transition.animateRect(label = "r>ect") { state -
    when (state) {
        >BoxState.Collapsed - Rect(0f, 0f, 100f, 100f)
       > BoxState.Expanded - Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = &quo>t;borderWidth") { state -
    when (state) >{
        BoxState.Collapsed - 1.>dp
        Bo
    }
}AnimationQuickGuide.kt

אופטימיזציה של ביצועי האנימציה

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

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

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

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

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

הנה סיכום של האפשרויות השונות של animationSpec:

  • ‫spring: אנימציה שמבוססת על פיזיקה, ברירת המחדל לכל האנימציות. אפשר לשנות את הערכים של stiffness או dampingRatio כדי לשנות את המראה והתחושה של האנימציה.
  • ‫tween (קיצור של between): אנימציה שמבוססת על משך זמן, יוצרת אנימציה בין שני ערכים באמצעות פונקציית Easing.
  • ‫keyframes: מפרט להגדרת ערכים בנקודות מפתח מסוימות באנימציה.
  • ‫repeatable: מפרט מבוסס-משך שפועל מספר מסוים של פעמים, שצוין על ידי RepeatMode.
  • ‫infiniteRepeatable: מפרט מבוסס-משך שפועל ללא הגבלת זמן.
  • ‫snap: מעבר מיידי לערך הסופי בלי אנימציה.
שתי אנימציות שמדגימות את ההבדל בין מצב שבו לא הוגדר מפרט לבין מצב שבו הוגדר מפרט קפיץ בהתאמה אישית.
תרשים 16. No spec set versus Custom Spring spec set

למידע נוסף על animationSpecs, קראו את המסמכים המלאים.

מקורות מידע נוספים

דוגמאות נוספות לאנימציות מהנות ב-Compose: