ניווט בין מקטעים באמצעות אנימציות

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

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

  • אפקט Enter קובע את האופן שבו מקטע נכנס למסך. לדוגמה, אפשר ליצור אפקט כדי להחליק את המקטע כלפי מטה מהקצה של כשמנווטים אליו.
  • אפקט יציאה קובע איך מקטע יוצא מהמסך. לדוגמה, אפשר ליצור אפקט עמעום הדרגתי של הפריים כשיוצאים ממנו ממנו.
  • מעבר של רכיבים משותפים קובע את האופן שבו תצוגה מפורטת משותפת שני מקטעים עובר ביניהם. לדוגמה, תמונה שמוצגת בImageView במקטע A עובר למקטע B פעם B הופך לגלוי.

לראות אנימציות.

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

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

אפשר להגדיר את האנימציות האלה בספרייה res/anim:

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

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

אנימציות של PopEnter ו-pop Exit. המקטע הנוכחי מחליק מושבת
            המסך מימין בזמן שהקטע הקודם מתעמעם.
איור 2. popEnter והקבוצה popExit אנימציות. המקטע הנוכחי מחליק אל מחוץ למסך ימינה בזמן שהקטע הקודם מתעמעם.

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

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

אחרי שמגדירים את האנימציות, אפשר להפעיל אותן FragmentTransaction.setCustomAnimations() להעביר את משאבי האנימציה לפי מזהה המשאב שלהם, כפי שמוצג בדוגמה הבאה:

Kotlin

supportFragmentManager.commit {
    setCustomAnimations(
        R.anim.slide_in, // enter
        R.anim.fade_out, // exit
        R.anim.fade_in, // popEnter
        R.anim.slide_out // popExit
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

הגדרת מעברים

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

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

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

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

תמיכה ב-Fragments מעברים ל-AndroidX. למרות שיש תמיכה גם במקטעים מעברים בין מסגרות, מומלץ להשתמש במעברים של AndroidX, כי הם נתמכים ברמות API 14 והם מכילים תיקוני באגים שלא קיימים בגרסאות ישנות יותר של מעברים בין מסגרות.

שימוש במעברים משותפים של רכיבים

חלק ממסגרת המעבר, מעברים בין רכיבים משותפים קובעים את האופן שבו התצוגות התואמות עוברות שני מקטעים במהלך מעבר בין מקטעים. לדוגמה, ייתכן שתרצו התמונה שמוצגת בImageView בקטע A כדי לעבור למקטע B ברגע ש-B הופך לגלוי, כפי שמוצג באיור 3.

מעבר בין מקטעים עם רכיב משותף.
איור 3. מעבר בין מקטעים עם רכיב משותף.

ברמה הכללית, כך מבצעים מעבר של מקטעים באמצעות רכיבים משותפים:

  1. הקצאת שם מעבר ייחודי לכל תצוגת רכיב משותף.
  2. להוסיף תצוגות משותפות לרכיבים ולהעביר שמות FragmentTransaction
  3. הגדרת אנימציה משותפת של מעבר רכיבים.

ראשית, יש להקצות שם מעבר ייחודי לכל תצוגת רכיבים משותפת כדי לאפשר מיפוי של התצוגות ממקטע אחד למקטע הבא. הגדרה את שם המעבר ברכיבים משותפים בכל פריסת מקטעים באמצעות ViewCompat.setTransitionName() שמספק תאימות לרמות API 14 ומעלה. דוגמה, שם המעבר של ImageView במקטעים A ו-B אפשר להקצות את הערכים האלה:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, item_image)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, hero_image)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, item_image);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, hero_image);
    }
}

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

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, hero_image)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, hero_image)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

כדי לציין את אופן המעבר של הרכיבים המשותפים מקטע אחד לאחר, יש להגדיר מעבר Enter בקטע להיות עברת אל. שיחת טלפון Fragment.setSharedElementEnterTransition() בשיטה onCreate() של המקטע, כפי שמוצג בדוגמה הבאה:

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

המעבר של shared_image מוגדר כך:

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

כל מחלקות המשנה של Transition נתמכות כמעברים משותפים של רכיבים. אם המיקום שאתם רוצים ליצור Transition בהתאמה אישית, יוצרים אנימציית מעבר בהתאמה אישית. changeImageTransform, שנעשה בו שימוש בדוגמה הקודמת, הוא אחד מהבאים של התרגומים שנוצרו מראש, שניתן להשתמש בהם. אפשר למצוא עוד Transition מחלקות המשנה בהפניה של ה-API עבור כיתה Transition.

כברירת מחדל, מעבר הכניסה לרכיב המשותף משמש גם בתור מעבר return עבור רכיבים משותפים. מעבר ההחזרה קובע את האופן שבו רכיבים משותפים חוזרים למקטע הקודם כאשר המקטע "העסקה "קופצת" מהמקבץ האחורי. אם ברצונך לציין של החזרת מוצרים קיימת, תוכלו לעשות זאת באמצעות Fragment.setSharedElementReturnTransition() בשיטה onCreate() של המקטע.

תאימות חזויה בחזרה

ניתן להשתמש בתכונת חיזוי חזרה עם הרבה אנימציות, אבל לא בכולן. כשמטמיעים 'חיזוי חזרה', חשוב להביא בחשבון את השיקולים הבאים:

  • ייבוא של Transitions 1.5.0 ואילך או Fragments 1.7.0 ואילך.
  • המחלקה ומחלקות המשנה של Animator וספריית המעבר של AndroidX הם נתמך.
  • אין תמיכה במחלקה Animation ובספרייה Transition של ה-framework.
  • אנימציות של מקטעים חזויים פועלות רק במכשירים עם Android 14 או גבוהה יותר.
  • setCustomAnimations, setEnterTransition, setExitTransition setReenterTransition, setReturnTransition, setSharedElementEnterTransition ו-setSharedElementReturnTransition הם נתמך באמצעות חיזוי חזרה.

מידע נוסף זמין במאמר הבא: הוספת תמיכה באנימציות אחורה בזמן

דחיית מעברים

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

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

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

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

כדי לדחות את המעבר למעבר, צריך להתקשר Fragment.postponeEnterTransition() בשיטה onViewCreated() של המקטע שהוזן:

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

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

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

במקרים כמו החיבור האיטי של המשתמש לאינטרנט, ייתכן צריכים שהמעבר שנדחה יתחיל לאחר פרק זמן מסוים מאשר להמתין עד שכל הנתונים נטענים. במצבים כאלה, אפשר להתקשר במקום זאת Fragment.postponeEnterTransition(long, TimeUnit) ב-method onViewCreated() של המקטע, מעביר את משך הזמן ואת יחידת הזמן. החיוב שנדחה יתחיל באופן אוטומטי ברגע הזמן שצוין חלף.

שימוש במעברים משותפים של רכיבים עם RecyclerView

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

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

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

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

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

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

מקורות מידע נוספים

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

דוגמיות

פוסטים בבלוגים