Привет!
В свое время Product Owner попросил подумать нас о создании эффективного процесса по внедрению анимации в наше приложение на android/ios. В то время мы делали задачу по предзаполнению заявки личными данными на кредитный продукт, и на ответ от сервера требовалось некоторое время, во время которого мы хотели показывать красивую анимацию загрузки.
Задача была понятна: дизайнер хотел нарисовать красиво, отдать исходники сразу на обе
платформы без допиливаний с его стороны, и чтобы это все не лагало на старых устройствах (да, мы ещё поддерживаем android 4.1).
Какие у меня были варианты для внедрения анимации:
- Ручками, используя animated vector drawable. Плюсы в том, что рендеринг работает
в отдельном потоке (начиная с api 25), минусы — это сложность создания таких анимаций и небольшое число манипуляций с объектами. Для простых анимаций это все работает хорошо, но чуть сложнее, и начинается ад. Да и на разных платформах не заведешь. - Gif — весят немало, имеют фиксированный размер, а значит не масштабируются нормально. Да и не сделаешь с ними никаких манипуляций особо.
- Последовательность png (без комментариев).
Покопав некоторое время в сторону нативной векторной анимации андроида и gif (о боже, мы все-таки рассматривали этот вариант), я вспомнил про замечательную библиотеку Lottie, и показал её коллегам.
После некоторых проведенных тестов с различными девайсами мы решили, что библиотеку надо внедрять, тем более, что ее возможности впечатляли. Особенно обрадовался дизайнер, теперь он мог делать практически любую анимацию в Adobe After Effects, и экспортировать в json файл несколькими кликами. Обрадовались и мы, но обо всем по порядку.
Lottie была придумана и реализована компанией Airbnb в ответ на растущий запрос в кроссплатформенной анимации, поэтому она одинаково (ну почти) работает на всех платформах. Сами разработчики утверждают, что их цель — реализовать максимальное количество функций After Effects, и в этом они преуспели. Сейчас внедрить Lottie анимацию так же просто, как и вставить картинку в ImageView.
Ключевых 3 класса:
- LottieAnimationView — наследник ImageView, и самый простой способ использовать анимацию. Вы можете описать анимацию в xml, а можете в коде, поддерживается большинство методов.
- LottieDrawable — наследник Drawable, с аналогичным предыдущему классу функционалом, позволяет применить анимацию к любой view.
- LottieComposition и его спутник LottieCompositionFactory позволяют предварительно загружать анимацию из различных источников и применять к LottieDrawable и LottieAnimationView.
Загрузка ресурсов
Поддерживается загрузка ресурсов из:
- res/raw
- src/assets
- Json строки
- Любого url из сети, ведущего на json или zip файл (реализуется через HttpURLConnection, чтобы не добавлять внешних зависимостей. Если у вас анимация с изображениями, то нужно использовать zip)
- InputStream json или zip файла
Кэширование анимации
Классно то, что все анимации, загруженные через res/raw или assets, сохраняются LRU кэшем, что позволяет не тратить попусту пользовательское время повторно на загрузку и парсинг анимации, так как в случае сложной анимации может потребоваться какое-то время. Что ещё круче, если вам нужно предварительно загрузить анимацию, а потом в следующем фрагменте отобразить анимацию мгновенно, вы можете воспользоваться кодом
LottieCompositionFactory.fromRawRes(context, rawFile)
Анимация закэшируется по ключу rawFile, и там, где вам нужно её будет реально использовать, она запустится почти мгновенно.
Управление прогрессом
Lottie позволяет устанавливать текущее состояние анимации через setProgress(...). Это может пригодиться, если вы хотите анимировать состояние загрузки файла, позиции скролла, различных жестах и т.д. Я видел различные реализации на BottomSheets, PullToRefresh, CollapsingToolbarLayout.
Вот как можно использовать прогресс с AppBarLayout:
appBarLayout.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener
{ appBarLayout, verticalOffset ->
val percent = Math.abs(verticalOffset).toFloat()/appBarLayout.totalScrollRange
animationView.progress = percent
})
Зацикливание
Lottie поддерживает циклическое воспроизведение setRepeatMode() или setRepeatCount() не только целой анимации, но и любого фрагмента в пределах (0.0...1.0). Это реализуется свойствами setMinFrame, setMaxFrame, setMinAndMaxFrame. Мы использовали это, чтобы не реализовывать 3 анимации для разных состояний загрузки файла: idle, progress, complete. Вот небольшой кусочек кода, который это решает:
when (loadingStatus) {
LoadingStatus.IDLE -> {
animationView.setMaxProgress(0.1f)
}
LoadingStatus.PROGRESS -> {
animationView.setMinAndMaxProgress(0.2f, 0.9f)
animationView.repeatCount = LottieDrawable.INFINITE
animationView.playAnimation()
}
LoadingStatus.COMPLETE -> {
animationView.setMinAndMaxProgress(0.9f, 1f)
animationView.repeatCount = 1
animationView.playAnimation()
}}
Картинки
Одним из главных достоинств Lottie для нас стало то, что библиотека поддерживает вставку картинок прямо в анимацию. Причем, вы можете вставить как статичную картинку, так и динамическую, скачанную из интернета. Сейчас объясню как это работает.
В случае со статической картинкой все просто: дизайнер выгружает вам архив, где содержится json плюс сама картинка.
{
"v": "5.1.13",
"fr": 29.9700012207031,
"ip": 0,
"op": 47.0000019143492,
"w": 1034,
"h": 1334,
"nm": "Композиция 1",
"ddd": 0,
"assets": [
{
"id": "image_0",
"w": 130,
"h": 436,
"u": "images/",
"p": "img_0.png"
},
{
"id": "comp_0",
"layers": [
...
]}]
}
Вот этот img_0.png, это и есть картинка, которую вы должны положить в src/assets, и которая будет внутри анимации.
При динамической загрузке используется метод setImageAssetDelegate, в который вы должны передать bitmap. Мы предварительно загружаем картинку Glide-ом, поэтому на этапе открытия фрагмента с анимацией и картинкой все достается из кэша, поэтому всё довольно быстро. Вот код:
glideLoader.loadAsBitmap(imageUrl).into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
viewAnimation.setImageAssetDelegate(object: ImageAssetDelegate {
override fun fetchBitmap(asset: LottieImageAsset?): Bitmap {
asset?.let {
val resizeBitmap =Bitmap.createScaledBitmap(resource, it.width, it.height, true);
return resizeBitmap
} ?: run {
return resource
}
}
})
setAnimation(viewAnimation, animationImage)
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
Производительность
Конечно, лучше не использовать много анимации на экранах, где пользователь проводит много времени, так как она требовательна к процессору. По нашим тестам, загрузка процессора на некоторых анимациях доходит до 20%. Поэтому идеальный кейс такой анимации — это интерактивные элементы, которые срабатывают один раз.
Если анимация на некоторых устройствах подтормаживает, иногда помогает
viewAnimation.useHardwareAcceleration(true)
Однако разработчики рекомендуют использовать этот метод с осторожностью, так как разные телефоны по разному используют аппаратное ускорение, поэтому вместо ускорения вы можете получить обратный эффект.
Заключение
В целом, использование библиотеки Lottie существенно упрощает внедрение анимации в приложение.
Основные плюсы lottie, которые мы выделили:
- Маленький размер библиотеки (300 кб)
- Кроссплатформенное решение ios/android/web
- Загрузка анимаций из сети
- Управление прогрессом и зацикливание на любом участке
- Большое число фич из after effects позволяет дизайнеру реализовать задуманный эффект.
Минусы:
- Рендеринг выполняется в главном потоке, и в приложении может существенно падать fps.
- Парсинг lottie анимации может занимать существенное время при сложных анимациях.
Кстати, чтобы проверить насколько ресурсоемка анимация, можно воспользоваться официальным приложением Lottie из Google Play. Там есть Render Graph, где вы можете увидеть время на рендеринг кадра, а также посмотреть, как будет выглядеть анимация, если разрезать её по кадрам, или как повлияет hardwareAcceleration и многое другое.
Автор: vernau