במצב 'פיתוח נייטיב' יש הרבה מנגנוני אנימציה מובנים, ולא תמיד קל לדעת באיזה מהם לבחור. בהמשך מפורטים תרחישים נפוצים לדוגמה לשימוש באנימציה. למידע מפורט יותר על כל אפשרויות ה-API הזמינות, קראו את מסמכי התיעוד המלאים של Compose Animation.
אנימציה של מאפיינים נפוצים שניתנים ליצירה
Compose מספק ממשקי API נוחים שמאפשרים לכם לפתור הרבה תרחישים נפוצים לדוגמה של אנימציה. בקטע הזה נסביר איך להוסיף אנימציה למאפיינים נפוצים של רכיב מורכב.
אנימציה של הופעה או היעלמות
משתמשים ב-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 // ... }
הפרמטרים של הכניסה והיציאה של AnimatedVisibility
מאפשרים להגדיר את ההתנהגות של תוכן קומפוזיציה כשהוא מופיע ומת消. מידע נוסף זמין במסמכי העזר המלאים.
אפשרות נוספת להאנימציה של החשיפה של רכיב מורכב היא להאנימציה של הערך של אלפא לאורך זמן באמצעות 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
מסיר את הפריט מההרכב בסופו של דבר.
אנימציית צבע רקע
val animatedColor by animateColorAsState( if (animateBackgroundColor) colorGreen else colorBlue, label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(animatedColor) } ) { // your composable here }
הביצועים של האפשרות הזו טובים יותר מאשר השימוש ב-Modifier.background()
.
אפשר להשתמש ב-Modifier.background()
להגדרת צבע ברמה אחת, אבל כשמוסיפים אנימציה של צבע לאורך זמן, יכול להיות שיהיו יותר הרכבות מחדש מהנדרש.
כדי להוסיף אנימציה אינסופית של צבע הרקע, אפשר לעיין במאמר חזרה על קטע אנימציה.
הוספת אנימציה לגודל של תוכן קומפוזבילי
ב-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
כדי לתאר איך השינויים בגודל צריכים להתרחש.
אנימציה של המיקום של רכיב ה-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 } )
אם אתם רוצים לוודא שתכנים קומפוזביליים לא ייכתבו מעל או מתחת לתכנים קומפוזביליים אחרים באנימציה של מיקום או גודל, צריך להשתמש ב-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) ) }
אנימציה של הרווח של רכיב מורכב
כדי להוסיף אנימציה למרווח הפנימי של תוכן קומפוזבילי, צריך להשתמש בפונקציה 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 } )
אנימציה של הגבהה של רכיב מורכב
כדי להוסיף אנימציה להעלאה של תוכן קומפוזבילי, צריך להשתמש ב-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
ולהגדיר את נכס הגובה לערכי שונים לכל מצב.
הוספת אנימציה לשינוי קנה המידה, התרגום או הסיבוב של הטקסט
כשמפעילים אנימציה של שינוי קנה המידה, התרגום או הסיבוב של הטקסט, מגדירים את הפרמטר 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) ) }
אנימציה של צבע הטקסט
כדי ליצור אנימציה של צבע הטקסט, משתמשים ב-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 }, // ... )
מעבר בין סוגי תוכן שונים
משתמשים ב-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
כדי להציג סוגים רבים ושונים של מעברים לכניסה וליציאה. למידע נוסף, אפשר לקרוא את מסמכי התיעוד בנושא AnimatedContent
או לקרוא את הפוסט הזה בבלוג בנושא
AnimatedContent
.
אנימציה בזמן ניווט ליעדים שונים
כדי להוסיף אנימציה למעברים בין תכנים קומפוזביליים כשמשתמשים בארטיפקט 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 -> ScreenDetails( // ... ) } }
יש סוגים רבים של מעברים בין כניסה ויציאה שמחילים אפקטים שונים על התוכן הנכנס והיוצא. במסמכי העזרה תוכלו לקרוא מידע נוסף.
חזרה על אנימציה
אפשר להשתמש ב-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 } )
יצירת אנימציות רצופות
שימוש בממשקי ה-API של Animatable
coroutine כדי לבצע אנימציות רצופות או בו-זמניות. קריאה ל-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)) }
יצירת אנימציות בו-זמנית
כדי ליצור אנימציות בו-זמנית, אפשר להשתמש בממשקי ה-API של קורוטינים (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{ }
, כי המשתנה הזה תמיד פועל בשלב הציור. מידע נוסף זמין בקטע דחיית קריאות במסמכי העזרה בנושא ביצועים.
שינוי התזמון של האנימציה
כברירת מחדל, ב-Compose נעשה שימוש באנימציות קפיצה ברוב האנימציות. אנימציות של קפיצים או אנימציות שמבוססות על פיזיקה נראות טבעיות יותר. הם גם ניתנים להפרעה, כי הם מביאים בחשבון את המהירות הנוכחית של האובייקט במקום זמן קבוע.
אם רוצים לשנות את ברירת המחדל, אפשר להשתמש ב-animationSpec
בכל ממשקי ה-API של האנימציה שצוינו למעלה כדי להתאים אישית את אופן ההפעלה של האנימציה, למשל, להגדיר שהיא תפעל למשך זמן מסוים או שתהיה יותר קופצנית.
בהמשך מופיע סיכום של האפשרויות השונות של animationSpec
:
spring
: אנימציה מבוססת-פיזיקה, ברירת המחדל לכל האנימציות. אפשר לשנות את הערך של stiffness או dampingRatio כדי לקבל מראה ותחושה שונים של האנימציה.tween
(קיצור של between): אנימציה שמבוססת על משך זמן, שמציגה אנימציה בין שני ערכים באמצעות פונקצייתEasing
.keyframes
: מפרט לציון ערכים בנקודות מפתח מסוימות באנימציה.repeatable
: מפרט שמבוסס על משך זמן, שפועל מספר מסוים של פעמים, כפי שמצוין ב-RepeatMode
.infiniteRepeatable
: מפרט מבוסס-משך זמן שפועל לנצח.snap
: הצמדה מיידית לערך הסופי ללא אנימציה.
מידע נוסף על animationSpecs זמין במסמכי התיעוד המלאים.
מקורות מידע נוספים
דוגמאות נוספות לאנימציות משעשעות חדשניות ב'כתיבה':
- 5 אנימציות מהירות ב-Compose
- איך גורמים ל-Jellyfish לזוז ב-Compose
- התאמה אישית של
AnimatedContent
ב-Compose - מתחילים להשתמש בפונקציות Easing ב-Compose