گذار (Transition) در انیمیشن‌ های اپلیکیشن های اندرویدی — راهنمای کاربردی

۱۷۰ بازدید
آخرین به‌روزرسانی: ۲۸ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
گذار (Transition) در انیمیشن‌ های اپلیکیشن های اندرویدی — راهنمای کاربردی

در این نوشته به معرفی برخی مفاهیم نسبتاً جدید در مورد انیمیشن‌های اندروید پرداخته‌ایم. گوگل با معرفی متریال دیزاین کمک زیادی به انیمیت کردن همه چیز در اندروید کرده و به این منظور Material motion را ارائه کرده است. بدین ترتیب می‌توان از گذار (Transition) در اندروید برای ایجاد انیمیشن‌های مختلف استفاده کرد.

Motion موجب ایجاد معنا می‌شود. اشیا بدون ایجاد وقفه در تجربه کاربر، حتی در زمان تبدیل شدن و شناسایی به وی عرضه می‌شوند. Motion دنیای طراحی متریال است که برای توصیف روابط فضایی، کارکردها و مقاصد به طرزی زیبا و سیال مورد استفاده قرار می‌گیرند.

-راهنمای متریال دیزاین

در واقع فرایند ایجاد انیمیشن زمان‌بر است. یک توسعه‌دهنده همواره وسوسه می‌شود که صرفاً خصوصیت (setVisibility(View.VISIBLE یک شیء را فراخوانی کند و وقت خود را صرف امور مهم‌تری مانند منطق تجاری ویژگی‌های جدید اضافه شده به اپلیکیشن نماید. همه این موارد در اوقاتی که همه زمان‌بندی‌ها منقضی شده‌اند بیشتر نمود می‌یابند. اما به خاطر داشته باشید، هر بار که فرصتی را برای افزودن یک گذارِ معنی‌بخش به UI اپلیکیشن از دست می‌دهید، فرصت بزرگی از بین رفته است.

اینک استفاده از انیمیشن در طراحی رابط کاربری به تلاش کمتری نیاز دارد. شما می‌توانید از API موجود برای Transition-ها استفاده کنید که از سوی گوگل برای ایجاد انیمیشن‌های زیبا بین اکتیویتی‌ها ارائه شده است. متأسفانه همه این امکانات تنها از نسخه 5.0 به بعد اندروید در اختیار ما قرار می‌گیرند؛ اما تصور کنید که این API می‌تواند به طرز مؤثری در موارد مختلف مورد استفاده قرار گیرد و اگر روی نسخه‌های قدیمی‌تر اندروید نیز عرضه می‌شد بسیار هیجان‌انگیزتر می‌بود.

بررسی تاریخچه

پارامتر جدید animateLayoutChange در نسخه 4.0 اندروید عرضه شده است. اما زمانی که آن را فراخوانی کرده و برخی موارد را درون آن پیکربندی می‌کنید همچنان ناپایدار است و به قدر کافی انعطاف‌پذیری ندارد. بنابراین نمی‌توانید کار زیادی با آن انجام دهید.

در نسخه 4.4 اندروید (Kitkat)، مفهوم «صحنه‌ها» (Scenes) و «گذارها» (Transitions) معرفی شده است. صحنه از لحاظ فنی به وضعیت همه «نما» (View) ها در ریشه Scene یعنی layout container گفته می‌شود. گذار مجموعه‌ای از انیمیشن‌ها است که روی نما اعمال می‌شوند تا گذار همواری از یک صحنه به صحنه دیگر اجرا کند. برای روشن‌تر شدن موضوع در ادامه به بررسی یک مثال می‌پردازیم.

طراحی یک دکمه

تصور کنید یک دکمه داریم و پس از یک کلیک می‌خواهیم متنی به زیر دکمه اضافه شود. طرح‌بندی این وضعیت چنین است:

1<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2             android:id="@+id/transitions_container"
3             android:layout_width="match_parent"
4             android:layout_height="match_parent"
5             android:gravity="center"
6             android:orientation="vertical">
7 
8    <Button
9       android:id="@+id/button"
10       android:layout_width="wrap_content"
11       android:layout_height="wrap_content"
12       android:text="DO MAGIC"/>
13 
14    <TextView
15       android:id="@+id/text"
16       android:layout_width="wrap_content"
17       android:layout_height="wrap_content"
18       android:layout_marginTop="16dp"
19       android:text="Transitions are awesome!"
20       android:visibility="gone"/>
21 
22</LinearLayout>

ما در جاوا یک «شنونده کلیک» (click listener) داریم. بنابراین نتیجه کار چنین است:

گذار (Transition) در اندروید

تا به این جا که بد نبوده است، با تنها یک خط کد توانستیم یک انیمیشن طراحی کنیم. نکته جالب در این کد آن است که «پدیداری» (visibility) متن بر حسب انیمیشن تعریف می‌شود؛ اما همزمان موقعیت دکمه نیز تغییر می‌یابد. فریمورک Transition به صورت خودکار طرح‌بندی را طوری انیمیت می‌کند که تغییر ناشی از ظاهر شدن TextView جبران شود و بنابراین لازم نیست به صورت دستی کاری انجام دهید. شما حتی می‌توانید انیمیشن جدیدی را هنگامی که انیمیشن قبلی در حال اجرا است آغاز کنید. فریمورک Transition انیمیشن در حال اجرا را متوقف کرده و سپس اقدام به انیمیت کردن نماها از موقعیت فعلی‌شان می‌کند. همه این کارها در پس‌زمینه به شیوه‌ای جادویی و خودکار انجام می‌شوند.

ما می‌توانیم روش دقیق اجرای گذار را تعیین کنیم که از طریق پارامتر دوم متد beginDelayedTransition اجرا می‌شود.

انواع ساده گذار

  • ChangeBounds: به انیمیت کردن تغییرات رخ داده در موقعیت و اندازه یک نما می‌پردازد. برای نمونه این نوع گذار موجب جابجایی دکمه در مثال ما شده است.
  • Fade: کلاس Visibility را بسط می‌دهد و رایج‌ترین انواع انیمیشن‌ها را اجرا می‌کند که fade in و fade out هستند. در مثال ما روی TextView اعمال شده است.
  • TransitionSet: عملاً گذار است که مجموعه‌ای از گذارهای دیگر را در خود دارد. این گذارها می‌توانند با همدیگر آغاز شوند یا به صورت ترتیبی اجرا شوند. برای تغییر دادن ترتیب آن‌ها باید setOrdering را فراخوانی کنید.
  • AutoTransition: یک «مجموعه گذار» (TransitionSet) است که شامل Fade Out ،ChangeBounds و Fade In با ترتیب متوالی است. در ابتدا نماهایی که در صحنه دوم وجود ندارند محو می‌شوند، سپس ChangeBounds برای تغییر موقعیت و اندازه استفاده می‌شود و در نهایت نماهای جدید ظاهر می‌شوند. به صورت پیش‌فرض از AutoTransition در زمانی که هیچ گذاری در آرگومان دوم beginDelayedTransition تعیین نشده باشد، استفاده می‌شود.

Backport

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

بنابراین باید گفت که کتابخانه «Transitions Everywhere» (+) یک backport برای API گذار از نسخه 4.0 اندروید به بالا است. برای شروع به استفاده از آن می‌توانید یک وابستگی gradle تعیین کنید:

1dependencies {
2    implementation "com.andkulikov:transitionseverywhere:1.8.1"
3}

در مورد همه کلاس‌های مرتبط نیز می‌توانید دستور ایمپورت را از *.android.transition به *.com.transitionseverywhere تغییر دهید. گوگل کتابخانه پشتیبانی فریمورک Transitions را منتشر کرده است؛ اما هنوز باگ‌هایی دارد که در کتابخانه فوق اصلاح شده‌اند.

چه کارهای دیگری می‌توان انجام داد؟

قبل از هر چیز می‌توانیم مدت گذار را تغییر دهیم، گذارها را میان‌یابی کنیم و آن را با تأخیر برای انیمیشن‌های درون گذار آغاز کنیم:

1transition.setDuration(300);
2transition.setInterpolator(new FastOutSlowInInterpolator());
3transition.setStartDelay(200);

در ادامه نگاهی به انواع مختلف گذارهای دیگر خواهیم داشت:

Slide

این گذار نیز مانند Fade اقدام به بسط Visibility می‌کند. این گذار به نماهای جدید کمک می‌کند که در صحنه از یک سمت به سمت دیگر بروند. نمونه‌ای از آن را به صورت (Slide(Gravity.RIGHT در ادامه مشاهده می‌کنید:

گذار (Transition) در اندروید

Explode و Propagation

Explode تا حدود زیادی شبیه به Slide است؛ اما در آن نما بسته به مرکز ثقل گذار در جهت خاصی محاسبه می‌شود. در این گذار شما باید مرکز ثقل را با استفاده از متد setEpicenterCallback تعیین کنید.

TransitionPropagation تأخیرهای آغاز را برای هر انیماتور محاسبه می‌کند. برای نمونه Explode به صورت پیش‌فرض از CircularPropagation استفاده می‌کند. تأخیر برای انیمیشن به مسافت بین نما و نقطه ثقل بستگی دارد. برای اعمال کردن آن باید setPropagation را در گذار فراخوانی کرد.

فرض کنید یک RecyclerView با GridLayoutManager داریم و می‌خواهیم همه عناصر را پس از یک بار ضربه روی عنصر خاص حذف کنیم. روش کار به این صورت است:

1public void onClick(View clickedView) {
2    // save rect of view in screen coordinates
3    final Rect viewRect = new Rect();
4    clickedView.getGlobalVisibleRect(viewRect);
5 
6    // create Explode transition with epicenter
7    Transition explode = new Explode()
8        .setEpicenterCallback(new Transition.EpicenterCallback() {
9            @Override
10            public Rect onGetEpicenter(Transition transition) {
11                return viewRect;
12            }
13        });
14    explode.setDuration(1000);
15    TransitionManager.beginDelayedTransition(recyclerView, explode);
16 
17    // remove all views from Recycler View
18    recyclerView.setAdapter(null);
19}

گذار (Transition) در اندروید

ChangeImageTransform

ChangeImageTransform به انیمیت کردن تغییرات از ماتریس تصویر می‌پردازد. این تبدیل برای موقعیت‌هایی مفید است که scaleType یک ImageView تغییر میابد. در اغلب موارد بهتر است از آن به همراه ChangeBounds برای انیمیت کردن موقعیت، اندازه و یا تغییرات scaleType استفاده کنیم.

1TransitionManager.beginDelayedTransition(transitionsContainer, new TransitionSet()
2    .addTransition(new ChangeBounds())
3    .addTransition(new ChangeImageTransform()));
4 
5ViewGroup.LayoutParams params = imageView.getLayoutParams();
6params.height = expanded ? ViewGroup.LayoutParams.MATCH_PARENT : 
7    ViewGroup.LayoutParams.WRAP_CONTENT;
8imageView.setLayoutParams(params);
9 
10imageView.setScaleType(expanded ? ImageView.ScaleType.CENTER_CROP : 
11    ImageView.ScaleType.FIT_CENTER);

گذار (Transition) در اندروید

حرکت منحنی روی مسیر

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

-راهنمای متریال دیزاین

در هر گذار که با استفاده از مختصات دوبُعدی کار می‌کند، برای نمونه در تغییرات موقعیت یک نما با استفاده از ChangeBounds، می‌توان از حرکت منحنی با استفاده از متد setPathMotion بهره گرفت.

1TransitionManager.beginDelayedTransition(transitionsContainer,
2    new ChangeBounds().setPathMotion(new ArcMotion()).setDuration(500));
3 
4FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) button.getLayoutParams();
5params.gravity = isReturnAnimation ? (Gravity.LEFT | Gravity.TOP) :
6    (Gravity.BOTTOM | Gravity.RIGHT);
7button.setLayoutParams(params);

گذار (Transition) در اندروید

TransitionName

فرض کنید قصد داریم همه نماها را از کانتینر حذف کنیم و یک مجموعه جدید از نماها به آن اضافه کنیم. برخی از عناصر جدید در عمل همانند وضعیت پیش از ایجاد مجدد خواهند بود. در این حالت چگونه می‌توانیم به فریمورک کمک کنیم که درک کند کدام آیتم‌ها حذف شده‌اند و کدام یک به یک موقعیت جدید جابجا شده‌اند؟ این کار با فراخوانی متد استاتیک زیر به سادگی قابل اجرا است:

1TransitionManager.setTransitionName(View v, String transitionName)

در متد فوق می‌توانید هر نام یکتایی را بسته به مدل داده‌ها برای هر نما ارائه کنید.

برای نمونه اگر می‌خواهید لیستی از عناوین را ایجاد کنید و آن را با نماهای ایجاد شده مجدد در هر بار کلیک شدن یک دکمه به هم بریزید، می‌توانید از کد زیر استفاده کنید:

1createViews(inflater, layout, titles);
2shuffleButton.setOnClickListener(new View.OnClickListener() {
3 
4    @Override
5    public void onClick(View v) {
6        TransitionManager.beginDelayedTransition(layout, new ChangeBounds());
7        Collections.shuffle(titles);
8        createViews(inflater, layout, titles);
9    }
10 
11});
12 
13// In createViews we should provide transition name for every view.
14
15private static void createViews(LayoutInflater inflater, ViewGroup layout, List<String> titles) {
16    layout.removeAllViews();
17    for (String title : titles) {
18        TextView textView = (TextView) inflater.inflate(R.layout.text_view, layout, false);
19        textView.setText(title);
20        TransitionManager.setTransitionName(textView, title);
21        layout.addView(textView);
22    }
23}

گذار (Transition) در اندروید

Scale

این گذار در واقع بخشی از API گذار محسوب نمی‌شود و ما آن را اضافه کرده‌ایم. این گذار امکان انیمیت کردن تغییرات visibility را با انیمیشن مقیاس‌بندی فراهم می‌کند.

نمونه ساده‌ای از یک ()scale جدید به صورت زیر است:

گذار (Transition) در اندروید

ضمناً می‌توان از این گذار به همراه گذارهای دیگر نیز استفاده کرد. برای نمونه می‌توان آن را با Fade ترکیب کرد. ما می‌توانیم مقیاس نهایی سفارشی خود را در قرار دهیم.

1TransitionSet set = new TransitionSet()
2    .addTransition(new Scale(0.7f))
3    .addTransition(new Fade())
4    .setInterpolator(visible ? new LinearOutSlowInInterpolator() : 
5        new FastOutLinearInInterpolator());
6
7TransitionManager.beginDelayedTransition(transitionsContainer, set);
8text2.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);

گذار (Transition) در اندروید

Recolor

همان طور که از نام این گذار مشخص است از آن می‌توان برای انیمیت کردن تغییرات رنگ پس‌زمینه و/یا رنگ متن یک آیتم استفاده کرد:

1TransitionManager.beginDelayedTransition(transitionsContainer, new Recolor());
2 
3button.setTextColor(getResources().getColor(!isColorsInverted ? R.color.second_accent :R.color.accent));
4button.setBackgroundDrawable(
5    new ColorDrawable(getResources().getColor(!mColorsInverted ? R.color.accent :
6        R.color.second_accent)));

گذار (Transition) در اندروید

Rotate

این گذار نیز نیاز به توضیح ندارد و در ادامه می‌توانید نمونه کد آن را مشاهده کنید:

1TransitionManager.beginDelayedTransition(transitionsContainer, new Rotate());
2icon.setRotation(isRotated ? 135 : 0);

ChangeText

این گذار به ما کمک می‌کند که یک انیمیشن fade ساده برای تغییرات متنی تعریف کنیم.

1TransitionManager.beginDelayedTransition(transitionsContainer,
2    new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN));
3 textView.setText(isSecondText ? TEXT_2 : TEXT_1);

Targets

پیکربندی گذارها آسان است. شما می‌توانید نماهای خاصی را برای هر گذار هدف‌گیری کنید تا تنها این موارد انیمیت شوند. متدهایی که برای افزودن هدف قابل استفاده هستند در ادامه شرح داده شده است:

  • (addTarget(View target : برای خود نما.
  • (addTarget(int targetViewId : برای id نما.
  • (addTarget(String targetName: روشی مشابه متد TransitionManager.setTransitionName دارد.
  • (addTarget(Class targetType: برای هدف‌گیری یک کلاس برای نمونه به صورت android.widget.TextView.class.

برای حذف یک هدف می‌توان از متدهای زیر استفاده کرد:

  • (removeTarget(View target
  • (removeTarget(int targetId
  • (removeTarget(String targetName
  • (removeTarget(Class target

برای حذف کردن برخی نماها از متدهای زیر استفاده کنید:

  • (excludeTarget(View target, boolean exclude
  • (excludeTarget(int targetId, boolean exclude
  • (excludeTarget(Class type, boolean exclude
  • (excludeTarget(Class type, boolean exclude

و برای حذف کردن همه فرزندان برخی ViewGroup-ها می‌توان از متدهای زیر استفاده کرد:

  • (excludeChildren(View target, boolean exclude
  • (excludeChildren(int targetId, boolean exclude
  • (excludeChildren(Class type, boolean exclude

ایجاد گذار با استفاده از XML

گذار می‌تواند در یک فایل XML نیز تعریف شود. این فایل XML باید در پوشه res/anim قرار داشته باشد. نمونه‌ای از آن چنین است:

1<?xml version="1.0" encoding="utf-8"?>
2<transitionSet xmlns:app="http://schemas.android.com/apk/res-auto"
3              app:transitionOrdering="together"
4              app:duration="400">
5    <changeBounds/>
6    <changeImageTransform/>
7    <fade
8       app:fadingMode="fade_in"
9       app:startDelay="200">
10        <targets>
11            <target app:targetId="@id/transition_title"/>
12        </targets>
13    </fade>
14</transitionSet>
15
16// And inflating:
17TransitionInflater.from(getContext()).inflateTransition(R.anim.my_the_best_transition);

گذارهای اکتیویتی و فرگمان

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

گذارهای سفارشی

گذارها می‌توانند برای هر منظوری و برای هر نمایی مورد استفاده قرار گیرند. در ادامه برخی گذارهای سفارشی منحصر به فرد خاص خود را می‌سازیم. تنها کاری که باید انجام دهیم پیاده‌سازی سه متد captureStartValues، captureEndValues و createAnimator است. دو متد اول به دریافت حالت نما پیش و پس از تغییر صحنه می‌پردازند.

در ادامه گذاری برای پیشرفت هموارتر تغییرات یک ProgressBar افقی ایجاد می‌کنیم:

1private class ProgressTransition extends Transition {
2 
3    /**
4     * Property is like a helper that contain setter and getter in one place
5     */
6    private static final Property<ProgressBar, Integer> PROGRESS_PROPERTY = 
7        new IntProperty<ProgressBar>() {
8 
9        @Override
10        public void setValue(ProgressBar progressBar, int value) {
11            progressBar.setProgress(value);
12        }
13 
14        @Override
15        public Integer get(ProgressBar progressBar) {
16            return progressBar.getProgress();
17        }
18    };
19 
20    /**
21      * Internal name of property. Like a intent bundles 
22      */
23    private static final String PROPNAME_PROGRESS = "ProgressTransition:progress";
24 
25    @Override
26    public void captureStartValues(TransitionValues transitionValues) {
27        captureValues(transitionValues);
28    }
29 
30    @Override
31    public void captureEndValues(TransitionValues transitionValues) {
32        captureValues(transitionValues);
33    }
34 
35    private void captureValues(TransitionValues transitionValues) {
36        if (transitionValues.view instanceof ProgressBar) {
37            // save current progress in the values map
38            ProgressBar progressBar = ((ProgressBar) transitionValues.view);
39            transitionValues.values.put(PROPNAME_PROGRESS, progressBar.getProgress());
40        }
41    }
42 
43    @Override
44    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 
45            TransitionValues endValues) {
46        if (startValues != null && endValues != null && endValues.view instanceof ProgressBar) {
47            ProgressBar progressBar = (ProgressBar) endValues.view;
48            int start = (Integer) startValues.values.get(PROPNAME_PROGRESS);
49            int end = (Integer) endValues.values.get(PROPNAME_PROGRESS);
50            if (start != end) {
51                // first of all we need to apply the start value, because right now
52                // the view has end value
53                progressBar.setProgress(start);
54                // create animator with our progressBar, property and end value
55                return ObjectAnimator.ofInt(progressBar, PROGRESS_PROPERTY, end);
56            }
57         }
58         return null;
59    }
60}

شیوه استفاده از آن نیز چنین است:

1private void setProgress(int value) {
2    TransitionManager.beginDelayedTransition(mTransitionsContainer, new ProgressTransition());
3    value = Math.max(0, Math.min(100, value));
4    mProgressBar.setProgress(value);
5}

در ادامه می‌توانید نتیجه کار را مشاهده کنید:

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

==

بر اساس رای ۴ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
andkulikov
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *