התאמה אישית של מעבר רכיב משותף

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

מפרט האנימציה

כדי לשנות את הגדרת האנימציה שמשמשת לתנועת הגודל והמיקום, אפשר לציין פרמטר boundsTransform שונה ב-Modifier.sharedElement(). המיקום ההתחלתי Rect והמיקום של היעד Rect.

לדוגמה, כדי שהטקסט בדוגמה הקודמת יזוז בתנועת קשת, מציינים את הפרמטר boundsTransform כך שישתמש במפרט keyframes:

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

אפשר להשתמש בכל AnimationSpec. בדוגמה הזו נעשה שימוש במפרט keyframes.

איור 1. דוגמה שמציגה פרמטרים שונים של boundsTransform

מצב שינוי גודל

כשמנפישים מעבר בין שני גבולות משותפים, אפשר להגדיר את הפרמטר resizeMode לערך RemeasureToBounds או ScaleToBounds. הפרמטר הזה קובע את אופן המעבר של האלמנט המשותף בין שני המצבים. ‫ScaleToBounds first מודד את פריסת רכיב הצאצא עם האילוצים של התצוגה המקדימה (או היעד). לאחר מכן, הפריסה היציבה של רכיב הצאצא מותאמת כך שתתאים לגבולות המשותפים. אפשר לחשוב על ScaleToBounds כעל "סולם גרפי" בין המצבים.

לעומת זאת, RemeasureToBounds מודד מחדש את פריסת הרכיב הצאצא של sharedBounds ומסדר אותו מחדש עם אילוצים קבועים מונפשים על סמך גודל היעד. המדידה מחדש מופעלת על ידי שינוי בגודל הגבולות, שיכול להיות בכל פריים.

לרכיבי Text composable, מומלץ להשתמש ב-ScaleToBounds, כי הוא מונע פריסה מחדש וסידור מחדש של הטקסט בשורות שונות. מומלץ להשתמש ב-RemeasureToBounds לגבולות עם יחסי גובה-רוחב שונים, ואם רוצים ליצור מעבר חלק בין שני הרכיבים המשותפים.

ההבדל בין שני מצבי שינוי הגודל מודגם בדוגמאות הבאות:

ScaleToBounds

RemeasureToBounds

הפעלה והשבתה דינמיות של אלמנטים משותפים

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

כדי לקבוע אם מעבר הרכיב המשותף יתרחש, אפשר להתאים אישית את SharedContentConfig שמועבר אל rememberSharedContentState(). המאפיין isEnabled קובע אם הרכיב המשותף פעיל.

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

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

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

דילוג לפריסה הסופית

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

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

ללא Modifier.skipToLookaheadSize() – שימו לב לטקסט 'לורם איפסום' שמוצג מחדש

Modifier.skipToLookaheadSize() – שימו לב שהטקסט 'לורם איפסום' נשאר במצב הסופי שלו בתחילת האנימציה

קליפים ושכבות-על

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

הוא יוצג מעל רכיבים אחרים בממשק המשתמש שלא משותפים. בסיום המעבר, הרכיב יוסר מהשכבה העליונה ויוצג בחלון DrawScope משלו.

כדי לחתוך אלמנט משותף לצורה, משתמשים בפונקציה הרגילה Modifier.clip(). ממקמים אותו אחרי sharedElement():

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

אם אתם רוצים לוודא שרכיב משותף אף פעם לא יוצג מחוץ למאגר האב, אתם יכולים להגדיר את clipInOverlayDuringTransition ב-sharedElement(). כברירת מחדל, כשמשתמשים ב-clipInOverlayDuringTransition עם גבולות משותפים מקוננים, הוא משתמש בנתיב הגזירה מהרכיב ההורה sharedBounds().

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

לדוגמה, באפליקציית Jetsnack, צריך למקם את BottomAppBar מעל הרכיב המשותף עד שהמסך לא יהיה גלוי. הוספת ה-modifier לרכיב הקומפוזבילי שומרת על ההדגשה שלו.

בלי Modifier.renderInSharedTransitionScopeOverlay()

עם Modifier.renderInSharedTransitionScopeOverlay()

יכול להיות שתרצו שהרכיב הניתן להרכבה שלא משותף יונפש החוצה וגם יישאר מעל שאר הרכיבים הניתנים להרכבה לפני המעבר. במקרים כאלה, משתמשים ב-renderInSharedTransitionScopeOverlay().animateEnterExit() כדי להנפיש את הרכיב הניתן להרכבה החוצה בזמן שמתבצעת ההנפשה של הרכיב המשותף:

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

איור 2. סרגל האפליקציות התחתון מחליק פנימה והחוצה במהלך המעבר בין האנימציות.

במקרים נדירים שבהם רוצים שהרכיב המשותף לא יוצג בשכבת-על, אפשר להגדיר את renderInOverlayDuringTransition ב-sharedElement() כ-false.

הודעה על שינויים בגודל של רכיב משותף בפריסות מקבילות

כברירת מחדל, sharedBounds() ו-sharedElement() לא מודיעים למאגר התגים ברמת ההורה על שינויים בגודל במהלך המעבר בין הפריסות.

כדי להעביר את שינויי הגודל למאגר האב במהלך המעבר, צריך לשנות את הפרמטר placeholderSize ל-PlaceholderSize.AnimatedSize. הפעולה הזו גורמת לפריט לגדול או לקטון. כל שאר הפריטים בפריסה מגיבים לשינוי.

PlaceholderSize.ContentSize (ברירת מחדל)

PlaceholderSize.AnimatedSize

(שימו לב איך הפריטים האחרים ברשימה זזים למטה בתגובה לגדילה של הפריט)