التنقّل بين الأجزاء باستخدام الرسوم المتحركة

توفر واجهة برمجة التطبيقات Fragment API طريقتين لاستخدام تأثيرات الحركة والتحويل لربط الأجزاء بصريًا أثناء التنقل. وأحد هذه الطرق هو إطار عمل الصور المتحركة، والذي يستخدم كلاً من Animation و Animator. والنوع الآخر هو إطار عمل الانتقال، الذي يتضمن انتقالات العناصر المشتركة.

يمكنك تحديد مؤثرات مخصّصة لإدخال الأجزاء والخروج منها ولانتقالات العناصر المشتركة بين الأجزاء.

  • يحدِّد التأثير Enter كيفية دخول الجزء إلى الشاشة. على سبيل المثال، يمكنك إنشاء تأثير لتمرير الجزء من حافة الشاشة عند الانتقال إليه.
  • يحدِّد تأثير الخروج كيفية خروج الجزء من الشاشة. على سبيل المثال، يمكنك إنشاء تأثير لتلاشي الجزء عند الانتقال بعيدًا عنه.
  • يحدد انتقال العنصر المشترَك كيفية انتقال العرض الذي تتم مشاركته بين جزأين بينهما. على سبيل المثال، إنّ الصورة المعروضة في ImageView في الجزء "أ" يتم نقلها إلى الجزء "ب" عندما تصبح الصورة "ب" مرئية.

ضبط الصور المتحركة

أولاً، يجب إنشاء رسوم متحركة لتأثيرات الإدخال والخروج، والتي يتم تشغيلها عند الانتقال إلى جزء جديد. يمكنك تعريف الرسوم المتحركة على أنها موارد رسوم متحركة ما قبل سن المراهقة. تتيح لك هذه الموارد تحديد كيفية تدوير الأجزاء وتمددها وتلاشيها وتحركها أثناء الرسوم المتحركة. على سبيل المثال، قد ترغب في أن يتلاشى الجزء الحالي ويتحرك الجزء الجديد من الحافة اليمنى للشاشة، كما هو موضح في الشكل 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 وpopExit. ويخرج الجزء الحالي من الشاشة إلى اليسار بينما يتلاشى الجزء السابق.
الشكل 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));
    }
}

تتوافق الأجزاء مع عمليات نقل AndroidX. على الرغم من أنّ الأجزاء تتوافق أيضًا مع عمليات النقل إلى إطار العمل، ننصح بشدة باستخدام عمليات انتقال AndroidX، لأنّها متوافقة مع المستويات 14 والمستويات الأعلى لواجهة برمجة التطبيقات، وتتضمّن إصلاحات للأخطاء لا تتوفّر في الإصدارات القديمة من عمليات انتقال الإطارات.

استخدام انتقالات العناصر المشتركة

في إطار عمل الانتقال، تحدد انتقالات العناصر المشتركة كيفية تحرك طرق العرض المقابلة بين جزأين أثناء انتقال الأجزاء. على سبيل المثال، قد تحتاج إلى عرض صورة في ImageView على الجزء A للانتقال إلى الجزء "ب" عندما تصبح "ب" مرئية، كما هو موضح في الشكل 3.

انتقال جزء مع عنصر مشترك.
الشكل 3. انتقال جزء مع عنصر مشترك.

على مستوى عالٍ، إليك كيفية إجراء انتقال مجزأ باستخدام العناصر المشتركة:

  1. قم بتعيين اسم نقل فريد لكل عرض عنصر مشترك.
  2. يمكنك إضافة طرق عرض العناصر المشترَكة وأسماء الانتقال إلى FragmentTransaction.
  3. يمكنك ضبط رسم متحرك لانتقال العناصر المشتركة.

أولاً، يجب تعيين اسم نقل فريد لكل عرض عنصر مشترك للسماح بتعيين طرق العرض من جزء إلى آخر. اضبط اسمًا نقليًا على العناصر المشتركة في كل تنسيق للأجزاء باستخدام ViewCompat.setTransitionName()، والتي توفر التوافق مع مستويات واجهة برمجة التطبيقات 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();

لتحديد كيفية انتقال العناصر المشتركة من جزء إلى آخر، يجب ضبط انتقال إدخال على الجزء الذي يتم الانتقال إليه. عليك استدعاء 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 في مرجع واجهة برمجة التطبيقات للفئة Transition.

بشكل تلقائي، يُستخدم الانتقال من عنصر مشترك كانتقال return للعناصر المشتركة. يحدد انتقال الإرجاع كيفية انتقال العناصر المشتركة مرة أخرى إلى الجزء السابق عندما تنبثق معاملة التجزئة خارج المكدس الخلفي. إذا كنت تريد تحديد انتقال مختلف للإرجاع، يمكنك إجراء ذلك باستخدام Fragment.setSharedElementReturnTransition() في طريقة onCreate() للجزء.

توافق توقّعي مع ظهر

يمكنك استخدام ميزة الرجوع التنبؤي مع العديد من الصور المتحركة التي تتضمّن أجزاءً متعددة، ولكن ليس كلها. عند تنفيذ عملية الرجوع التنبؤية، ضع الاعتبارات التالية في الاعتبار:

  • استيراد Transitions 1.5.0 أو إصدار أحدث وFragments 1.7.0 أو إصدار أحدث.
  • ويمكن استخدام الفئة Animator والفئات الفرعية ومكتبة AndroidX Transition.
  • لا تتوفّر مكتبة Transition لصف Animation وإطار العمل.
  • لا تعمل الصور المتحركة المقترَحة للأجزاء إلا على الأجهزة التي تعمل بالإصدار 14 من نظام التشغيل Android أو الإصدارات الأحدث.
  • ويمكن استخدام عبارات "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(). يستخدم المثال التالي المكتبة Glide لتحميل صورة في 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) في طريقة 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);
        ...
    }
}

مراجع إضافية

لمعرفة المزيد حول الانتقالات المجزأة، اطلع على الموارد الإضافية التالية.

العيّنات

مشاركات المدونة