Site icon AppTractor

Как мы делали красивую анимацию поезда

Джереми Мартинес в своем блоге рассказал, как он делал анимацию для своего приложения. Мы делимся с вами русским переводом статьи.

На прошлой неделе мы сделали ребрендинг – приложение Captain Train превратилось Trainline. Это означает, что нам пришлось изменить цвета, иконки, все пустые экраны и… анимации, чтобы они соответствовали нашему новому бренду. Прощай зеленый и привет темно синий и мятный!

У нас возник ряд вопросов о том, как создавать новую анимацию загрузки. Поэтому, думаю, эта статья поможет другим разработчикам. На самом деле, я попытаюсь объяснить, как мы разработали и сделали эту анимацию.

Спойлер: она полностью рендерится на устройстве – нет видео, нет gif, только старый добрый view и векторная анимация.

Контекст и цели

До ребрендинга у нас была такая анимация при загрузке результатов поиска:

Она была очень простая и сделана за 3 часа до выпуска приложения. У нас просто покадровая анимация (animation-list drawable) с 3 разными положениями шпал.

Для приложения Trainline нам надо было что-то получше. Дизайнер создал прекрасную иллюстрацию, и нашей задачей было анимировать ее. Давайте начнем с конца – вот что у нас получилось:

Теперь давайте разберемся, как я ее сделал.

Реализация

Есть несколько техник, которые можно использовать для реализации анимации в Android. Важно отметить  — я хотел, чтобы эта анимация работала на Ice Cream Sandwich (4.0), то есть на minSdk для нашего приложения.

Давайте рассмотрим разные возможности:

Дизайн

Чем дальше, тем медленнее

Первым шагом мы создали параллакс анимацию, чтобы придать чувство скорости и движения. Я провел декомпозицию оригинального изображения и разложил его на составные части в ImageViews:

Тут правило довольно простое: «Чем объект дальше, тем медленнее он движется». Например, очевидно, ветер должен двигаться быстрее горизонта.

Принципы анимации слоев также очень просты и одинаковы для всех: я двигаю слой справа налево. Он начинается за пределами окна просмотра и заканчивается тоже за пределами с другой стороны. Это повторяется бесконечно. Наконец, чем слой дальше, тем длительность анимации больше. Посмотрите на пример кода:

int translationXStart = totalViewWidth - mLayerView.getLeft() + OFFSET;
int translationXEnd = -mLayerView.getLeft() - mLayerView.getWidth() - OFFSET;

mLayerAnimator = ObjectAnimator.ofFloat(mLayerView,
                                        View.TRANSLATION_X,
                                        translationXStart,
                                        translationXEnd);

mLayerAnimator.setRepeatCount(ObjectAnimator.INFINITE);
mLayerAnimator.setRepeatMode(ObjectAnimator.RESTART);
mLayerAnimator.setInterpolator(LINEAR_INTERPOLATOR);
mLayerAnimator.setDuration(ANIMATION_DURATION);

Как вы можете видеть, все очень просто. Этот пример просто двигается по шкале X, но для некоторых слоев (поезд и птицы) мне надо было задействовать и ось Y.

Еще одна важная деталь – чтобы уменьшить потребление памяти, каждый слой переносит контент, то есть Android отрисовывает только необходимые изменения.

Как вы можете видеть, в анимации не двигаются только рельсы.

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

Волшебное исчезновение

Так как анимация повторяет сама себя бесконечно, мне надо было найти возможность плавного затухания между двумя циклами. На самом деле, когда птица достигнет левой стороны экрана, она должна снова появиться справа. Тут я использовал небольшой трюк: два View с полупрозрачным фоновым градиентом. Посмотрите ниже пример:

Естественное движение поезда

Моей первой реализацией было просто движение поезда вверх и вниз на 1dp. Нормально? Нет. Я поговорил с дизайнером, и он посоветовал мне использовать более естественную реализацию. Правда ведь, поезд не двигается просто вверх вниз.

Я хотел создать собственную кривую, чтобы он двигался по-моему:

Лучший способ добиться этого – использовать PathInterpolator. Он, вероятно, менее известен среди разработчиков, но, по моему мнению, он самый мощный, особенно теперь, когда он позаимствован для PathInterpolatorCompat.

Давайте посмотрим на код:

private void prepareLocomotiveAnimation() {

        // First, create your path
        final Path path = new Path();
        path.lineTo(0.25f, 0.25f);
        path.lineTo(0.5f, -0.25f);
        path.lineTo(0.7f, 0.5f);
        path.lineTo(0.9f, -0.75f);
        path.lineTo(1f, 1f);

        // Create the ObjectAnimatpr
        mLocomotiveAnimator = ObjectAnimator.ofFloat(mLocomotiveView,
                                                     View.TRANSLATION_Y,
                                                     -1dp,
                                                     0);

        mLocomotiveAnimator.setRepeatCount(ObjectAnimator.INFINITE);
        mLocomotiveAnimator.setRepeatMode(ObjectAnimator.REVERSE);
        mLocomotiveAnimator.setDuration(ANIMATION_DURATION_LOCOMOTIVE);

        // Use the PathInterpolatorCompat
        mLocomotiveAnimator.setInterpolator(PathInterpolatorCompat.create(path));
}

Никакой магии. Обратите внимание, что мне пришлось улучшить эту интерполяцию, используя quadTo вместо LineTo, чтобы сделать ее более гладкой. Также отметьте, что я зациклил анимацию, чтобы сделать ее непрерывной.

Заставляем птиц летать

Это настоящая вишенка на торте. Я хотел, чтобы птицы летели. Хотя объяснить друзьям, что «я целый день учил птиц летать», было непросто :) Но давайте посмотрим, как я сделал это.

Я говорил, что AnimatedVectorDrawables прекрасен? Ну так он отлично подходит для этого случая. Во-первых, импортируем VectorDrawable:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportWidth="18"
    android:viewportHeight="9"
    android:width="18dp"
    android:height="9dp">
    <path
        android:name="bird"
        android:pathData="M2,4c3,-1 6,-1.5 7,2c1,-3.5 4,-3 7,-2"
        android:strokeLineJoin="round"
        android:strokeWidth="2"
        android:strokeColor="#20416b"
        android:strokeMiterLimit="1.41421"
        android:strokeLineCap="round" />
</vector>

Значение pathData это вектор, когда крылья подняты.

Теперь вам надо анимировать, как крылья поднимаются и опускаются. Самая важная часть – иметь одинаковое количество точек в обоих веторах пути:

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:propertyName="pathData"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="M2,4c3,-1 6,-1.5 7,2c1,-3.5 4,-3 7,-2"
    android:valueTo="M4,7c3,-1 4,-4.5 5,-1c1,-3.5 2,0 5,1"
    android:valueType="pathType" />

Как вы видите, значение valueFrom это вектор поднятых крыльев, а valueTo – вектор опущенных.

Наконец, надо совместить VectorDrawable с AnimatedVectorDrawable:

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/animated_train_bird">
    <target
        android:animation="@animator/fly"
        android:name="bird" />
</animated-vector>

Как вы можете видеть, это простой пример, иллюстрирующий всю силу AnimatedVectorDrawables, и красивый последний штрих к анимации.

Заключение

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

 

Exit mobile version