אנימציה של ערך יחיד באמצעות animate*AsState
הפונקציות animate*AsState
הן ממשקי ה-API הפשוטים ביותר ליצירת אנימציה ב-Compose, שמאפשרים ליצור אנימציה של ערך יחיד. אתם מספקים רק את ערך היעד (או ערך הסיום), וה-API מתחיל את האנימציה מהערך הנוכחי לערך שצוין.
בהמשך מופיעה דוגמה ליצירת אנימציה של אלפא באמצעות ה-API הזה. פשוט עוטפים את ערך היעד ב-animateFloatAsState
, וערך האלפא הוא עכשיו ערך אנימציה בין הערכים שסופקו (1f
או 0.5f
במקרה הזה).
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( Modifier .fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
שימו לב שאתם לא צריכים ליצור מופע של שיעור אנימציה כלשהו, או לטפל בהפרעות. בחלק העליון, המערכת תיצור אובייקט אנימציה (כלומר, מכונה של Animatable
) ויישמר באתר הקריאה, וערך היעד הראשון יהיה הערך הראשוני שלו. לאחר מכן, בכל פעם שתספקו את התוכן הקומפוזבילי
של ערך יעד שונה, האנימציה תתחיל באופן אוטומטי בערך הזה. אם כבר קיימת אנימציה בטיסה, האנימציה מתחילה מהערך הנוכחי (והמהירות) שלה וממשיכה לכיוון ערך היעד. במהלך האנימציה, ה-Composable הזה עובר קומפוזיציה מחדש ומחזיר ערך אנימציה מעודכן בכל פריים.
מעכשיו, ההצעות לכתיבה מספקות פונקציות animate*AsState
ל-Float
, ל-Color
, ל-Dp
, ל-Size
, ל-Offset
, ל-Rect
, ל-Int
, ל-IntOffset
ול-IntSize
. כדי להוסיף תמיכה בסוגי נתונים אחרים, אפשר לספק ל-animateValueAsState
את הערך TwoWayConverter
שמקבל סוג כללי.
אם רוצים להתאים אישית את מפרטי האנימציה, אפשר לספק 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
. לדוגמה, הוא מאפשר לנו להתחיל את האנימציה ברגע שהקוד נכנס ל-composition.
// 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
, והוא מפעיל מעברים של כניסה/יציאה לפי הצורך כשהערך של targetState
ב-Transition
משתנה. פונקציות התוספים האלה מאפשרות להעביר את כל האנימציות של הכניסה/היציאה/השינוי בגודל, שהיו אמורות להיות פנימיות ב-AnimatedVisibility
/AnimatedContent
, אל Transition
.
באמצעות פונקציות התוסף האלה, אפשר לראות מבחוץ את השינוי במצב של AnimatedVisibility
/AnimatedContent
. במקום פרמטר visible
בוליאני, בגרסה הזו של AnimatedVisibility
מופיע פונקציית lambda שממירה את מצב היעד של המעבר ההורה לבוליאני.
פרטים נוספים זמינים במאמרים 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 האלה מבוססים על ממשק ה-API הבסיסי יותר Animation
. רוב האפליקציות לא יתקשרו ישירות עם Animation
, אבל חלק מיכולות ההתאמה האישית של Animation
זמינות דרך ממשקי API ברמה גבוהה יותר. מידע נוסף על AnimationVector
ו-AnimationSpec
זמין במאמר התאמה אישית של אנימציות.
Animatable
: אנימציה של ערך יחיד שמבוססת על שגרת המשך (coroutine)
Animatable
הוא מאגר ערכים שאפשר להוסיף לו אנימציה כשהערך משתנה באמצעות animateTo
. זהו ממשק ה-API שתומך בהטמעה של animate*AsState
.
הוא מבטיח המשכיות עקבית ויחס הדדי של החרגה, כלומר שינוי הערך תמיד מתבצע באופן רציף וכל אנימציה מתמשכת מבוטלת.
תכונות רבות של Animatable
, כולל animateTo
, הן פונקציות השהיה. כלומר, צריך לעטוף אותם בהיקף מתאים של קורוטין. לדוגמה, אפשר להשתמש ב-composable LaunchedEffect
כדי ליצור היקף רק למשך משך הזמן של ערך המפתח שצוין.
// 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
. כל שינוי שיתבצע בערך הבוליאני יתחיל את האנימציה לצבע האחר. אם יש אנימציה מתמשכת כשהערך משתנה, האנימציה מתבטלת והאנימציה החדשה מתחילה מהערך הנוכחי של קובץ ה-snapshot במהירות הנוכחית.
זוהי הטמעת האנימציה שתומכת ב-API animate*AsState
שצוין בקטע הקודם. בהשוואה ל-animate*AsState
, השימוש ב-Animatable
נותן לנו שליטה פרטנית יותר מכמה היבטים. ראשית, ל-Animatable
יכול להיות ערך ראשוני ששונה מערך היעד הראשון שלו.
לדוגמה, בדוגמת הקוד שלמעלה מוצגת בהתחלה תיבה אפורה, שמתחילה מיד לזוז בירוק או באדום. שנית, Animatable
מספק יותר פעולות על ערך התוכן, כלומר snapTo
ו-animateDecay
. snapTo
מגדיר את הערך הנוכחי לערך היעד באופן מיידי. האפשרות הזו שימושית כשהאנימציה עצמה היא לא המקור היחיד לאמת, ויש לסנכרן אותה עם מצבים אחרים, כמו אירועי מגע. animateDecay
מתחיל אנימציה שמאטה מהמהירות שצוינה. האפשרות הזו שימושית להטמעת התנהגות של תנועה מהירה. מידע נוסף זמין במאמר תנועות ואנימציות.
כברירת מחדל, Animatable
תומך ב-Float
וב-Color
, אבל אפשר להשתמש בכל סוג נתונים על ידי הצגת TwoWayConverter
. מידע נוסף זמין במאמר AnimationVector.
כדי להתאים אישית את מפרטי האנימציה, אפשר לספק את הפרמטר AnimationSpec
.
מידע נוסף זמין במאמר AnimationSpec.
Animation
: אנימציה בשליטה ידנית
Animation
הוא רמת Animation API הנמוכה ביותר שזמינה. רבות מהאנימציות שראינו עד עכשיו מבוססות על Animation. יש שני סוגי משנה של Animation
: TargetBasedAnimation
ו-DecayAnimation
.
יש להשתמש ב-Animation
רק כדי לשלוט באופן ידני בזמן של האנימציה.
Animation
הוא ללא שמירת מצב, ואין בו מושג של מחזור חיים. הוא משמש כמנוע חישוב של אנימציה שמשמש את ממשקי ה-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
, לא צריך לספק targetValue
כשמשתמשים ב-DecayAnimation
. במקום זאת, הוא מחשב את הערך של targetValue
על סמך תנאי ההתחלה שמוגדרים על ידי initialVelocity
ו-initialValue
ועל סמך הערך של DecayAnimationSpec
שסופק.
אנימציות דעיכה משמשות לעיתים קרובות אחרי תנועת משיכה כדי להאט את הרכיבים עד שהם נעצרים. מהירות האנימציה מתחילה בערך שמוגדר על ידי initialVelocityVector
ומאטה עם הזמן.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- התאמה אישית של אנימציות {:#customize-animations}
- אנימציות ב-Compose
- רכיבי אנימציה ורכיבים שניתנים לשילוב