ל-Compose יש הרבה מנגנוני אנימציה מובנים, ויכול להיות שיהיה קשה לדעת איזה מהם לבחור. בהמשך מפורטים תרחישים נפוצים לדוגמה לשימוש באנימציה. למידע מפורט יותר על כל אפשרויות ה-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 } )
אם רוצים לוודא שרכיבי composable לא מצוירים מעל או מתחת לרכיבי composable אחרים כשמפעילים אנימציה של המיקום או הגודל, משתמשים ב-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
.
אנימציה בזמן ניווט ליעדים שונים
כדי להוסיף אנימציה למעברים בין רכיבי 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( // ... ) } }
יש סוגים רבים של מעברים לכניסה וליציאה שמחילים אפקטים שונים על התוכן הנכנס והיוצא. מידע נוסף זמין במסמכי העזרה.
איך חוזרים על אנימציה
כדי לחזור על האנימציה באופן קבוע, משתמשים ב-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 עלולות לגרום לבעיות בביצועים. הסיבה לכך היא מהות האנימציה: תנועה או שינוי של פיקסלים במסך במהירות, פריים אחרי פריים, כדי ליצור את האשליה של תנועה.
כדאי להכיר את השלבים השונים של Compose: קומפוזיציה, פריסה ורישום. אם אנימציה משנה את שלב הפריסה, כל הרכיבים המורכבים שמושפעים ממנה צריכים לעבור פריסה מחדש ורישום מחדש. אם האנימציה מתרחשת בשלב הציור, הביצועים שלה טובים יותר כברירת מחדל מאשר אם מריצים את האנימציה בשלב הפריסה, כי יש לה פחות עבודה לעשות באופן כללי.
כדי להבטיח שהאפליקציה תבצע כמה שפחות פעולות במהלך האנימציה, כדאי לבחור בגרסה של lambda של Modifier
כשהדבר אפשרי. הפעולה הזו מדלגת על הרכבת מחדש ומבצעת את האנימציה מחוץ לשלב הרכבת התמונה. אחרת, צריך להשתמש ב-Modifier.graphicsLayer{ }
, כי המשתנה הזה תמיד פועל בשלב הציור. מידע נוסף זמין בקטע דחיית קריאות במסמכי העזרה בנושא ביצועים.
שינוי התזמון של האנימציה
כברירת מחדל, ב-Compose נעשה שימוש באנימציות spring ברוב האנימציות. אנימציות של קפיצים או אנימציות שמבוססות על פיזיקה נראות טבעיות יותר. הם גם ניתנים להפרעה, כי הם מביאים בחשבון את המהירות הנוכחית של האובייקט במקום זמן קבוע.
אם רוצים לשנות את ברירת המחדל, אפשר להשתמש ב-animationSpec
בכל ממשקי ה-API של האנימציה שמוצגים למעלה כדי להתאים אישית את אופן ההפעלה של האנימציה, למשל, להגדיר שהיא תפעל למשך זמן מסוים או שתהיה יותר קופצנית.
בהמשך מופיע סיכום של האפשרויות השונות של animationSpec
:
spring
: אנימציה מבוססת-פיזיקה, ברירת המחדל לכל האנימציות. אפשר לשנות את הערך של stiffness או dampingRatio כדי לקבל מראה ותחושה שונים של האנימציה.tween
(קיצור של between): אנימציה שמבוססת על משך זמן, שמציגה אנימציה בין שני ערכים באמצעות פונקצייתEasing
.keyframes
: מפרט לציון ערכים בנקודות מפתח מסוימות באנימציה.repeatable
: מפרט שמבוסס על משך זמן, שפועל מספר מסוים של פעמים, כפי שמצוין ב-RepeatMode
.infiniteRepeatable
: מפרט שמבוסס על משך זמן ופועל לנצח.snap
: האנימציה עוברת מיידית לערך הסופי ללא אנימציה.
מידע נוסף על animationSpecs זמין במסמכי התיעוד המלאים.
מקורות מידע נוספים
דוגמאות נוספות לאנימציות מהנות ב-Compose:
- 5 אנימציות מהירות ב-Compose
- איך גורמים ל-Jellyfish לזוז ב-Compose
- התאמה אישית של
AnimatedContent
ב-Compose - מתחילים להשתמש בפונקציות Easing ב-Compose