پیاده‌سازی انیمیشن اندروید با کاتلین و RXJava2 — راهنمای کاربردی

۱۱۵ بازدید
آخرین به‌روزرسانی: ۲۰ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
پیاده‌سازی انیمیشن اندروید با کاتلین و RXJava2 — راهنمای کاربردی

روش‌های مختلفی برای اجرای انیمیشن در اندروید وجود دارند. در این مقاله در مورد روش پیاده‌سازی انیمیشن اندروید با کاتلین و RXJava2 صحبت می‌کنیم. 4 کلاس وجود دارند که به صورت پیش‌فرض از سوی این فریمورک اندروید مورد استفاده قرار می‌گیرند.

  • ValueAnimator برای انیمیت کردن مقادیر استفاده می‌شود. بنابراین متدهای آماده‌ای برای انیمیت مقادیر خاص در اختیار دارید که به صورت عمده شامل مقادیر مقدماتی می‌شود.
  • ObjectAnimator یک کلاس فرعی از ValueAnimator است که امکان پشتیبانی از انیمیشن برای مشخصه‌های شیء را می‌دهد.
  • AnimatorSet اساساً برای زمان‌بندی انیمیشن‌ها مورد استفاده قرار می‌گیرد. نمونه‌هایی از کاربرد آن شامل موارد زیر هستند:
    • نما (View) از سمت چپ صفحه وارد می‌شود.
    • پس از تکمیل انیمیشن نخست، لازم است یک انیمیشن ظاهر شدن برای نمای دیگر رخ دهد.
  • ViewPropertyAnimator به صورت خودکار آغاز می‌شود و انیمیشن‌ها را برای نمای مشخصه منتخب بهینه‌سازی می‌کند. این کلاس در اغلب موارد مورد استفاده قرار می‌گیرد. بنابراین صد داریم از این API-ی ViewPropertyAnimator استفاده کرده و سپس آن را درون RxJava قرار دهیم.

ValueAnimator

در ادامه مثالی از ValueAnimator را بررسی می‌کنیم. می‌توان از ValueAnimator.ofFloat استفاده کرد که یک عدد اعشاری را از 0 تا 100 انیمیت می‌کند. می‌توان «مدت» (Duration) را تعیین کرده و سپس انیمیشن را آغاز کرد.

به مثال زیر توجه کنید:

1val animator = ValueAnimator.ofFloat(0f, 100f)
2animator.duration = 1000
3animator.start()
4animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
5    override fun onAnimationUpdate(animation: ValueAnimator) {
6        val animatedValue = animation.animatedValue as Float
7textView.translationX = animatedValue
8    }
9})

در مثال فوق، یک UpdateListener اضافه کرده‌ایم و هنگامی که مقدار به‌روزرسانی می‌شود، آن مقدار را روی View اعمال کرده و شیء را از 0 تا 100 جابجا می‌کنیم. با این حال این روش مناسبی برای اجرای این عملیات محسوب نمی‌شود.

ObjectAnimator

یک روش بهتر برای اجرای انیمیشن فوق استفاده از ObjectAnimator است:

1val objectAnimator = ObjectAnimator.ofFloat(textView, “translationX”, 100f)
2objectAnimator.duration = 1000
3objectAnimator.start()

دستور تغییر پارامتر مشخص شده View مطلوب را به یک مقدار معین می‌دهیم و زمان را به وسیله setDuration تعیین می‌کنیم. نکته اینجا است که باید متد setTranslationX را در کلاس خود داشته باشید، زیرا برای اجرای این متد از reflection استفاده کرده و سپس به انیمیت واقعی نما اقدام می‌کند. مشکل اینجا است که چون از reflection استفاده می‌کند، کُند است.

1val bouncer = AnimatorSet()
2bouncer.play(bounceAnim).before(squashAnim1)
3bouncer.play(squashAnim1).before(squashAnim2)
4val fadeAnim = ObjectAnimator.ofFloat(newBall, “alpha”, 1f, 0f)
5fadeAnim.duration = 250
6val animatorSet = AnimatorSet()
7animatorSet.play(bouncer).before(fadeAnim)
8animatorSet.start()

استفاده از آن به این سادگی نیست، به خصوص اگر موارد زیادی وجود داشته باشند و بخواهید کارهای پیچیده‌ای انجام دهید، با مقداری تأخیر بین انیمیشن‌ها مواجه خواهید شد. هر چه آن را روی چیزهای بیشتری به کار بگیرید، کنترل کردن آن دشوارتر می‌شود.

ViewPropertyAnimator

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

ViewPropertyAnimator یک API عالی برای زمان‌بندی انیمیشن‌هایی که باید اجرا شوند ارائه می‌کند:

1ViewCompat.animate(textView)
2        .translationX(50f)
3        .translationY(100f)
4        .setDuration(1000)
5        .setInterpolator(AccelerateDecelerateInterpolator())
6        .setStartDelay(50)
7        .setListener(object : Animator.AnimatorListener {
8            override fun onAnimationRepeat(animation: Animator) {}
9            override fun onAnimationEnd(animation: Animator) {}
10            override fun onAnimationCancel(animation: Animator) {}
11            override fun onAnimationStart(animation: Animator) {}
12        })

متد ViewCompat.animate را اجرا می‌کند که یک ViewPropertyAnimator بازگشت می‌دهد و برای انیمیت، مقدار translationX را روی 50 و پارامتر translationY را روی 100 قرار می‌دهد. سپس مدت انیمیشن اعلان می‌شود و میان‌یابی (interpolator) که استفاده خواهد شد، تعیین می‌شود. میان‌یاب شیوه اجرای انیمیشن را تعریف می‌کند. در مثال فوق از یک میان یاب استفاده شده است که در ابتدا با سرعت آغاز می‌شود و سپس در انتها کند می‌شود. ما می‌توانیم مقداری تأخیر آغازین برای انیمیشن تعیین کنیم.

به علاوه AnimatorListener را نیز داریم. با استفاده از این شنونده می‌توانید به رویدادهای خاصی که در طی اجرای انیمیشن رخ می‌دهند گوش کنید. این اینترفیس 4 متد به نام‌های onAnimationStart ،onAnimationCancel، onAnimationEnd و onAnimationRepeat دارد. ما معمولاً تنها به انتهای اکشن اهمیت می‌دهیم. در API 16 مفهومی به نام withEndAction معرفی شده است:

1.withEndAction({ //API 16+
2    //do something here where animation ends
3})

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

  1. متد ()start اختیاری است، به محض این که ViewPropertyAnimator را تعریف کنید، انیمیشن‌ها شروع به زمان‌بندی می‌کنند.
  2. برای هر نمای خاص تنها یک انیماتور در زمان مشخص می‌تواند انیمیت کند. معنی این حرف آن است که اگر قصد داشته باشید یک نما را انیمیت کنید، یک انیماتور می‌تواند روی آن عمل کند. اگر بخواهید چند انیمیشن را اجرا کنید، مثلاً یک شیء را جابجا کرده و همزمان آن را بسط دهید، باید آن را در یک انیماتور انجام دهید. شما نمی‌توانید دو انیماتور تعریف کنید و آن‌ها را همزمان به کار بگیرید، زیرا تنها یکی روی نمای منفرد اعمال خواهد شد.

چرا باید از RxJava استفاده کنیم؟

ابتدا یک مثال ساده را بررسی می‌کنیم. فرض کنید یک متد به نام fadeIn ایجاد می‌کنیم:

1fun fadeIn(view: View, duration: Long): Completable {
2    val animationSubject = CompletableSubject.create()
3    return animationSubject.doOnSubscribe {
4        ViewCompat.animate(view)
5                .setDuration(duration)
6                .alpha(1f)
7                .withEndAction {
8                     animationSubject.onComplete()
9                 }
10    }
11}

این یک راه‌حل نسبتاً ساده است و برای اعمال آن روی یک پروژه باید برخی موارد را در نظر بگیرید. ما می‌خواهیم یک CompletableSubject ایجاد کنیم که برای انتظار برای کامل شدن انیمیشن استفاده خواهد شد و سپس از متد onComplete برای ارسال پیام‌ها به مشترکان استفاده می‌شود. برای آغاز ترتیبی انیمیشن، نباید انیمیشن را بی‌درنگ آغاز کنید، بلکه باید اجرای آن را به زمانی که فردی در آن مشترک شد موکول کنید. بدین ترتیب می‌توانید چندین انیمیشن را به شیوه‌ای واکنشی و ترتیبی اجرا نمایید.

اگر به خود انیمیشن دقت کنید می‌بینید که در آن View را با اجرای انیمیشن انتقال می‌دهیم و مدت انیمیشن را نیز تعیین می‌کنیم. از آنجا که این یک انیمیشنِ ظاهر شدن است، باید میزان شفافیت را نیز 1 تعیین کنیم. فرض کنید یک انیمیشن ساده با استفاده از آن چه نوشته‌ایم ایجاد کنیم. چهار دکمه داریم و می‌خواهیم آن‌ها را انیمیت کرده و با مدت 1000 میلی‌ثانیه یا یک ثانیه محو کنیم:

1val durationMs = 1000L
2button1.alpha = 0f
3button2.alpha = 0f
4button3.alpha = 0f
5button4.alpha = 0f
6fadeIn(button1, durationMs)
7        .andThen(fadeIn(button2, durationMs))
8        .andThen(fadeIn(button3, durationMs))
9        .andThen(fadeIn(button4, durationMs))
10        .subscribe()

در نتیجه یک ساختار ساده داریم که اجرا می‌شود. می‌توانیم از عملگر andThen برای اجرای ترتیبی انیمیشن‌ها استفاده کنیم. هنگام که در آن مشترک می‌شویم، یک رویداد doonSubscribe به Completable ارسال می‌کند که در خط نخست اجرا قرار دارد. پس از کامل شدن آن، اگر خطایی دریک مرحله رخ بدهد، کل دنباله یک خطا تولید می‌کند. همچنین باید یک مقدار alpha 0 تعیین کنید، زیرا می‌خواهیم دکمه در ابتدا نامریی باشد. ظاهر آن اینک چنین است:

 کاتلین و RXJava2

در کاتلین می‌توانیم یک متد extension (+) بسازیم:

1fun View.fadeIn(duration: Long): Completable 
2    val animationSubject = CompletableSubject.create()
3    return animationSubject.doOnSubscribe {
4        ViewCompat.animate(this)
5                .setDuration(duration)
6                .alpha(1f)
7                .withEndAction {
8                     animationSubject.onComplete()
9                 }
10    }
11}

ما انیمیشن fadeIn را روی نما اعمال کردیم، به طوری که می‌توانیم با تعریف this آن را به اکستنشنی از نما تبدیل کنیم. دلیل این که گفتیم ViewCompat.animate(this) این است که شیئی که قصد داریم متد را روی آن اجرا کنیم اینک به عنوان this ارجاع یافته است و می‌توانیم آن را از this به دست آوریم.

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

1button1.fadeIn(durationMs)
2        .andThen(button2.fadeIn(durationMs))
3        .andThen(button3.fadeIn(durationMs))
4        .andThen(button4.fadeIn(durationMs))
5        .subscribe()

ساختار آن کاملاً زیبا است. اساساً می‌توانیم آن چه را که در جریان است بخوانیم. ابتدا انیمیشن FadeIn دکمه 1 و سپس FadeIn دکمه 2 و سپس FadeIn دکمه 3 و همین طور تا آخر اجرا می‌شود.

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

1fun View.fadeIn(duration: Long = 1000L):

اگر لازم باشد که برای مثال انیمیشن دکمه 2 به مدت دو ثانیه اجرا شود، می‌توانیم به طور خاص این مقدار را برای آن دکمه تعیین کنیم. در این حالت دکمه‌های دیگر همچنان در طی 1 ثانیه ظاهر خواهند شد:

1button1.fadeIn()
2        .andThen(button2.fadeIn(duration = 2000L))
3        .andThen(button3.fadeIn())
4        .andThen(button4.fadeIn())
5        .subscribe()

اجرای ترتیبی

ما می‌توانستیم یک دنباله انیمیشن را با استفاده از گزاره andThen اجرا کنیم. اگر بخواهیم 2 انیمیشن به صورت همزمان اجرا شوند چه کار باید بکنیم؟ یک عملگر به نام mergeWith وجود دارد که با Completable به عنوان نسخه قبلی خود ادغام یافته است و از این رو همه آن‌ها همزمان با هم اجرا می‌شوند و آخری یعنی callsonComplete اجرا شده و در نهایت callonComplete رخ می‌دهد. این متد همه انیمیشن‌ها را اجرا کرده و زمانی که آخرین انیمیشن اجرا شود پایان می‌یابد. اگر andThen را به mergeWith تغییر دهیم، یک انیمیشن به دست می‌آوریم که همه دکمه‌ها به طور همزمان ظاهر می‌شوند، اما دکمه 2 کمی طولانی‌تر از بقیه ظاهر می‌شود:

1button1.fadeIn()
2        .mergeWith(button2.fadeIn(2000))
3        .mergeWith(button3.fadeIn())
4        .mergeWith(button4.fadeIn())
5        .subscribe()

 کاتلین و RXJava2

همچنین می‌توانیم انیمیشن‌ها را گروه‌بندی کنیم. برای نمونه می‌توانید ابتدا دو دکمه را با هم fade in کنید و سپس سومی و چهارمی را fade in کنید.

1(button1.fadeIn().mergeWith(button2.fadeIn()))
2        .andThen(button3.fadeIn().mergeWith(button4.fadeIn()))
3        .subscribe()

در کد فوق ابتدا اقدام به ترکیب دکمه‌های اول و دوم با mergeWith و سپس تکرار اکشن برای دکمه‌های سوم و چهارم می‌کنیم و در ادامه این گروه‌ها را به صورت ترتیبی با استفاده از عملگر andThen اجرا می‌کنیم. اکنون کد خود را با افزودن متد fadeInTogether بهبود می‌بخشیم:

1fun fadeInTogether(first: View, second: View): Completable {
2    return first.fadeIn()
3            .mergeWith(second.fadeIn())
4}

این متد به شما امکان می‌دهد که انیمیشن fadeIn را برای دو View به صورت همزمان اجرا کنید. بدین ترتیب تغییرات زنجیره انیمیشن به صورت زیر خواهد بود:

1fadeInTogether(button1, button2)
2        .andThen(fadeInTogether(button3, button4))
3        .subscribe()

در نتیجه:

 کاتلین و RXJava2

در ادامه یک مثال پیچیده‌تر را بررسی می‌کنیم. فرض کنید می‌خواهیم یک انیمیشن با مقداری تأخیر تنظیم نمایش دهیم. interval به اجرای این کار کمک می‌کند:

1fun animate() {
2    val timeObservable = Observable.interval(100,    TimeUnit.MILLISECONDS)
3    val btnObservable = Observable.just(button1, button2, button3, button4)
4}

این کد مقادیری برای هر 100 میلی‌ثانیه تولید می‌کند. هر دکمه پس از 100 میلی‌ثانیه ظاهر می‌شود. سپس Observable دیگری تعیین می‌شود که دکمه‌ها را صادر می‌کند در این مورد، چهار دکمه داریم:

 کاتلین و RXJava2

اکنون «جریان‌های رویداد» را در پیش روی خود داریم:

1Observable.zip(timeObservable, btnObservable,
2        BiFunction<Long, View, Disposable>{ _, button ->
3            button.fadeIn().subscribe()
4})

رشته‌ای که داریم نخستین timeObservable است. این رشته اعدادی را در قاب‌های زمانی مشخص ارسال می‌کند. فرض کنید 100 میلی‌ثانیه را تعیین کرده‌ایم. همچنین observable دکمه دوم، اقدام به صادر کردن نماهای ما می‌کند و آن‌ها را با هم ملاقات خواهد کرد. Zip منتظر یک شیء می‌ماند تا از رشته اول بیاید و یک شیء را از استریم دوم می‌گیرد و سپس آن‌ها را با هم ادغام می‌کند. با این که همه این چهار شیء به صورت آماده در اختیار ما قرار دارند، اما منتظر استریم نخست می‌ماند تا شیء خود را بفرستد. بنابراین شیء نخست از این استریم با نمای اول ادغام می‌شود و پس از 1000 میلی‌ثانیه انتظار هنگامی که شیء دوم برسد آن را با دومی ادغام می‌کند. بدین ترتیب می‌بینیم که نماها اساساً در بازه‌های زمانی معینی ظاهر می‌شوند.

در این بخش به تعریف BiFunction در RxJava می‌پردازیم. این تابع دو شیء می‌گیرد و شیء سومی را بازگشت می‌دهد. ما می‌خواهیم time و view را داده و disposable را به دست آوریم، ما انیمیشن FadeIn را تحریک کرده و در آن مشترک می‌شویم. مقدار زمان مهم نیست و از این رو انیمیشن زیر به دست می‌آید:

 کاتلین و RXJava2

کتابخانه mbltdev

پروژه‌ی mbltdev (+) طیفی از پوسته‌ها برای انیمیشن نمایش می‌دهد. این پروژه همچنین شامل انیمیشن‌های آماده‌ای است که می‌توان مورد استفاده قرار داد. این کتابخانه کامپوننت‌های قدرتمندی برای برنامه‌نویسی واکنشی در اختیار ما قرار می‌دهد:

1fun fadeIn(view:View) : AnimationCompletable {
2    return AnimationBuilder.forView(view)
3            .alpha(1f)
4            .duration(2000L)
5            .build().toCompletable()
6}

فرض کنید می‌خواهیم این را بسازیم و این بار قرار نیست یک Completable بازگشت دهد، بلکه یک AnimationCompletable بازمی‌گرداند. کلاس AnimationCompletable شیء Completable را بسط می‌دهد به طوری که گزینه‌های بیشتری در اختیار داریم. یک نکته مهم در پیاده‌سازی قبلی این است که نمی‌توانیم انیمیشن‌ها را لغو کنیم. بنابراین در زمانی که آن‌ها رسم می‌شوند می‌توانیم، بازخوردی نشان دهیم، اما نمی‌توانیم آن‌ها را لغو کنیم. اکنون می‌توانیم Completable انیمیشن را ایجاد کنیم که در عمل وقتی از آن لغو اشتراک می‌کنیم، انیمیشن را cancel می‌کند.

انیمیشن ظاهر شدن را با استفاده از AnimationBuilder که یکی از کلاس‌های کتابخانه است می‌سازیم. سپس نمایی که انیمیشن روی آن اعمال خواهد شد را تعیین می‌کنیم. در واقع این کلاس رفتار ViewPropertyAnimator را کپی می‌کند، اما تفاوت در این است که خروجی یک استریم است.

از این رو باید alpha را روی 1f قرار دهیم، مدت روی 2 ثانیه تنظیم شده و سپس build می‌کنیم. اینک زمانی که build را فراخوانی کنیم، انیمیشن را به دست می‌آوریم. ما انیمیشن را به صورت یک شیء «تغییرناپذیر» (immutable) تعریف می‌کنیم، بنابراین پارامترهایی برای انیمیشن در خود نگهداری می‌کند. این فرایند قرار نیست چیزی را آغاز کند. از این رو صرفاً شیوه نمایش ظاهری انیمیشن را تعریف می‌کنیم.

toCompletable را اجرا می‌کنیم که AnimationCompletable را ایجاد می‌کند. AnimationCompletable پارامترهای انیمیشن را به شیوه‌ای واکنشی در خود قرار می‌دهد و زمانی که اشتراک را فراخوانی کنید، انیمیشن اجرا خواهد شد. اگر پیش از پایان انیمیشن آن را dispose کنید، انیمیشن متوقف خواهد شد. همچنین می‌توانید یک callback روی آن اضافه کنید. امکان استفاده از متدهای doOnAnimationReady ،doOnAnimationStart و doOnAnimationEnd وجود دارد:

1fun fadeIn(view:View) : AnimationCompletable {
2    return AnimationBuilder.forView(view)
3            .alpha(1f)
4            .duration(2000L)
5            .buildCompletable()
6            .doOnAnimationReady { view.alpha = 0f }
7}

در این مثال با میزان آسانی استفاده از AnimationBuilder آشنا شدیم و حالت View خود را پیش از آغاز انیمیشن تغییر دادیم.

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

==

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

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