אנימציות מבוססות-ערך

הוספת אנימציה של ערך יחיד באמצעות animate*AsState

הפונקציות animate*AsState הן ממשקי ה-API הפשוטים ביותר לאנימציה ב-Composer עבור ליצירת אנימציה של ערך יחיד. אתם מציינים רק את ערך היעד (או ערך הסיום), וגם ה-API מתחיל אנימציה מהערך הנוכחי לערך שצוין.

בהמשך מוצגת דוגמה לאנימציה של גרסת אלפא באמצעות ה-API הזה. פשוט על ידי האריזה של ערך היעד ב-animateFloatAsState, ערך האלפא הוא עכשיו ערך אנימציה בין הערכים שצוינו (1f או 0.5f במקרה הזה).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

שימו לב שאתם לא צריכים ליצור מופע של כל מחלקה של אנימציה או כינוי נוסף. מתחת למכסה הכובע נמצא אובייקט אנימציה (כלומר, Animatable 'חלון ראווה') ייווצר ויישמר באתר ההתקשרות, כשהיעד הראשון הוא כערך הראשוני. מכאן ואילך, בכל פעם שתספקו את התוכן הקומפוזבילי הזה ערך יעד שונה, האנימציה תתחיל באופן אוטומטי בערך עם ערך מסוים. אם כבר קיימת אנימציה בטיסה, האנימציה תתחיל הערך הנוכחי (ומהירות) ואנימציה כלפי ערך היעד. במהלך אנימציה, התוכן הקומפוזבילי הזה עובר קומפוזביליות מחדש ומחזיר אנימציה מעודכנת ערך כל מסגרת.

התכונה 'כתיבה' מספקת פונקציות 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 = updateTransition(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),
    elevation = 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()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(Modifier.fillMaxSize().background(color))

ממשקי API של אנימציה ברמה נמוכה

כל ממשקי ה-API ברמה הכללית של אנימציה שהוזכרו בקטע הקודם מבוססים על הבסיס של ממשקי ה-API של אנימציה ברמה נמוכה.

הפונקציות animate*AsState הן ממשקי ה-API הפשוטים ביותר, כלומר כערך אנימציה. הוא מגובה על ידי Animatable, API מבוסס-coroutine להנפשה של ערך יחיד. updateTransition יוצר אובייקט מעבר שיכול לנהל ערכים מרובים של אנימציה ולהריץ אותם על סמך על שינוי במדינה. rememberInfiniteTransition דומה, אבל הוא יוצר מעבר אינסופי שיכול לנהל מספר אנימציות שממשיכות לפעול ללא הגבלת זמן. כל ממשקי ה-API האלה הם תכנים קומפוזביליים, מלבד Animatable, המשמעות היא שניתן ליצור את האנימציות האלה בלי הרכבה.

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

תרשים שמציג את הקשר בין ממשקי ה-API השונים של אנימציה ברמה נמוכה

Animatable: אנימציה של ערך יחיד שמבוסס על קורוטין

Animatable הוא שומר ערך שיכול להוסיף אנימציה לערך כשהוא משתנה דרך animateTo זהו ה-API שמגבה את ההטמעה של animate*AsState. הוא מבטיח המשך עקבי ובלעדיות הדדית, כלומר שינוי הערך הוא תמיד מתמשך וכל אנימציה מתמשכת תבוטל.

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

זוהי הטמעת האנימציה שמגבה את ה-API של animate*AsState שהוזכרו בקטע הקודם. בהשוואה ל-animate*AsState, נעשה שימוש השירות Animatable נותן לנו שליטה פרטנית יותר מכמה היבטים. קודם כל, אפשר להגדיר ל-Animatable ערך ראשוני שונה מערך היעד הראשון. לדוגמה, הקוד לדוגמה שלמעלה מציג תחילה תיבה אפורה, שמיד לאחר מכן מתחילה אנימציה לירוק או לאדום. שנית, Animatable מספק יותר פעולות על ערך התוכן, snapTo ו-animateDecay. snapTo מגדירה את הערך הנוכחי לערך היעד באופן מיידי. זה שימושי כאשר האנימציה עצמה היא לא מקור האמת היחיד וצריך לסנכרן אותה עם מצבי מגע, כמו אירועי מגע. animateDecay מפעיל אנימציה שנמצאת בהילוך איטי מהמהירות הנתונה. האפשרות הזו שימושית להטמעת התנהגות של החלקה. צפייה תנועה ואנימציה לקבלת מידע נוסף.

מחוץ למסגרת, Animatable תומך ב-Float וב-Color, אבל כל סוג נתונים יכול משמש לציון TwoWayConverter. צפייה למידע נוסף, AnimationVector.

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

Animation: אנימציה בשליטה ידנית

Animation הוא ה-API של אנימציה ברמה הנמוכה ביותר. הרבה מהאנימציות שראינו עד עכשיו פיתוחים מעל אנימציה. יש שני סוגי משנה של 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 { mutableStateOf(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 והוא מואט עם הזמן.