Animations in Android based on Kotlin and RxJava



    Hi, Habr! Last year, Ivan Škorić from PSPDFKit spoke at MBLT DEV with a report on creating animations on Android based on Kotlin and the RxJava library.

    I now use the techniques from the report to work on my project, they are great help. Under the cut - the transcript of the report and the video, now you can use these techniques.

    Animation


    There are 4 classes in Android, which are used as if by default:

    1. ValueAnimator - this class provides a simple synchronization mechanism for launching animations that calculate animated values ​​and set them for View.
    2. ObjectAnimator is a subclass of ValueAnimator that allows you to support animation for object properties.
    3. AnimatorSet is used to create a sequence of animations. For example, you have a sequence of animations:

      1. View leaves on the left of the screen.
      2. After completing the first animation, we want to animate the appearance for another View, etc.
    4. ViewPropertyAnimator - automatically launches and optimizes animations for the selected View property. Basically we will use it. Therefore, we will apply this API and then put it into RxJava as part of reactive programming.


    ValueAnimator


    Let's analyze the ValueAnimator framework . It is used to change the value. You set the range of values ​​through ValueAnimator.ofFloat for the primitive type float from 0 to 100. Specify the value of the Duration duration and start the animation.
    Consider an example:

    val animator = ValueAnimator.ofFloat(0f, 100f)
    animator.duration = 1000
    animator.start()
    animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
       overridefunonAnimationUpdate(animation: ValueAnimator) {
           val animatedValue = animation.animatedValue asFloat
           textView.translationX = animatedValue
       }
    })
    

    Here we add UpdateListener and with each update we will move our View horizontally and change its position from 0 to 100, although this is not a very good way to perform this operation.

    ObjectAnimator


    Another example of animation implementation is ObjectAnimator:

    val objectAnimator = ObjectAnimator.ofFloat(textView, "translationX", 100f)
    objectAnimator.duration = 1000
    objectAnimator.start()

    We give him the command to change the specific parameter of the desired View to a certain value and set the time using the setDuration method . The bottom line is that the setTranslationX method should be in your class , then the system will find this method through reflection, and then the View will be animated. The problem is that reflection is used here.

    Animatorset


    Now consider the AnimatorSet class :

    val bouncer = AnimatorSet()
    bouncer.play(bounceAnim).before(squashAnim1)
    bouncer.play(squashAnim1).before(squashAnim2)
    val fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f)
    fadeAnim.duration = 250
    val animatorSet = AnimatorSet()
    animatorSet.play(bouncer).before(fadeAnim)
    animatorSet.start()

    In fact, it is not very convenient to use, especially for a large number of objects. If you want to make more complex animations - for example, setting a delay between the appearance of animations, and the more animations you want to perform, the more difficult it is to control it all.

    ViewPropertyAnimator


    The last class is ViewPropertyAnimator . It is one of the best classes for animating View. This is a great API for introducing the sequence of animations you run:

    ViewCompat.animate(textView)
           .translationX(50f)
           .translationY(100f)
           .setDuration(1000)
           .setInterpolator(AccelerateDecelerateInterpolator())
           .setStartDelay(50)
           .setListener(object : Animator.AnimatorListener {
               overridefunonAnimationRepeat(animation: Animator) {}
               overridefunonAnimationEnd(animation: Animator) {}
               overridefunonAnimationCancel(animation: Animator) {}
               overridefunonAnimationStart(animation: Animator) {}
           })
    

    Run the ViewCompat.animate method , which returns a ViewPropertyAnimator , and set the value 50 to animate translationX , set to translatonY - 100. Then specify the duration of the animation, as well as the interpolator. The interpolator determines the sequence in which the animations will appear. In this example, an interpolator is used, which accelerates the beginning of the animation and adds a slowdown at the end. Also add a delay to start the animation. In addition, we have AnimatorListener . With it, you can subscribe to certain events that occur during the execution of the animation. This interface has 4 methods: onAnimationStart ,onAnimationCancel , onAnimationEnd , onAnimationRepeat .

    As a rule, we are only interested in the completion of the animation. API Level 16
    added withEndAction:

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

    In it, you can define the Runnable interface , and after the display of the specific animation is complete, the action will be executed.

    Now a few comments on the process of creating animations in general:

    1. The start () method is optional: as soon as you call the animate () method , a sequence of animations is entered. When ViewPropertyAnimator is configured, the system will start the animation as soon as it is ready to do it.
    2. Only one ViewPropertyAnimator class can only animate a specific View. Therefore, if you want to perform several animations, for example, you want something to move, and at the same time increase in size, then you need to specify it in one animator.

    Why did we choose RxJava?


    Let's start with a simple example. Suppose we create a fadeIn method:

    funfadeIn(view: View, duration: Long): Completable {
       val animationSubject = CompletableSubject.create()
       return animationSubject.doOnSubscribe {
           ViewCompat.animate(view)
                   .setDuration(duration)
                   .alpha(1f)
                   .withEndAction {
                       animationSubject.onComplete()
                   }
       }
    }

    This is a rather primitive decision, and to apply it to your project, you will need to take into account some of the nuances.

    We are going to create a CompletableSubject that we will use to wait for the animations to complete, and then use the onComplete method to send messages to subscribers. For the sequential launch of animations, it is necessary to start the animation not immediately, but as soon as someone signs up for it. In this way, you can launch several reactive-style animations in sequence.

    Consider the animation itself. In it we transfer View, over which animation will be made, and also we specify duration of animation. And since this animation is an appearance, we must specify transparency 1.

    Let's try to use our method and create a simple animation. Suppose we have 4 buttons on the screen, and we want to add an appearance animation of 1 second for them:

    val durationMs = 1000L
    button1.alpha = 0f
    button2.alpha = 0f
    button3.alpha = 0f
    button4.alpha = 0f
    fadeIn(button1, durationMs)
           .andThen(fadeIn(button2, durationMs))
           .andThen(fadeIn(button3, durationMs))
           .andThen(fadeIn(button4, durationMs))
           .subscribe()

    The result is such a concise code. With the help of the operator andThen you can run animations sequentially. When we subscribe to it, it will send the doOnSubscribe event to the Completable , which is first in the queue for execution. After its completion, he subscribes to the second, third, and so on the chain. Therefore, if at some stage an error occurs, the entire sequence produces an error. You must also specify an alpha value of 0 before the animation starts so that the buttons are invisible. And this is how it will look like:


    Using Kotlin , we can use extensions:

    fun View.fadeIn(duration: Long): Completable {
       val animationSubject = CompletableSubject.create()
       return animationSubject.doOnSubscribe {
           ViewCompat.animate(this)
                   .setDuration(duration)
                   .alpha(1f)
                   .withEndAction {
                       animationSubject.onComplete()
                   }
       }
    }

    For the View class, an extension function has been added. In the future, there is no need to pass the View argument to the fadeIn method. Now it is possible within the method to replace all references to the View with the keyword this . This is what Kotlin is capable of .

    Let's see how the call to this function has changed in our animation chain:

    button1.fadeIn(durationMs)
           .andThen(button2.fadeIn(durationMs))
           .andThen(button3.fadeIn(durationMs))
           .andThen(button4.fadeIn(durationMs))
           .subscribe()
    

    Now the code looks more understandable. It explicitly states that we want to apply an animation with a specific duration to the desired display. With the help of the andThen operator, we create a sequential chain of animations to the second, third button, and so on.

    Always specify the duration of the animations, this value is the same for all mappings - 1000 milliseconds. Kotlin comes to the rescue again . We can make the time default.

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

    If you do not specify the duration parameter , the time will automatically be set to 1 second. But if we want for the button at number 2 to increase this time to 2 seconds, we simply specify this value in the method:

    button1.fadeIn()
           .andThen(button2.fadeIn(duration = 2000L))
           .andThen(button3.fadeIn())
           .andThen(button4.fadeIn())
           .subscribe()
    

    Running two animations


    We were able to start a sequence of animations using the andThen operator . What to do if you need to run 2 animations simultaneously? To do this, there is a mergeWith operator in RxJava , which allows you to combine the elements Completable so that they will run simultaneously. This statement starts all items and finishes work after the last item is shown. If you change andThen to mergeWith , you get an animation in which all the buttons appear at the same time, but button 2 will appear a little longer than the others:

    button1.fadeIn()
           .mergeWith(button2.fadeIn(2000))
           .mergeWith(button3.fadeIn())
           .mergeWith(button4.fadeIn())
           .subscribe()


    Now we can group the animations. Let's try to complicate the task: for example, we want to first appear at the same time button 1 and button 2, and then button 3 and button 4:

    (button1.fadeIn().mergeWith(button2.fadeIn()))
           .andThen(button3.fadeIn().mergeWith(button4.fadeIn()))
           .subscribe()

    We merge the first and second buttons with the operator mergeWith , repeat the action for the third and fourth, and run these groups sequentially using the operator andThen . Now let's improve the code by adding the fadeInTogether method :

    funfadeInTogether(first: View, second: View): Completable {
       return first.fadeIn()
               .mergeWith(second.fadeIn())
    }

    It allows you to run fadeIn animation for two views at the same time. How the animation chain has changed:

    fadeInTogether(button1, button2)
           .andThen(fadeInTogether(button3, button4))
           .subscribe()

    The result is the following animation:


    Consider a more complex example. Suppose we need to show the animation with some predetermined delay. The interval statement will help :

    funanimate() {
       val timeObservable = Observable.interval(100, TimeUnit.MILLISECONDS)
       val btnObservable = Observable.just(button1, button2, button3, button4)
    }

    It will generate values ​​every 100 milliseconds. Each button will appear after 100 milliseconds. Next, specify another Observable, which will emit buttons. In this case, we have 4 buttons. Let's use the zip operator .

    image

    Before us are streams of events:

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

    The first corresponds to timeObservable . This Observable will generate numbers at regular intervals. Suppose it will be 100 milliseconds.

    The second Observable will generate a view. The zip operator waits until the first object appears in the first stream, and connects it with the first object from the second stream. Despite the fact that all these 4 objects in the second stream will appear immediately, it will wait until the objects begin to appear on the first stream. Thus, the first object from the first stream will connect to the first object from the second in the form of our view, and 100 milliseconds later, when a new object appears, the operator will merge it with the second object. Therefore, the view will appear with a certain delay.

    Let's deal with BiFinction in RxJava . This function receives two objects as input, performs some operations on them and returns the third object. We want to take time and view objects and get Disposable because we invoke the fadeIn animation and subscribe to subscribe . The value of time is not important to us. As a result, we get the following animation:


    Vangogh


    I'll tell you about the project that Ivan began to develop for MBLT DEV 2017.

    In the library, which Ivan developed, there are various skins for animations. We have already considered this above. It also contains ready-made animations that you can use. You get a generalized set of tools for creating your own animations. This library will provide you with more powerful components for reactive programming.

    Consider the library by example:

    funfadeIn(view:View) : AnimationCompletable {
       return AnimationBuilder.forView(view)
               .alpha(1f)
               .duration(2000L)
               .build().toCompletable()
    }

    Suppose you want to create an appearing animation, but this time, instead of the Completable object, there is an AnimationCompletable . This class is inherited from Completable , so now more functions appear. One important feature of the previous code was that it was impossible to cancel animations. Now you can create an AnimationCompletable object that makes the animation stop as soon as we unsubscribe from it.

    Create an emerging animation using AnimationBuilder - one of the library classes. Specify which view will be applied to the animation. In essence, this class copies the behavior of ViewPropertyAnimator , but with the difference that the output is a stream.

    Next, set alpha 1f and a duration of 2 seconds. Then we collect the animation. As soon as we call the build statement , an animation appears. We assign the animation property of an unmodifiable object, so it will save these characteristics for its launch. But the animation itself will not start.

    Call toCompletable , which will create an AnimationCompletable . Wrap the parameters of this animation into a kind of shell for reactive programming, and as soon as you subscribe to it, it will start the animation. If you turn it off before the process is complete, the animation will end. You can now also add a callback function. You can set the operators doOnAnimationReady , doOnAnimationStart , doOnAnimationEnd etc:

    funfadeIn(view:View) : AnimationCompletable {
       return AnimationBuilder.forView(view)
               .alpha(1f)
               .duration(2000L)
               .buildCompletable()
               .doOnAnimationReady { view.alpha = 0f }
    }

    In this example, we showed how to use AnimationBuilder conveniently , and change the state of our View before starting the animation.

    Video of the report


    We looked at one of the options for creating, compositing, and customizing animations using Kotlin and RxJava. Here is a link to the project , which describes the basic animations and examples for them, as well as the basic shell for working with animation.

    In addition to decoding, I share a video of the report:


    Speakers MBLT DEV 2018


    Before MBLT DEV 2018 , a little more than two months are left. We will perform:

    • Laura Morinigo, Google Developer Developer
    • Kaushik Gopal, author of the Fragmented podcast
    • Artyom Rudoy, ​​Badoo
    • Dina Sidorova, Google, and others .

    Tomorrow the ticket price will change. Sign up today.

    Also popular now: