پیمایش کردن بین قطعه‌ها بااستفاده از پویانمایی

‫Fragment API دو روش برای استفاده از جلوه‌های حرکتی و تبدیل‌ها ارائه می‌دهد تا قطعه‌ها را به‌صورت بصری درطول پیمایش متصل کند. یکی از این‌ها «چارچوب پویانمایی» است که هم از Animation و هم از Animator استفاده می‌کند. دیگری چارچوب گذار است که شامل گذارهای عنصر مشترک می‌شود.

می‌توانید جلوه‌های سفارشی برای ورود و خروج قطعه‌ها و برای انتقال عناصر مشترک بین قطعه‌ها مشخص کنید.

  • جلوه ورود تعیین می‌کند که قطعه چگونه وارد صفحه شود. برای مثال، می‌توانید جلوه‌ای ایجاد کنید تا وقتی به قطعه پیمایش می‌کنید، آن قطعه از لبه صفحه‌نمایش به داخل بلغزد.
  • جلوه خروج تعیین می‌کند که قطعه چگونه از صفحه خارج شود. برای مثال، می‌توانید جلوه‌ای ایجاد کنید تا هنگام پیمایش از قطعه، آن را محو کند.
  • گذار عنصر مشترک تعیین می‌کند که نمای مشترک بین دو قطعه چگونه بین آن‌ها حرکت کند. برای مثال، تصویری که در ImageView در قطعه A نمایش داده می‌شود، وقتی B نمایان می‌شود به قطعه B منتقل می‌شود.

تنظیم پویانمایی‌ها

ابتدا باید پویانمایی‌هایی برای جلوه‌های ورود و خروج ایجاد کنید که هنگام پیمایش به قطعه جدید اجرا می‌شوند. می‌توانید پویانمایی‌ها را به‌عنوان منابع پویانمایی بینابینی تعریف کنید. این منابع به شما امکان می‌دهند نحوه چرخش، کشش، محو شدن، و حرکت قطعه‌ها را درطول پویانمایی تعریف کنید. برای مثال، ممکن است بخواهید قطعه فعلی محو شود و قطعه جدید از لبه سمت راست صفحه به داخل بلغزد، همان‌طور که در شکل ۱ نشان داده شده است.

پویانمایی‌های ورود و خروج. قطعه فعلی محو می‌شود و
            قطعه بعدی از سمت راست وارد می‌شود.
شکل ۱. پویانمایی‌های ورود و خروج. قطعه فعلی محو می‌شود و قطعه بعدی از سمت راست وارد می‌شود.

این پویانمایی‌ها را می‌توان در دایرکتوری 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. قطعه فعلی از صفحه به سمت راست می‌لغزد و قطعه قبلی به‌تدریج ظاهر می‌شود.
شکل ۲. 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() و ارسال منابع پویانمایی ازطریق شناسه منبع آن‌ها، از آن‌ها استفاده کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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)
}

جاوا

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() در قطعه خروجی اعمال کنید و منابع انتقال گسترده‌شده را با شناسه منبع آن‌ها ارسال کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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)
    }
}

جاوا

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 استفاده کنید، زیرا در سطوح ای‌پی‌آی ۱۴ و بالاتر پشتیبانی می‌شوند و حاوی رفع اشکال‌هایی هستند که در نسخه‌های قدیمی‌تر انتقال‌های چارچوب وجود ندارند.

استفاده از انتقال‌های عنصر مشترک

بخشی از چارچوب انتقال، انتقال‌های عنصر مشترک تعیین می‌کنند که نماهای متناظر چگونه بین دو قطعه درطول انتقال قطعه حرکت کنند. برای مثال، ممکن است بخواهید تصویری که در ImageView در قطعه A نمایش داده می‌شود، به قطعه B منتقل شود وقتی B قابل‌مشاهده شد، همان‌طور که در شکل ۳ نشان داده شده است.

انتقال قطعه با عنصر مشترک.
شکل ۳. انتقال قطعه با عنصر مشترک.

در سطح بالا، نحوه ایجاد انتقال قطعه با عناصر مشترک در اینجا آمده است:

  1. به هر نمای عنصر هم‌رسانی‌شده، نام انتقال یکتایی اختصاص دهید.
  2. نماهای عنصر مشترک و نام‌های انتقال را به FragmentTransaction اضافه کنید.
  3. پویانمایی انتقال عنصر مشترک را تنظیم کنید.

ابتدا باید یک نام انتقال منحصربه‌فرد به هر نمای عنصر مشترک اختصاص دهید تا امکان نگاشت نماها از یک قطعه به قطعه بعدی فراهم شود. بااستفاده از ViewCompat.setTransitionName()، نام انتقال را روی عناصر مشترک در هر طرح‌بندی قطعه تنظیم کنید. این روش برای میانای برنامه‌سازی کاربردی سطح ۱۴ و بالاتر سازگاری ارائه می‌دهد. به‌عنوان مثال، نام انتقال برای یک ImageView در قطعات A و B می‌تواند به صورت زیر اختصاص داده شود:

کاتلین

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”)
    }
}

جاوا

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.addSharedElement() به FragmentTransaction اضافه کنید، و نما و نام گذر نمای متناظر را در قطعه بعدی ارسال کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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

برای مشخص کردن نحوه انتقال عناصر مشترک از یک قطعه به قطعه بعدی، باید انتقال ورود را روی قطعه‌ای که به آن پیمایش می‌شود تنظیم کنید. Fragment.setSharedElementEnterTransition() را در روش onCreate() قطعه فراخوانی کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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 پیدا کنید.

به‌طور پیش‌فرض، انتقال ورودی عنصر مشترک به‌عنوان انتقال برگشت برای عناصر مشترک نیز استفاده می‌شود. گذار برگشت تعیین می‌کند که وقتی تراکنش قطعه از پشته قبلی‌ها خارج می‌شود، عناصر مشترک چگونه به قطعه قبلی برمی‌گردند. اگر می‌خواهید انتقال برگشت متفاوتی را مشخص کنید، می‌توانید این کار را بااستفاده از Fragment.setSharedElementReturnTransition() در روش onCreate() قطعه انجام دهید.

سازگاری با اشاره برگشت پیش‌گویانه

می‌توانید از اشاره برگشت پیش‌گویانه با بسیاری از پویانمایی‌های بین‌تکه‌ای استفاده کنید، اما نه همه آن‌ها. هنگام پیاده‌سازی اشاره برگشت پیش‌گویانه، ملاحظات زیر را درنظر داشته باشید:

  • Transitions 1.5.0 یا جدیدتر و Fragments 1.7.0 یا جدیدتر را وارد کنید.
  • کلاس Animator و زیرکلاس‌ها و کتابخانه AndroidX Transition پشتیبانی می‌شوند.
  • کلاس Animation و کتابخانه چارچوب Transition پشتیبانی نمی‌شود.
  • پویانمایی‌های قطعه پیش‌بینی‌کننده فقط در دستگاه‌های دارای Android نسخه ۱۴ یا بالاتر کار می‌کند.
  • ‫setCustomAnimations، setEnterTransition، setExitTransition، setReenterTransition، setReturnTransition، setSharedElementEnterTransition، و setSharedElementReturnTransition از «بازگشت پیش‌بینانه» پشتیبانی می‌کنند.

برای کسب اطلاعات بیشتر، به افزودن پشتیبانی از پویانمایی‌های اشاره برگشت پیش‌گویانه مراجعه کنید.

به‌تعویق انداختن انتقال‌ها

در برخی موارد، ممکن است لازم باشد انتقال قطعه را برای مدت کوتاهی به تعویق بیندازید. برای مثال، ممکن است لازم باشد صبر کنید تا همه نماها در قطعه ورودی اندازه‌گیری و چیدمان شوند تا Android بتواند حالت‌های شروع و پایان آن‌ها را به‌طور دقیق برای انتقال ثبت کند.

به‌علاوه، ممکن است لازم باشد انتقال شما تا زمانی که برخی داده‌های ضروری بار شود به‌تعویق بیفتد. برای مثال، ممکن است لازم باشد تا زمانی که تصاویر برای عناصر هم‌رسانی‌شده بار شوند صبر کنید. درغیراین‌صورت، اگر بار کردن تصویر درطول یا پس‌از گذار تمام شود، ممکن است گذار ناهنجار باشد.

برای به‌تعویق انداختن انتقال، ابتدا باید مطمئن شوید که تراکنش قطعه امکان مرتب‌سازی مجدد تغییرات وضعیت قطعه را فراهم می‌کند. برای مجاز کردن تغییرات وضعیت قطعه با ترتیب مجدد، FragmentTransaction.setReorderingAllowed() را فراخوانی کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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() قطعه ورودی فراخوانی کنید:

کاتلین

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

جاوا

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

وقتی داده‌ها را بار کردید و برای شروع انتقال آماده بودید، با Fragment.startPostponedEnterTransition() تماس بگیرید. مثال زیر از کتابخانه Glide برای بار کردن تصویر در ImageView مشترک استفاده می‌کند و انتقال مربوطه را تا تکمیل بار کردن تصویر به‌تعویق می‌اندازد.

کاتلین

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)
    }
}

جاوا

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 برای ترسیم آماده شوند سپس انتقال را شروع کنید. مثالی در اینجا ارائه شده است:

کاتلین

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()
            }
        }
    }
}

جاوا

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، نام انتقال یکتایی به آن بدهید. برای مثال، اگر داده‌های هر مورد شامل شناسه یکتا باشد، می‌توان از آن به‌عنوان نام انتقال استفاده کرد، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

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

جاوا

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);
        ...
    }
}

منابع بیشتر

برای کسب اطلاعات بیشتر درباره انتقال‌های قطعه، منابع تکمیلی زیر را ببینید.

نمونه‌ها

پست‌های وبلاگ