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

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

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

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

הצגת אנימציה / נעלמת

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

אפשר להשתמש ב-AnimatedVisibility כדי להסתיר או להציג תוכן קומפוזבילי. ילדים בתוך 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
    // ...
}

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

אפשרות נוספת לאנימציה של הניראות של תוכן קומפוזבילי היא להוסיף אנימציה alpha לאורך זמן באמצעות 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.TopCenter)
) {
}

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

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

אנימציית צבע רקע

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

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

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

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

אנימציה של הגודל של רכיב מורכב

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

ב-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. תנועה מורכבת לפי אופסט

כדי להנפיש את המיקום של רכיב מורכב, משתמשים ב-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
        }
)

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

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

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

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

כדי ליצור אנימציה של הרווח של רכיב מורכב, משתמשים ב-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
        }
)

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

איור 8. אנימציה של הגובה של Composable בלחיצה

כדי להוסיף אנימציה להעלאה של תוכן קומפוזבילי, צריך להשתמש ב-animateDpAsState בשילוב עם Modifier.graphicsLayer{ }. לשינויי גובה חד-פעמיים, משתמשים ב-Modifier.shadow(). אם אתם יוצרים אנימציה של הצל, השימוש במשתנה 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(colorGreen)
) {
}

לחלופין, אפשר להשתמש ברכיב ה-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 = TextMotion.Animated)
    )
}

אנימציה של צבע הטקסט

המילים
איור 10. דוגמה להנפשה של צבע הטקסט

כדי ליצור אנימציה של צבע הטקסט, משתמשים ב-lambda ‏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 = {
        animatedColor
    },
    // ...
)

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

מסך ירוק אומר
איור 11. שימוש ב-AnimatedContent כדי ליצור אנימציה של שינויים בין רכיבים שונים של רכיבים ניתנים לשילוב (בהילוך איטי)

משתמשים ב-AnimatedContent כדי להוסיף אנימציה למעבר בין רכיבים שונים של Composables. אם רוצים רק מעבר סטנדרטי בין רכיבים של Composables, משתמשים ב-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"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

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

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

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

כדי להוסיף אנימציה למעברים בין רכיבי composable כשמשתמשים באובייקט navigation-compose, צריך לציין את הערכים enterTransition ו-exitTransition ברכיב composable. אפשר גם להגדיר את אנימציית ברירת המחדל שתהיה רלוונטית לכל היעדים ברמה העליונה 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 ->
        ScreenDetails(
            // ...
        )
    }
}

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

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

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

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

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

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)
    }
) {
    // your composable here
}

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

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

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

יצירת אנימציות רצופות

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

שימוש בממשקי ה-API של coroutine 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 = tween(100))
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

בהמשך מופיע סיכום של האפשרויות השונות של animationSpec:

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

מידע נוסף על animationSpecs זמין במסמכי התיעוד המלאים.

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

דוגמאות נוספות לאנימציות משעשעות חדשניות ב'כתיבה':