בדף הזה מוסבר איך ליצור אנימציות מבוססות-ערך ב-Jetpack Compose, עם דגש על ממשקי API שמנפישים ערכים על סמך המצבים הנוכחיים והמצבים שהוגדרו כיעד.
יצירת אנימציה של ערך יחיד באמצעות 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 כדי ליצור מעבר צאצא. הטכניקה הזו שימושית להפרדה בין בעיות בכמה רכיבי משנה בממשק מורכב שניתן להרכבה. מעבר האב מודע לכל ערכי האנימציה במעברי הבן.
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, ומפעיל אנימציות של כניסה, יציאה ו-sizeTransform לפי הצורך כש-targetState של Transition משתנה. פונקציות התוסף האלה מאפשרות להעביר את כל האנימציות של הכניסה, היציאה ו-sizeTransform, שאחרת היו פנימיות ל-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, אפשר לגשת לחלק מהיכולות שלו להתאמה אישית באמצעות ממשקי API ברמה גבוהה יותר. מידע נוסף על AnimationVector ועל AnimationSpec מופיע במאמר התאמה אישית של אנימציות.
Animatable: אנימציה של ערך יחיד שמבוססת על קורוטינה
Animatable הוא placeholder לערך שאפשר להנפיש אותו בזמן שהוא משתנה באמצעות animateTo. זהו ה-API שמשמש לגיבוי ההטמעה של animate*AsState. הוא מבטיח המשכיות עקבית ובלעדיות הדדית, כלומר שינוי הערך תמיד רציף ו-Compose מבטל כל אנימציה שמתבצעת.
הרבה תכונות של Animatable, כולל animateTo, הן פונקציות השעיה.
כלומר, צריך להוסיף אותן ל-CoroutineScope מתאים. לדוגמה,
אפשר להשתמש ב-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. כל שינוי עוקב בערך הבוליאני יתחיל אנימציה לצבע השני.
אם אנימציה מתבצעת בזמן שהערך משתנה, Compose מבטל את האנימציה והאנימציה החדשה מתחילה מערך התמונה הנוכחי עם המהירות הנוכחית.
Animatable 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, ב-DecayAnimation לא צריך לספק targetValue. במקום זאת, המערכת מחשבת את targetValue על סמך תנאי ההתחלה שהוגדרו על ידי initialVelocity ו-initialValue ועל סמך DecayAnimationSpec שסופק.
אנימציות של דעיכה משמשות לעיתים קרובות אחרי תנועת הדיפדוף כדי להאט את הרכיבים עד לעצירה. מהירות האנימציה מתחילה בערך שמוגדר על ידי initialVelocityVector
ומואטת לאורך זמן.
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- התאמה אישית של אנימציות
- אנימציות ב-Compose
- אנימציה של משנים ורכיבים שאפשר להשתמש בהם שוב