Animator — инструмент для создания анимаций на Android
Что такое Animator?
Немного истории. С момента запуска платформы Android существовал фреймворк View Animation. Предназначался он, как следует из названия, для анимаций. Но производительность устройств в конце нулевых была настолько низкой, что о красивых анимациях никто особо не думал, поэтому фреймворк не был удобным и гибким. Он имел только четыре типа анимации (TranslateAnimation, AlphaAnimation, ScaleAnimation, RotateAnimation), класс, позволяющий их комбинировать (AnimationSet), а также способность работать только с классами, унаследованными от View.
В Android 3.0 появился куда более гибкий фреймворк Property Animation. Он умеет изменять любое доступное свойство, а также может работать с любыми классами. Его основным инструментом является Animator.
Animator — это тип классов, предназначенных для изменения значений выбранного объекта относительно времени. Грубо говоря, это инструмент для управления потоком заданной длительности, который изменяет определённое свойство от начального значения к конечному. Таким плавно меняющимся свойством в анимации может быть, например, прозрачность.
Классы, унаследованные от Animator
ValueAnimator (наследуется от Animator)
В самом простом варианте мы задаём этому классу тип изменяемого значения, начальное значение и конечное значение, и запускаем. В ответ нам будут приходить события на начало, конец, повторение и отмену анимации и ещё на два события, которые задаются отдельно для паузы и изменения значения. Событие изменения, пожалуй, самое важное: в него будет приходить изменённое значение, с помощью которого мы и будем менять свойства объектов.
Посмотрите на изменение alpha с его помощью:
val animator = ValueAnimator.ofFloat(0, 1) animator.addUpdateListener { view.alpha = animation.animatedValue as Float } animator.start()
ObjectAnimator, наследуется от ValueAnimator
Это класс, призванный упростить работу с ValueAnimator. С ним вам не нужно вручную изменять какое-либо значение по событию изменения — вы просто даёте Animator’у объект и указываете поле, которое вы хотите изменить, например scaleX. С помощью Java Reflection ищется setter для этого поля (в данном случае — setScaleX. Далее Animator самостоятельно будет менять значение этого поля.
С помощью ObjectAnimator изменение alpha будет выглядеть так:
ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).start()
У класса View есть несколько свойств, специально предназначенных для анимирования с помощью Animator:
- прозрачность (View.ALPHA)
- масштаб (View.SCALE_X, View.SCALE_Y)
- вращение (View.ROTATION, View.ROTATION_X, View.ROTATION_Y)
- положение (View.X, View.Y, View.Z)
- положение отображаемой части (View.TRANSLATION_X, View.TRANSLATION_Y, View.TRANSLATION_Z)
AnimatorSet (наследуется от Animator)
Это класс, позволяющий комбинировать анимации различными способами: запускать одновременно или последовательно, добавлять задержки и т. д.
ViewPropertyAnimator
Это отдельный класс. Он не наследуется от Animator, но обладает той же логикой, что и ObjectAnimator для View, и предназначен для лёгкого анимирования какой-либо View без лишних заморочек.
Вот так с его помощью можно изменить alpha:
view.animate().alphaBy(0).alpha(1).start()
Как мы начали использовать Animator
Около года назад передо мной встала задача сделать анимацию при клике на элемент. Вот такую:
Не то чтобы я не делал анимаций прежде, но на аутсорсе они редко нужны. Поэтому я загуглил Animation Android. Первые пять ссылок довольно подробно описывали, как делаются анимации, и я приступил. Вот первый результат:
Код Animation
fun likeAnimation(@DrawableRes icon: Int, imageView: ImageView) { imageView.setImageResource(icon) imageView.visibility = View.VISIBLE val showAlphaAnimation = AlphaAnimation(0.0f, 1.0f) showAlphaAnimation.duration = SHOW_DURATION val showScaleAnimation = ScaleAnimation( 0.2f, 1.4f, 0.2f, 1.4f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) showScaleAnimation.duration = SHOW_DURATION val showAnimationSet = AnimationSet(false) showAnimationSet.addAnimation(showAlphaAnimation) showAnimationSet.addAnimation(showScaleAnimation) showAnimationSet.setAnimationListener(object : OnEndAnimationListener() { override fun onAnimationEnd(animation: Animation?) { val toNormalScaleAnimation = ScaleAnimation( 1.4f, 1.0f, 1.4f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) toNormalScaleAnimation.duration = TO_NORMAL_DURATION toNormalScaleAnimation.setAnimationListener(object : OnEndAnimationListener() { override fun onAnimationEnd(animation: Animation?) { val hideAlphaAnimation = AlphaAnimation(1.0f, 0.0f) hideAlphaAnimation.duration = HIDE_DURATION val hideScaleAnimation = ScaleAnimation( 1.0f, 0.2f, 1.0f, 0.2f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) hideScaleAnimation.duration = HIDE_DURATION val hideAnimationSet = AnimationSet(false) hideAnimationSet.startOffset = HIDE_DELAY hideAnimationSet.addAnimation(hideAlphaAnimation) hideAnimationSet.addAnimation(hideScaleAnimation) hideAnimationSet.setAnimationListener(object : OnEndAnimationListener() { override fun onAnimationEnd(animation: Animation?) { imageView.visibility = View.GONE } }) imageView.startAnimation(hideAnimationSet) } }) imageView.startAnimation(toNormalScaleAnimation) } }) imageView.startAnimation(showAnimationSet) }
Код получился малопонятным, что подтолкнуло меня к поискам иного подхода в составлении последовательности анимаций. Решение было найдено на StackOveflow. Идея такая: помещать в последовательности анимаций каждую последующую анимацию в AnimationSet со сдвигом, равным сумме длительностей предыдущих анимаций. Получилось гораздо лучше, чем было:
AnimationSet:
fun likeAnimation(@DrawableRes icon: Int, imageView: ImageView) { imageView.setImageResource(icon) imageView.visibility = View.VISIBLE val animationSet = AnimationSet(false) animationSet.addAnimation(showAlphaAnimation()) animationSet.addAnimation(showScaleAnimation()) animationSet.addAnimation(toNormalScaleAnimation()) animationSet.addAnimation(hideAlphaAnimation()) animationSet.addAnimation(hideScaleAnimation()) animationSet.setAnimationListener(object : OnEndAnimationListener() { override fun onAnimationEnd(animation: Animation?) { imageView.visibility = View.GONE } }) imageView.startAnimation(animationSet) } private fun showAlphaAnimation(): Animation? { val showAlphaAnimation = AlphaAnimation(0.0f, 1.0f) showAlphaAnimation.duration = SHOW_DURATION return showAlphaAnimation } private fun showScaleAnimation(): Animation? { val showScaleAnimation = ScaleAnimation( 0.2f, 1.4f, 0.2f, 1.4f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) showScaleAnimation.duration = SHOW_DURATION return showScaleAnimation } private fun toNormalScaleAnimation(): Animation? { val toNormalScaleAnimation = ScaleAnimation( 1.4f, 1.0f, 1.4f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) toNormalScaleAnimation.duration = TO_NORMAL_DURATION toNormalScaleAnimation.startOffset = SHOW_DURATION return toNormalScaleAnimation } private fun hideAlphaAnimation(): Animation? { val hideAlphaAnimation = AlphaAnimation(1.0f, 0.0f) hideAlphaAnimation.duration = HIDE_DURATION hideAlphaAnimation.startOffset = SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY return hideAlphaAnimation } private fun hideScaleAnimation(): Animation? { val hideScaleAnimation = ScaleAnimation( 1.0f, 0.2f, 1.0f, 0.2f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) hideScaleAnimation.duration = HIDE_DURATION hideScaleAnimation.startOffset = SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY return hideScaleAnimation }
Код стал понятнее и читабельнее, но есть одно «но»: следить за сдвигом у каждой анимации довольно неудобно даже в такой простой последовательности. Если добавить ещё несколько шагов, то это станет почти невыполнимой задачей. Также важным минусом такого подхода стало странное поведение анимации: размер анимированного объекта, по непонятным для меня причинам, был больше, чем при обычной последовательности анимаций. Попытки разобраться ни к чему не привели, а вникать глубже я уже не стал — подход мне всё равно не нравился. Но я решил развить эту идею и разбить каждый шаг на отдельный AnimatorSet. Вот что вышло:
AnimatorSet в AnimatorSet
fun likeAnimation(@DrawableRes icon: Int, imageView: ImageView) { imageView.setImageResource(icon) imageView.visibility = View.VISIBLE val animationSet = AnimationSet(false) animationSet.addAnimation(showAnimationSet()) animationSet.addAnimation(toNormalAnimationSet()) animationSet.addAnimation(hideAnimationSet()) animationSet.setAnimationListener(object : OnEndAnimationListener() { override fun onAnimationEnd(animation: Animation?) { imageView.visibility = View.GONE } }) imageView.startAnimation(animationSet) } private fun showAnimationSet(): AnimationSet? { val showAlphaAnimation = AlphaAnimation(0.0f, 1.0f) val showScaleAnimation = ScaleAnimation( 0.2f, 1.4f, 0.2f, 1.4f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) val set = AnimationSet(false) set.addAnimation(showAlphaAnimation) set.addAnimation(showScaleAnimation) set.duration = SHOW_DURATION return set } private fun toNormalAnimationSet(): AnimationSet? { val toNormalScaleAnimation = ScaleAnimation( 1.4f, 1.0f, 1.4f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) val set = AnimationSet(false) set.addAnimation(toNormalScaleAnimation) set.duration = TO_NORMAL_DURATION set.startOffset = SHOW_DURATION return set } private fun hideAnimationSet(): AnimationSet? { val hideAlphaAnimation = AlphaAnimation(1.0f, 0.0f) val hideScaleAnimation = ScaleAnimation( 1.0f, 0.2f, 1.0f, 0.2f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) val set = AnimationSet(false) set.duration = HIDE_DURATION set.addAnimation(hideAlphaAnimation) set.addAnimation(hideScaleAnimation) set.startOffset = SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY return set }
Некорректная работа анимации, плохой подход, всё плохо. Вновь я обратился к Google, и наткнулся на то, что Animation уже является Legacy code, то есть устарел и не поддерживается, хотя и используется.
Я понял, что нужно делать анимации совершенно иначе. И вот на просторах Android Developers я наткнулся на Animator. Попытка сделать анимацию с его помощью выглядела так:
Animator
fun likeAnimation(@DrawableRes icon: Int, view: ImageView?) { if (view != null && !isAnimate) { val set = AnimatorSet() set.playSequentially( showAnimatorSet(view), toNormalAnimatorSet(view), hideAnimatorSet(view) ) set.addListener(getLikeEndListener(view, icon)) set.start() view.animate().alphaBy(0f).alpha(1f).start() } } private fun getLikeEndListener(view: ImageView, icon: Int): AnimatorListenerAdapter? { return object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator?) { super.onAnimationStart(animation) isAnimate = true view.visibility = View.VISIBLE view.setImageResource(icon) view.setLayerType(View.LAYER_TYPE_HARDWARE, null) } override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) isAnimate = false view.visibility = View.GONE view.setImageDrawable(null) view.setLayerType(View.LAYER_TYPE_NONE, null) } } } private fun showAnimatorSet(view: View): AnimatorSet? { val set = AnimatorSet() set.setDuration(SHOW_DURATION).playTogether( ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f), ObjectAnimator.ofFloat(view, View.SCALE_X, 0.2f, 1.4f), ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.2f, 1.4f) ) return set } private fun toNormalAnimatorSet(view: View): AnimatorSet? { val set = AnimatorSet() set.setDuration(TO_NORMAL_DURATION).playTogether( ObjectAnimator.ofFloat(view, View.SCALE_X, 1.4f, 1f), ObjectAnimator.ofFloat(view, View.SCALE_Y, 1.4f, 1f) ) return set}private fun hideAnimatorSet(view: View): AnimatorSet? { val set = AnimatorSet() set.setDuration(HIDE_DURATION).playTogether( ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f), ObjectAnimator.ofFloat(view, View.SCALE_X, 1f, 0.2f), ObjectAnimator.ofFloat(view, View.SCALE_Y, 1f, 0.2f) ) set.startDelay = HIDE_DELAY return set }
Анимация работала безупречно, а значит, поиски можно считать оконченными. Единственное, что при работе с Animator нужно помнить, не запущен ли уже какой-то Animator для конкретной view, потому что в противном случае старый продолжит выполнятся, как ни в чем не бывало.
Глубже в Animator
Я начал поиски того, что ещё интересного можно сделать с помощью Animator. Полёт мысли привёл меня к следующему:
При нажатии на кнопку одновременно выполняется четыре Animator’a:
– Одновременный запуск
val showHideSet = AnimatorSet() showHideSet.playTogether( ScrollAnimatorUtils.translationYAnimator(translationY, footerButtons), ScrollAnimatorUtils.translationYAnimator(translationY, footerText), ScrollAnimatorUtils.scrollAnimator(startScroll, endScroll, scrollView), ScrollAnimatorUtils.alphaAnimator(1, 0, recyclerView) ) showHideSet.start()
1) двигает вниз footer списка;
2) двигает вниз кнопки;
fun translationYAnimator(start: Float, end: Int, view: View, duration: Int): Animator? { val animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, end.toFloat()) animator.duration = duration.toLong() animator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) view.translationY = start } }) return animator }
3) скроллит ScrollView до самого низа;
public static Animator scrollAnimator(int start, int end, final View view, int duration) { ValueAnimator scrollAnimator = ValueAnimator.ofInt(start, end); scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { view.scrollTo(0, (int) valueAnimator.getAnimatedValue()); } }); scrollAnimator.setDuration(duration); scrollAnimator.addListener(getLayerTypeListener(view)); return scrollAnimator; }
4) накладывает alpha эффект на recyclerView.
fun alphaAnimator(start: Int, end: Int, view: View, duration: Int): Animator? { val alphaAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, start.toFloat(), end.toFloat()) alphaAnimator.duration = duration.toLong() alphaAnimator.addListener(getLayerTypeListener(view)) return alphaAnimator }
Источник: Animator — инструмент для создания анимаций на Android