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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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