Главная » Хабрахабр » Анимации в Android на базе Kotlin и RxJava

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

В прошлом году на 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
})

Здесь добавляем 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 создаём последовательную цепочку из анимаций ко второй, третьей кнопке и так далее.

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

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 миллисекунд.

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

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

VanGogh

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

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

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

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

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

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

Затем cобираем анимацию. Далее выставляем alpha 1f и длительность 2 секунды. Присваиваем анимации свойство не изменяемого объекта, поэтому он сохранит эти характеристики для её запуска. Как только вызываем оператор 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, и другие.

Уже завтра стоимость билета изменится. Регистрируйся сегодня.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

OpenSceneGraph: Групповые узлы, узлы трансформации и узлы-переключатели

Когда происходит рисование точки, линии или сложного полигона в трехмерном мире, финальный результат, в конечном итоге, будет изображен на плоском, двухмерном экране. Соответственно, трехмерные объекты проходят некий путь преобразования, превращаясь в набор пикселей, выводимых в двумерное окно. Идеологически и «чистые» ...

«Монстры в играх или 15 см достаточно для атаки»

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