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

Ссылка на код (java)

Код получился малопонятным, что подтолкнуло меня к поискам иного подхода в составлении последовательности анимаций. Решение было найдено на 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
}

Ссылка на код (java)

Код стал понятнее и читабельнее, но есть одно «но»: следить за сдвигом у каждой анимации довольно неудобно даже в такой простой последовательности. Если добавить ещё несколько шагов, то это станет почти невыполнимой задачей. Также важным минусом такого подхода стало странное поведение анимации: размер анимированного объекта, по непонятным для меня причинам, был больше, чем при обычной последовательности анимаций. Попытки разобраться ни к чему не привели, а вникать глубже я уже не стал — подход мне всё равно не нравился. Но я решил развить эту идею и разбить каждый шаг на отдельный 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
}

Ссылка на код (java)

Некорректная работа анимации, плохой подход, всё плохо. Вновь я обратился к 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
}

Ссылка на полный код (java)

Ссылка на проект в Github


Источник: Animator — инструмент для создания анимаций на Android