Анимация в Android: как это работает. Создание анимаций переходов между Activity в Android

Начиная с Android 4.4 в арсенале разработчиков появился дополнительный инструмент для создания анимаций - Transitions Framework. Изначально он предназначался для создания анимаций изменения состояния приложения путём манипулирования несколькими View. С выходом Android 5.0 набор доступных для использования анимаций был расширен, чтобы соответствовать представленной тогда же концепции Material Design.

Transitions Framework позволяет быстро и безболезненно создавать различные анимации. Поэтому в процессе работы над iFunny было невозможно пройти мимо этого инструментария. Вниманию читателей предлагается частный случай использования Transitions API - создание анимации перехода между Activity с эффектом «бесшовности».

С визуальной точки зрения представленные в Transitions Framework анимации переходов между Activity можно условно разделить на два типа: обычные анимации и анимации с общим элементом. Концепт анимации с общим элементом продемонстрирован на честно украденном с сайта developer.android.com рис. 1. На нём в роли общих элементов выступают аватар и имя контакта.

Рис. 1. Анимация перехода между Activity с общими элементами

Но никто не любит длинные вступления, поэтому сразу перейдём к рассказу о том, как создавались анимации данного типа в приложении iFunny. В качестве первого примера рассмотрим анимацию, показанную на рис. 2. Для её использования нам потребуется Android версии 5.0 и выше.


Рис. 2. Анимация перехода между Activity на экране аутентификации пользователя

С точки зрения пользователя, здесь нет ничего необычного: один экран, простенькая анимация. Но, как вы уже могли догадаться, «под капотом» - переход между двумя экранами с одним общим элементом.

Первым шагом к созданию подобного перехода является, как ни странно, выбор этого самого элемента и определение его местоположения в вёрстке обеих Activity. После этого в описание каждого View, отображающего выбранный элемент, нужно добавить атрибут android:transitionName, а также назначить им android:id, если таковой отсутствует.

В нашем случае это обычные ImageView следующего вида:

Здесь стоит отметить два важных момента. Во-первых, в обоих ImageView необходимо установить одинаковые transitionName, что логично. Во-вторых, коль скоро мы используем ImageView, то и содержимое у них должно быть одним и тем же, поскольку использование двух отличающихся ресурсов может привести к неожиданным последствиям (как минимум к морганию анимируемого View в начале и конце анимации).

На втором шаге необходимо добавить опции для запускаемой (второй) Activity, сообщающие о том, что при её запуске должна быть запущена анимация.

Примечание. Под «второй» подразумевается запускаемая Activity, переход к которой должен быть осуществлён, а под «первой» - запускающая Activity.

Делается это следующим образом:

Bundle bundle = null; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { View v = activity.findViewById(R.id.auth_logo); if (v != null) { ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, v, activity.getString(R.string.email_auth_transition)); bundle = options.toBundle(); } } Intent intent = new Intent(activity, SecondActivity.class); if (bundle == null) { activity.startActivity(intent); } else { activity.startActivity(intent, bundle); }

В приведённом листинге:

  • R.id.auth_logo - ImageView из первой Activity, используемый в анимации;
  • activity - первая Activity;
  • R.string.email_auth_transition - метка, ранее оставленная в вёрстке обоих ImageView;
  • SecondActivity.class - вторая Activity.

И сейчас внимательный читатель может испытать недоумение: во вступлении речь шла об использовании API level 19, в примере фигурировал API level 21, а в листинге выше стоит ограничение на API level 22. К сожалению, при написании кода выяснилось, что анимации перехода с общим элементом могут вести себя некорректно на телефонах c API level 21. Проявляется это в виде подтормаживаний анимации в целом и артефактов на анимируемом View в частности. Если вы уже знакомы с темой, знаете причины подобного поведения и/или способы решения описанной проблемы - расскажите нам об этом в комментариях.

На третьем шаге необходимо описать анимацию перехода, т.е. указать путь, проходимый анимируемым View, и трансформацию самого View. Для этого создадим отдельный файл projectName/src/main/res/transitions/email_auth_transition.xml со следующим содержимым:

Немного теории. Тег transitionSet предназначен для описания сразу нескольких трансформаций, применяемых к анимируемому View. Параметр transitionOrdering отвечает за порядок применения этих трансформаций. В нашем случае они применяются одновременно. Существует несколько типов готовых трансформаций, представленных в Transitions Framework. С полным списком можно ознакомиться на этой странице . Мы же остановимся на двух конкретных: changeBounds и changeImageTransform.

Первая предназначена для трансформации размера View. Вторая работает только с ImageView и в связке с первой позволяет изменять не только размер, но и форму ImageView. Использовав данные трансформации, получаем на выходе анимацию изменения размера изображения, представленную на рис. 2. Если не указывать тип движения анимируемого View, то он будет двигаться по кратчайшему пути. Более интересный способ передвижения рассмотрим во втором примере.

Последним шагом создания анимации является её объявление в темах обеих Activity. Для этого отредактируем описание тем следующим образом (или создадим новые в папке projectName/src/main/res/values-v22/theme.xml):

  • android:windowActivityTransitions разрешает выполнение анимации перехода;
  • android:windowSharedElementEnterTransition указывает на файл с описанием анимации перехода от первой Activity ко второй;
  • android:windowSharedElementExitTransition указывает на файл с описанием анимации перехода при возвращении из второй Activity в первую.

Следует отметить, что для версий ОС ниже 5.1 необходимо создать темы с идентичными стилями, чтобы избежать вполне ожидаемых последствий в виде падения приложения. Например, поместим их в файл projectName/src/main/res/values/theme.xml:

Либо программно:

Transition explode = TransitionInflater.from(this).inflateTransition(R.transition.explode);
getWindow().setEnterTransition(explode);

Слайд

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

Слайд переход позволяет последовательно скользить в дочерние образы

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

android:interpolator=“@android:interpolator/decelerate_cubic“
android:slideEdge=“end“/>

Здесь мы:

  • Объявляем слайд переход
  • Устанавливаем переходный slideEdge,чтобы закончить на этом (справа), таким образом, слайды идут справа - нижний слайд должен быть установлен внизу

Угасание

Угасающий переход позволяет перейти к транзакции внутри или снаружи, используя угасающий эффект.

Угасающий переход прост, хотя переход с угасанием приятен глазу.

Создать его даже проще, чем предыдущие переходы:

android:duration=“300“/>

Здесь мы:

  • Объявляем угасающийпереход
  • Устанавливаем продолжительность до 300 миллисекунд

Оптимизация переходов

Несмотря на эксперименты, я нашел пару подходов, которые могут помочь улучшить эффекты перехода, названные выше.

Разрешение переходам содержимого окон - вы должны включить следующий атрибут в темах, которые унаследованы из материальной темы:

true

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

true

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

android:duration=“200“>




Панель инструментов и панель действий - При переходе между действиями с использованием Панели действий на использование панели инструментов (и наоборот), иногда я обнаруживал, что переход не всегда был гладким. Чтобы исправить это, я убедился, что эти два вида деятельности, участвующие в переходе, используют один и тот же компонент.

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

Общие элементы переходов

Общие элементы переходов позволяют оживить переходы между общими образами через транзакцию, это создает более приятные переходы и дает пользователю лучше ощутить свое путешествие.

Здесь, образ из нашего первого действия масштабируется и переводится в образ заголовка в нашем втором действии

В наших макетах, мы должны связать любые общие образы с использованием атрибута transitionName - он устанавливает переходные отношения между образами. Ниже приведены общие образы из выше данной анимации:

Это общие образы, что означает - они будут оживать друг с другом во время перехода действий

Для перехода между этими двумя мы начинаем с объявления имени общего перехода, выполненный, с помощью атрибута transitionName в XML макетах.




android:transitionName=“@string/transition_view“/>



android:id=“@+id/view_shared_transition“
android:transitionName=“@string/transition_view“/>
android:id=“@+id/view_separator“/>
android:id=“@+id/text_detail“/>
android:id=“@+id/text_close“/>

После того, как это будет сделано, мы создаем Pair объект в действии 1), содержащий наш переходный образ, и его transitionName. Затем мы передаем его в примерные варианты транзакций, например (ActivityOptionsCompat), так чтобы оба действия были в курсе общих компонентов. Оттуда мы начнем свою транзакцию, через примерный вариант:

Pair participants = new Pair<>(mSquareView, ViewCompat.getTransitionName(mSquareView));
ActivityOptionsCompat transitionActivityOptions =
ActivityOptionsCompat.makeSceneTransitionAnimation(
SharedTransitionsActivity.this, participants);
ActivityCompat.startActivity(SharedTransitionsActivity.this,
intent, transitionActivityOptions.toBundle());

Разделение этих образов, в то время как происходит переход, действительно помогает завершить переход

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

(Те что слева)

Я рад, что вы спросили! Этого также просто достичь, как показано ниже:

Slide slide = new Slide(Gravity.BOTTOM);
slide.addTarget(R.id.view_separator);
slide.addTarget(R.id.text_detail);
slide.addTarget(R.id.text_close);
getWindow().setEnterTransition(slide);
Как вы видите, мы создаем новый образец Слайд перехода, добавив целевые представления для перехода и установив слайд в качестве перехода входа транзакции.

Пользовательские Переходы

У нас также есть возможность создавать свои собственные переходы, используя любую анимацию из API, которую мы рассмотрели до сих пор. Например, мы можем взять Shared Element переходы на один шаг дальше, чтобы превратиться в переходный образ - это может пригодиться, когда мы хотим отобразить диалоги (или аналогичные всплывающие образы), как показано ниже:

Это движение помогает направлять внимание пользователя между составными состояниями

Давайте коротко остановимся на том, что происходит здесь:

  • Мы начинаем с создания SharedTransition, проходя в нажатом состоянии вместе с именем перехода, чтобы ссылаться на общий компонент
  • Далее мы создаем экземпляр ArcMotion, это позволяет нам создать изогнутый эффект движения при переходе между двумя образами
  • Затем мы расширяем ChangeBounds, чтобы создать пользовательский переход и трансформировать две формы (у нас есть отдельный класс для кнопки и FAB). Здесь мы переопределяем различные методы из класса, так что мы можем оживить требуемые свойства. Мы воспользуемся ViewPropertyAnimator, чтобы оживить прозрачность диалоговых образов, ObjectAnimator, чтобы оживить образы между двумя видами цвета и образец AnimatorSet, чтобы мы смогли анимировать оба этих эффекта вместе.

Анимированный вектор вводимого коэффициента

По API версии 21 (Lollipop), AnimatedVectorDrawable может быть использован для анимации VectorDrawable свойств для получения анимации drawable.

Теперь легко сделать несколько различных видов анимации на вводимом коэффициенте

Но как мы это делаем? Ну, давайте взглянем на это:

Он состоит из нескольких различных файлов, и мы начинаем с создания наших двух отдельных векторных файлов каждый из которых имеет несколько свойств:

  • Высота и ширина- Фактический размер векторного изображения
  • Viewport высоты и ширины- Объявляет размер виртуального холста, на котором нарисованы векторные дорожки
  • Название группы- Объявите группу, к которой принадлежит дорожка
  • Сводные X & Y- Объявите стержень, используемый для групповой шкалы и вращения
  • Траектория цвета заливки- Цвет заливки траектории вектора
  • Траектория данных- Объявите данные векторного пути, используемого для рисования вектора

Примечание: Все свойства ссылок хранятся в общем строчном файле, что помогает хранить элементы в порядке и аккуратности.

android:height=“56dp“
android:width=“56dp“

android:viewportWidth=“24.0“>

android:pivotX=“12“
android:pivotY=“12“>

android:pathData=“@string/path_add“/>

Вектор генерируется из нашего ic_add.xml файла (ниже)

android:height=“56dp“
android:width=“56dp“
android:viewportHeight=“24.0“
android:viewportWidth=“24.0“>
android:name=“@string/groupAddRemove“
android:pivotX=“12“
android:pivotY=“12“>
Android:fillColor=“@color/stroke_color“
android:pathData=“@string/path_remove“/>

Вектор генерируется из нашего ic_remove.xml файла (ниже)

Далее мы заявляем файлы Animated Vector Drawable , которые устанавливают как Вектор Drawable так и анимации, используемые для каждого «растяжимого» состояния (Добавить или Удалить). Глядя на добавленный или удаленный анимационный вектор, мы объявляем цель:

Анимация из одного состояния в другое
Анимация вращения вводимого коэффициента

android:drawable=“@drawable/ic_add“>
android:name=“@string/add“
android:animation=“@animator/add_to_remove“ />
android:name=“@string/groupAddRemove“
android:animation=“@animator/rotate_add_to_remove“ />

Затем мы должны создать каждый из файлов, упомянутый в этих целях.

Изменение состояния вводимого коэффициента

В add_to_remove.xml мы используем ObjectAnimator, чтобы трансформироваться между формами, используя следующие свойства:

  • PropertyName- Свойство анимации
  • valueFrom- Начальное значение для векторного пути
  • valueTo- Целевое значение для векторного пути
  • Продолжительность- Продолжительность анимации
  • интерполятор- Интерполятор, используемый для анимации
  • ValueType- Тип значения, который мы оживляем

xmlns:android=“//schemas.android.com/apk/res/android“
android:propertyName=“pathData“
android:valueFrom=“@string/path_add“
android:valueTo=“@string/path_remove“

android:interpolator=“@android:interpolator/fast_out_slow_in“
android:valueType=“pathType“ />

Поворачиваем форму

Мы применяем аналогичный подход, чтобы повернуть форму, используя свойство вращения и величины:

xmlns:android=“//schemas.android.com/apk/res/android“
android:propertyName=“rotation“
android:valueFrom=“-180“
android:valueTo=“0“
android:duration=“@integer/duration“
android:interpolator=“@android:interpolator/fast_out_slow_in“ />
Анимация обратного (от Убрать до Добавить) работает также, только со значениями анимации вспять.

Наш завершенный Анимационный Вектор вводимого коэффициента выглядит классно, не так ли!

И в заключении…

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

Если вам понравилась эта статья, то, пожалуйста, нажмите «Рекомендую»!

Я хотел бы услышать ваши мысли по этому поводу и где вы используете эти анимации - пожалуйста, оставьте отзыв или напишите мне в твиттере!

Transition Api включён в стандартный android sdk. Проблема в том, что большинство возможностей Transition Api доступно только для api 21+. Недавно Google забэкпортил многие возможности Transition Api для api 19+, но этого всё равно может быть недостаточно. На выручку нам приходит либа Transition Everywhere (), которая является бэкпортом гугловских Transitions для более ранних версий android, вплоть до android 4.0 (api 14). Кроме того, библиотека включает в себя ещё различные фичи.

Кратко подытожу: если разрабатываете приложение для api 19 и не хотите подключать ничего дополнительно, то можно обойтись Transition Api, включённым в sdk, если же вам нужна поддержка более ранних версий, то придётся использовать дополнительную либу, Transition-Everywhere (ссылка выше). Подключается она одной строчкой:

Dependencies { compile "com.andkulikov:transitionseverywhere:1.7.0" }

Чем же эта Transition Api хороша? Тем, что она просто в использовании и с её помощью можно реализовать большинство необходимых анимаций. Для реализации некоторых анимаций нужно написать всего одну строчку! Да и реализовывать более сложные анимации тоже одно удовольствие 🙂

Наконец, начнём.

Рассмотрим метод beginDelayedTransition у TransitionManager . Первым параметром идёт корневая View, к членам которой мы хотим применить анимацию, включая саму корневую вьюху. Вторым параметром мы указываем тип анимации, которую надо применить. Если второй параметр пропущен для будет установлен дефолтный AutoTransition. После вызова этого метода все View внутри указанного 1м параметром корневого View будут «слушать» действия, запускающие анимацию.

Реализуем анимацию исчезновения текста. Сначала укажем View контейнер, в котором лежит View, которую нам нужно анимировать. В нашем случае transitionContainer — это RelativeLayout , а text — это TextView , лежащая внутри этого layout . Второй параметр пропустим. Затем меняем свойство видимости у text , что запускает процесс анимации.

TransitionManager.beginDelayedTransition(transitionsContainer); visible = !visible; text.setVisibility(visible ? View.VISIBLE: View.GONE);

Результат:

Так как мы не указали 2й параметр в beginDelayedTransition , к text была применена AutoTransition . Он включает в себя анимации Fade и ChangeBounds. Рассмотрим пример с типом анимации Slide . Сделаем так, чтобы текст ускользил за пределы экрана и исчез. Для этого укажем вторым параметром в методе beginDelayedTransition наш тип Slide и укажем направление скольжения, с помощью Gravity . Затем изменим свойство видимости у text.

TransitionManager.beginDelayedTransition(transitionsContainer, new Slide(Gravity.RIGHT)); visible = !visible; text.setVisibility(visible ? View.VISIBLE: View.GONE);

Результат:

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

MToRightAnimation = !mToRightAnimation; Transition transition = new ChangeBounds(); transition.setDuration(mToRightAnimation ? 700: 300); transition.setInterpolator(mToRightAnimation ? new FastOutSlowInInterpolator() : new AccelerateInterpolator()); transition.setStartDelay(mToRightAnimation ? 0: 500);

Затем в вызове метода beginDelayedTransition укажем 2м параметром определённую нами анимацию и определим изменение Layout параметров у нашей кнопки.

TransitionManager.beginDelayedTransition(transitionsContainer, transition); FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)button.getLayoutParams(); params.gravity = mToRightAnimation ? (Gravity.RIGHT | Gravity.TOP) : (Gravity.LEFT | Gravity.TOP); button.setLayoutParams(params);

Результат:

Рассмотрим ещё одну интересную анимацию, которую можно сделать, определив у ChangeBounds движение по пути методом setPathMotion .

TransitionManager.beginDelayedTransition(transitionsContainer, new ChangeBounds().setPathMotion(new ArcMotion()).setDuration(500)); mToRightAnimation = !mToRightAnimation; FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) button.getLayoutParams(); params.gravity = mToRightAnimation ? (Gravity.RIGHT | Gravity.BOTTOM) : (Gravity.LEFT | Gravity.TOP); button.setLayoutParams(params);

Результат:

Реализуем анимацию изменения цвета. Для этого нам нужна анимация типа Recolor .

TransitionManager.beginDelayedTransition(transitionsContainer, new Recolor()); mColorsInverted = !mColorsInverted; button.setTextColor(getResources().getColor(!mColorsInverted ? R.color.second_accent: R.color.accent)); button.setBackgroundDrawable(new ColorDrawable(getResources().getColor(!mColorsInverted ? R.color.accent: R.color.second_accent)));

Результат:

Реализуем анимацию поворота иконки. Для этого будем использовать анимацию типа Rotate .

TransitionManager.beginDelayedTransition(transitionsContainer, new Rotate()); mRotated = !mRotated; icon.setRotation(mRotated ? 135: 0);

Результат:

В общем, как вы могли убедится выше, большинство анимаций имеют «говорящие» имена и понять их предназначение не трудно.

Посмотрим, как применить несколько анимаций к одному объекту. Реализуем исчезновения текста одновременно с его уменьшением. Определим множество анимацию TransitionSet , включив в него анимацию Scale с коэффициентом уменьшение 0.7 и анимацию Fade .

TransitionSet set = new TransitionSet() .addTransition(new Scale(0.7f)) .addTransition(new Fade()) .setInterpolator(visible ? new LinearOutSlowInInterpolator() : new FastOutLinearInInterpolator()); TransitionManager.beginDelayedTransition(transitionsContainer, set); text2.setVisibility(visible ? View.VISIBLE: View.INVISIBLE);

Результат:

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

Это делается с помощью методов класса Transition: addTarget - добавить цель, removeTarget - удаление раннее выбранной цели, excludeTarget - метод, с помощью которого можно применять действие «для всех, кроме» и excludeChildren с помощью которого можно исключать детей некоторого layout.

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

TransitionManager.beginDelayedTransition(mRecyclerView, new Explode()); // remove all views from Recycler View mRecyclerView.setAdapter(null);

Результат:

А теперь сделаем так, чтобы на ячейку, на которую мы нажали не действовала анимацию Explode , а действовала анимация Fade . Для этого создадим множество из 2х анимаций Explode и Fade . Для Explode с помощью метода excludeTarget исключим нажатую ячейку, а для метода Fade методом addTarget добавим только одну цель - нашу нажатую ячейку.

Final Rect viewRect = new Rect(); clickedView.getGlobalVisibleRect(viewRect); TransitionSet set = new TransitionSet() .addTransition(new Explode().setEpicenterCallback(new Transition.EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { return viewRect; } }) .excludeTarget(clickedView, true)) .addTransition(new Fade().addTarget(clickedView)); TransitionManager.beginDelayedTransition(mRecyclerView, set);

Результат:

Теперь нажатая ячейка остаётся на месте и плавно исчезает, а остальные ячейки разбегаются.

Надеюсь, было полезно. Спасибо за внимание!