Pull to refresh
0
e-legion
Делаем приложения, которыми пользуются миллионы

Анимации в Android на базе Kotlin и RxJava

Reading time 9 min
Views 21K


Привет, Хабр! В прошлом году на MBLT DEV выступал Ivan Škorić из PSPDFKit c докладом о создании анимаций в Android на базе Kotlin и библиотеки RxJava.

Приёмы из доклада я сейчас использую в работе над своим проектом, они здорово помогают. Под катом — расшифровка доклада и видеозапись, теперь этими приёмами можете воспользоваться и вы.

Анимация


В Android есть 4 класса, которые применяются как бы по умолчанию:

  1. ValueAnimator — этот класс предоставляет простой механизм синхронизации для запуска анимаций, которые вычисляют анимированные значения и устанавливают их для View.
  2. ObjectAnimator — это подкласс ValueAnimator, который позволяет поддерживать анимацию для свойств объекта.
  3. AnimatorSet применяется для создания последовательности анимаций. Например, у вас есть последовательность из анимаций:

    1. View выезжает слева на экране.
    2. После завершения первой анимации мы хотим выполнить анимацию появления для другой View и т.д.
  4. ViewPropertyAnimator — автоматически запускает и оптимизирует анимации для выбранного свойства View. В основном мы будем использовать именно его. Поэтому мы применим этот API-интерфейс, а затем поместим его в RxJava в рамках реактивного программирования.


ValueAnimator


Разберём фреймворк ValueAnimator. Он применяется для изменения значения. Вы задаёте диапазон значений через ValueAnimator.ofFloat для примитивного типа float от 0 до 100. Указываете значение длительности Duration и запускаете анимацию.
Рассмотрим на примере:

val animator = ValueAnimator.ofFloat(0f, 100f)
animator.duration = 1000
animator.start()

animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
   override fun onAnimationUpdate(animation: ValueAnimator) {
       val animatedValue = animation.animatedValue as Float
       textView.translationX = animatedValue
   }
})

Здесь добавляем UpdateListener и при каждом обновлении будем двигать нашу View по горизонтали и менять её положение от 0 до 100, хотя это не очень хороший способ выполнения этой операции.

ObjectAnimator


Ещё один пример реализации анимации — ObjectAnimator:

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

Даём ему команду изменить у нужной View конкретный параметр до определённого значения и выставляем время методом setDuration. Суть в том, что в вашем классе должен находиться метод setTranslationX, потом система через рефлексию найдёт этот метод, а затем будет происходить анимирование View. Проблема в том, что здесь используется рефлексия.

AnimatorSet


Теперь рассмотрим класс AnimatorSet:

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

На самом деле, он не очень удобен в применении, особенно для большого количества объектов. Если вы хотите делать более сложные анимации — например устанавливать задержку между появлением анимаций, и чем больше анимаций вы хотите выполнять, тем сложнее всё это контролировать.

ViewPropertyAnimator


Последний класс — ViewPropertyAnimator. Он является одним из лучших классов для анимирования View. Это отличный API для введения последовательности запускаемых вами анимаций:

ViewCompat.animate(textView)
       .translationX(50f)
       .translationY(100f)
       .setDuration(1000)
       .setInterpolator(AccelerateDecelerateInterpolator())
       .setStartDelay(50)
       .setListener(object : Animator.AnimatorListener {
           override fun onAnimationRepeat(animation: Animator) {}
           override fun onAnimationEnd(animation: Animator) {}
           override fun onAnimationCancel(animation: Animator) {}
           override fun onAnimationStart(animation: Animator) {}
       })

Запускаем метод ViewCompat.animate, который возвращает ViewPropertyAnimator, и для анимирования translationX задаём значение 50, для параметра translatonY — 100. Затем указываем длительность анимации, а также интерполятор. Интерполятор определяет последовательность, в которой будут появляться анимации. В данном примере используется интерполятор, который ускоряет начало анимации и добавляет замедление в конце. Также добавляем задержку для старта анимации. Кроме этого, у нас есть AnimatorListener. С его помощью можно подписываться на определённые события, которые возникают во время выполнения анимации. У данного интерфейса есть 4 метода: onAnimationStart, onAnimationCancel, onAnimationEnd, onAnimationRepeat.

Как правило, нас интересует только завершение анимации. В API Level 16
добавили withEndAction:

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

В ней можно определить интерфейс Runnable, и после завершения показа конкретной анимации выполнится действие.

Теперь несколько замечаний по поводу процесса создания анимаций в целом:

  1. Метод start() не обязателен: как только вы вызываете метод animate(), вводится последовательность анимаций. Когда ViewPropertyAnimator будет настроен, система запустит анимацию сразу, как будет готова это сделать.
  2. Только один класс ViewPropertyAnimator может анимировать только конкретный View. Поэтому если вы хотите выполнять несколько анимаций, например, вам хочется, чтобы что-то двигалось, и при этом увеличивалось в размерах, то нужно указать это в одном аниматоре.

Почему мы выбрали RxJava?


Начнём с простого примера. Предположим, мы создаем метод fadeIn:

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

Это достаточно примитивное решение, и чтобы применить его для своего проекта, вам нужно будет учесть некоторые нюансы.

Мы собираемся создать CompletableSubject, который будем использовать, чтобы дожидаться завершения анимаций, а затем при помощи метода onComplete отправлять сообщения подписчикам. Для последовательного запуска анимаций необходимо стартовать анимацию не сразу, а как только на неё кто-то подпишется. Таким образом можно последовательно запускать несколько анимации в реактивном стиле.

Рассмотрим саму анимацию. В ней передаём View, над которой будет совершаться анимация, а также указываем длительность анимации. И поскольку это анимация — появление, то мы должны указать прозрачность 1.

Попробуем использовать наш метод и создадим простую анимацию. Предположим, у нас на экране есть 4 кнопки, и мы хотим добавить для них анимацию появления длительностью 1 секунду:

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

В результате получается вот такой лаконичный код. С помощью оператора andThen можно запускать анимации последовательно. Когда мы подпишемся на него, он отправит событие doOnSubscribe к Completable, который стоит первым в очереди на исполнение. После его завершения он подпишется ко второму, третьему, и так по цепочке. Поэтому если на каком-то этапе появляется ошибка, то вся последовательность выдаёт ошибку. Нужно также указать значение alpha 0 до начало анимации, чтобы кнопки были невидимыми. И вот как это будет выглядеть:


Используя Kotlin, то мы можем использовать расширения:

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

Для класса View добавили функцию-расширение. В дальнейшем нет необходимости передавать в метод fadeIn аргумент View. Теперь можно внутри метода заменить все обращения к View на ключевое слово this. Это то, на что способен язык Kotlin.

Посмотрим, как изменился вызов этой функции в нашей цепочке анимаций:

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

Теперь код выглядит более понятным. В нём явно указано, что мы хотим применить к нужному отображению анимацию с определённой продолжительностью. При помощи оператора andThen создаём последовательную цепочку из анимаций ко второй, третьей кнопке и так далее.

Всегда указываем длительность анимаций, это значение одинаково для всех отображений — 1000 миллисекунд. На помощь снова приходит Kotlin. Мы можем сделать значение времени по умолчанию.

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

Если не указывать параметр duration, то время автоматически будет установлено как 1 секунда. Но если мы захотим для кнопки под номером 2 увеличить это время до 2 секунд, мы просто указываем это значение в методе:

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

Запуск двух анимаций


Мы смогли запустить последовательность из анимаций с помощью оператора andThen. Что делать, если понадобится запустить одновременно 2 анимации? Для этого в RxJava существует оператор mergeWith, который позволяет объединять элементы Completable таким образом, что они будут запускаться одновременно. Этот оператор запускает все элементы и заканчивает работу после того, как будет показан последний элемент. Если поменять andThen на mergeWith, то получим анимацию, в которой все кнопки появляются одновременно, но кнопка 2 будет появляться немного дольше остальных:

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


Теперь мы можем группировать анимации. Попробуем усложнить задачу: например, мы хотим, чтобы сначала одновременно появились кнопка 1 и кнопка 2, а затем — кнопка 3 и кнопка 4:

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

Объединяем первую и вторую кнопки оператором mergeWith, повторяем действие для третьей и четвертой, и запускаем эти группы последовательно с помощью оператора andThen. Теперь улучшим код, добавив метод fadeInTogether:

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

Он позволит запускать анимацию fadeIn для двух View одновременно. Как изменилась цепочка анимаций:

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

В итоге получится следующая анимация:


Рассмотрим более сложный пример. Предположим, нам нужно показывать анимацию с некоторой заданной задержкой. В этом поможет оператор interval:

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

Он будет генерировать значения каждые 100 миллисекунд. Каждая кнопка будет появляться спустя 100 миллисекунд. Далее указываем ещё один Observable, который будет эмитить кнопки. В данном случае у нас 4 кнопки. Воспользуемся оператором zip.

image

Перед нами потоки событий:

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

Первый соответствует timeObservable. Этот Observable будет генерировать цифры с определённой периодичностью. Предположим, она будет составлять 100 миллисекунд.

Второй Observable будет генерировать view. Оператор zip ждёт, пока первый объект появится в первом потоке, и соединяет его с первым объектом из второго потока. Несмотря на то, что все эти 4 объекта во втором потоке появятся сразу, он будет ждать, пока объекты начнут появляться на первом потоке. Таким образом, первый объект из первого потока будет соединяться с первым объектом из второго в виде нашего view, а 100 миллисекунд спустя, когда появится новый объект, оператор объединит его со вторым объектом. Поэтому view будут появляться с опеределённой задержкой.

Разберёмся с BiFinction в RxJava. Эта функция получает на вход два объекта, делает какие-то операции над ними и возвращает третий объект. Мы хотим взять объекты time и view и получить Disposable за счёт того, что мы вызываем анимацию fadeIn и подписываемся subscribe. Значение time нам не важно. В итоге получаем вот такую анимацию:


VanGogh


Расскажу про проект, который Иван начал разрабатывать для MBLT DEV 2017.

В библиотеке, которую разработал Иван, представлены различные оболочки для анимаций. Мы уже рассматривали это выше. В ней также содержатся готовые анимации, которые можно использовать. Вы получаете обобщённый набор инструментов для создания собственных анимаций. Эта библиотека предоставит вам более мощные компоненты для реактивного программирования.

Рассмотрим библиотеку на примере:

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

Предположим, вы хотите создать появляющуюся анимацию, но в этот раз вместо объекта Completable появляется AnimationCompletable. Данный класс наследуется от Completable, так что теперь появляется больше функций. Одной важной особенностью предыдущего кода было то, что отменять анимации было нельзя. Теперь можно создать объект AnimationCompletable, который заставляет анимацию остановиться, как только мы отпишемся от неё.

Создаём появляющуюся анимацию с помощью AnimationBuilder — один из классов библиотеки. Указываем, к какой View будет применена анимация. По сути этот класс копирует поведение ViewPropertyAnimator, но с разницей в том, что на выходе получаем поток.

Далее выставляем alpha 1f и длительность 2 секунды. Затем cобираем анимацию. Как только вызываем оператор build, появляется анимация. Присваиваем анимации свойство не изменяемого объекта, поэтому он сохранит эти характеристики для её запуска. Но сама анимация не запустится.

Вызываем toCompletable, который создаст AnimationCompletable. Обернёт параметры этой анимации в своеобразную оболочку для реактивного программирования, и как только вы подпишетесь на него — запустит анимацию. Если отключить его до завершения процесса, анимация закончится. Также теперь можно добавить функцию обратного вызова. Можно прописать операторы doOnAnimationReady, doOnAnimationStart, doOnAnimationEnd и тому подобное:

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

В этом примере мы показали, как удобно использовать AnimationBuilder, и изменили состояние нашей View перед запуском анимации.

Видеозапись доклада


Мы рассмотрели один из вариантов создания, компоновки и настройки анимации с помощью Kotlin и RxJava. Вот ссылка на проект, в котором описаны базовые анимации и примеры для них, а также основные оболочки для работы с анимацией.

Помимо расшифровки делюсь видеозаписью доклада:


Спикеры MBLT DEV 2018


До MBLT DEV 2018 осталось чуть больше двух месяцев. У нас выступят:

  • Laura Morinigo, Google Developer Expert
  • Kaushik Gopal, автор подкаста Fragmented
  • Артём Рудой, Badoo
  • Дина Сидорова, Google, и другие.

Уже завтра стоимость билета изменится. Регистрируйся сегодня.
Tags:
Hubs:
+14
Comments 5
Comments Comments 5

Articles

Information

Website
www.e-legion.ru
Registered
Founded
Employees
101–200 employees
Location
Россия