Pull to refresh

Анимации c помощью Transitions API

Reading time 11 min
Views 70K
Привет!

На хабре ещё не была освещена тема Transitions API для анимаций, которые появились в Android начиная с 4.4 (KitKat) и продолжили свое развитие в 5.0 (Lollipop). В своей статье я расскажу о том, как упростить работу с анимациями с их использованием и как применять их на любом устройстве с версией Android 4.0 и выше.



Вместе с Android 4.4 был представлен новый механизм анимации изменений в layout. Ещё в версии 4.0 появился первый вариант решения этой проблемы — флаг animateLayoutChange для ViewGroup. Но даже с вызовом метода getLayoutTransition() и изменением его параметров он был недостаточно гибок и не давал полного контроля над тем, как будет анимироваться наше изменение (transition).

KitKat Transition API приносит нам понятия сцены — Scene и некоторого изменения между сценами — Transition. В добавок к ним вводим понятие Scene root для определения корневого layout, внутри которого будут происходить изменения сцен. А сама сцена это некоторый враппер над ViewGroup, который описывает конкретное состояние его самого и всех содержащихся в нем объектов View.

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

Начнем с самого простого варианта использования Transitions API.
Представим, что текущее состояние нашего layout это первая сцена. Допустим у нас просто изображен квадрат.
Опишем layout через xml:

<FrameLayout
    android:id="@+id/container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/transition_square"
        android:layout_width="@dimen/square_size_normal"
        android:layout_height="@dimen/square_size_normal"
        android:background="@color/square_bg"/>

</FrameLayout>

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

    ViewGroup sceneRoot = (ViewGroup) findViewById(R.id.container);         
    View square = mSceneRoot.findViewById(R.id.transition_square);   
    int newSquareSize = getResources().getDimensionPixelSize(R.dimen.square_size_expanded);

    // вызываем метод, говорящий о том, что мы хотим анимировать следующие изменения внутри sceneRoot
    TransitionManager.beginDelayedTransition(sceneRoot);                             

    // и применим сами изменения                     
    ViewGroup.LayoutParams params = square.getLayoutParams();                        
    params.width = newSquareSize;                                                          
    params.height = newSquareSize;                                                         
    square.setLayoutParams(params);                                                  

Результат:
image

Неплохо, анимация всего одной строкой. Причем таких изменений внутри layout может быть любое количество.
Теперь попробуем настроить некоторые параметры анимации. Для этого нужно указать конкретный Transition, который мы будем выполнять. Метод beginDelayedTransition может принимать вторым параметром любого наследника класса Transition. Именно про них мы сейчас и поговорим.

Простые типы Transition


  • ChangeBounds. Именно он выполнялся в первом примере. Это Transition, который отвечает за изменение координат View внутри layout и его размеров.
  • Fade. Объединяет в себе всем известные анимации fade in и fade out. Он является наследником класса Visibility, который придуман для тех Transition, которые выполняются в тот момент, когда View была видна в первой сцене, но должна исчезнуть при переходе во вторую, или же, наоборот, появиться.
  • TransitionSet. Тоже является наследником Transition, однако представляет из себя всего лишь набор какого-либо количества других Transition, которые выполняются по очереди или одновременно. Очередность устанавливается с помощью метода setOrdering.
  • AutoTransition. Он является таким TransitionSet, который всего лишь является набором из последовательно выполняющихся Fade с параметром Fade.OUT, ChangeBounds и Fade с параметром Fade.IN. Таким образом, сначала с fade out эффектом прячутся все View, которых нет во второй сцене, потом происходит перемещение и изменение размеров для View, у которых эти параметры изменились, и, наконец, с fade in появлятся новые элементы из второй сцены. Именно AutoTransition выполняется, когда мы, как и в первом примере, не указали никакого конкретного Transition методу beginDelayedTransition.

Сцены


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

Опишем layout нашего Activity так, чтобы в нем была кнопка для смены сцен и FrameLayout, который будет нашим Scene root. Внутрь его сразу ставим layout, который является первой сценой, но вынесем его в отдельный файл.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="16dp">

    <Button
        android:id="@+id/btn_change_scene"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Change scene"
        android:layout_gravity="center_horizontal" />

    <FrameLayout
        android:id="@+id/scene_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include layout="@layout/scene1"/>

    </FrameLayout>

</LinearLayout>

Содержимое scene1. Поместим туда всё тот же квадрат, но в левый верхний угол:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    android:id="@+id/container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/transition_square"
        android:layout_width="@dimen/square_size_normal"
        android:layout_height="@dimen/square_size_normal"
        android:background="@color/square_bg"
        android:layout_gravity="top|left"/>

</FrameLayout>

И создадим файл scene2. Квадрат переместим в правый верхний угол и увеличим в размерах. Также, добавим TextView, которого не было в первой сцене.

<FrameLayout
    android:id="@+id/container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/transition_square"
        android:layout_width="@dimen/square_size_expanded"
        android:layout_height="@dimen/square_size_expanded"
        android:background="@color/square_bg"
        android:layout_gravity="top|right"/>

    <TextView
        android:id="@+id/transition_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="This text fades in."
        android:textAppearance="?android:attr/textAppearanceLarge"/>/>

</FrameLayout>

В onCreate у Activity опишем:

    ViewGroup sceneRoot = (ViewGroup) findViewById(R.id.scene_root);                   
                                                                                       
    // You can also inflate a generate a Scene from a layout resource file.            
    final Scene scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.scene3, this);    
                                                                                       
    findViewById(R.id.btn_change_scene).setOnClickListener(new View.OnClickListener() {
        @Override                                                                      
        public void onClick(View v) {                                  
            // опишем свой аналог AutoTransition                
            TransitionSet set = new TransitionSet();                                   
            set.addTransition(new Fade());                                             
            set.addTransition(new ChangeBounds());          
            // выполняться они будут одновременно                              
            set.setOrdering(TransitionSet.ORDERING_TOGETHER);        
            // уставим свою длительность анимации                                 
            set.setDuration(500);                             
            // и изменим Interpolator                                           
            set.setInterpolator(new AccelerateInterpolator());                         
            TransitionManager.go(scene2, set);                                         
        }                                                                              
    });                                                                                                       

Результат:
image

И о главном. Бэкпорт


Легко понять, почему этим API ещё мало кто пользуется и даже мало кто про него знает. Сейчас то время, когда разработчики только недавно осознали моральное право отказаться от поддержки Android 2.x и большинство новых проектов наконец начинаются с поддержкой minSdk Android 4.0. Но пока ещё мало смысла использовать API, которое будет работать только начиная с 4.4, ведь большинство девайсов ещё на более старых версиях.
Но! Transition API уже можно использовать для Android 4.0+ и везде анимация будет работать одинаково. Для этого я разрабатываю библиотеку, которую назвал Transitions Everywhere. Она является бэкпортом Transition API на более старые версии Android. Для интеграции в свой проект требуется подключить библиотеку как gradle зависимость:

dependencies {
    compile "com.andkulikov:transitionseverywhere:1.7.4"
}

И для всех классов, связанных с этим API, заменить импорт на пакет com.transitionseverywhere.* вместо android.transition.*

О библиотеке


Когда я начал интересоваться этой темой я нашел две похожие библиотеки:
github.com/guerwan/TransitionsBackport
github.com/pardom/TransitionSupportLibrary
Что их объединяет, так это то, что их создатели, видимо, забили на их развитие и обе они не реализовали до конца все, что можно было бы реализовать для обратной совместимости некоторых функциональностей на старых версиях Android. Я взял их за основу и добавил много нового. Различные исправления некоторых некорректных поведений, которые проявлялись, например на версиях до 4.3. К тому же, API моей библиотеки совместимо с версиями Android 2.2+, но на версиях до 4.0 смена сцен(layout) будет выполняться без анимации. Также, я стараюсь идти в ногу со временем. Не так давно выпустили новую версию Android 5.0 (Lollipop) и код всего пакета transitions API в ней, также, во многом изменился. Я смержил все изменения в свою библиотеку.

Что нового в API из 5.0


  • Появилась возможность задавать TransitionName для View. Помечая одним и тем же именем View в первой и во второй сцене мы говорим, что переход именно между ними нужно анимировать. Это удобно, потому что раньше нам необходимо было для двух сцен, описанных в разных xml файлах указывать одинаковые id для таких View. В 5.0 transitionName можно указывать как атрибут в описании через xml, или вызывая метод setTransitionName у любого наследника View. В своей библиотеке я эту функциональность частично портировал таким образом: я предлагаю вызывать статический метод TransitionManager.setTransitionName(View v, String transitionName) вместо аналогичного у самого наследника View и отказаться от установки transitionName через xml.

  • Slide. Новый Transition, который является наследником Visibility, как и Fade. C помощью него появляющаяся в сцене View может «прибегать» из выбранного края. Пример с new Slide(Gravity.LEFT)):
    image

  • Explode. Во многом похож на Slide, но View будет выбегать или из некоторого направления, который задается с помощью так называемого эпицентра Transition(см. метод setEpicenterCallback) или же из случайного угла, если этот эпицентр не задан.
  • TransitionPropagation. Описывает задержку начала анимации. Например, при установке CircularPropagation чем View ближе к установленному эпицентру, тем раньше она начинает анимироваться. Задается для Transition через параметр setPropagation.
    Пример удаления всех View из FrameLayout с Explode Transition и установленным CircularPropagation(эпицентром ставится точка тапа на экран):


  • ChangeImageTransform. Transition, который анимирует матричный переход изображения внутри ImageView. С помощью него можно плавно изменять размеры и scaleType изображений. Разберем на примере. Барышню слева мы подвергли операциям ChangeBounds и ChangeImageTransform, тогда как девушке справа достался только ChangeBounds. Невооруженным взглядом заметно как правая «дергается» в начале анимации:
    image

  • ChangeClipBounds. Плавное изменение параметра clipBounds у View. Но! Так как метод setClipBounds появился только в Android 4.3, то и изменять этот параметр и его же анимировать мы сможем только начиная с этой версии. Пример:
    image

  • Path (Curved) motion. Появилась возможность устанавливать для любого Transition правило расчета координат параметров, состоящих из двух координат с помощью метода setPathMotion. например, при изменении координат x и y, View может прибежать к новым координатам не по прямой, а по дуге. Чтобы показать пример того, как это выглядит просто приведу ссылку на неплохую статью об этом.

  • Немного о том, что ещё появилось нового, но что пока не удалось портировать.
    Activity Transition, хорошо разрекламированный гуглом в анонсах Lollipop и Material Design. Видимо, много логики спрятано в самих Activity, даже нужно устанавливать специальный флаг. Так что, напрямую это портировать не так просто. Та же ситуация и с Fragment Transition.
    Новый Transition под именем ChangeTransform — аналог ChangeImageTransform, но применяет матричное преобразование для любых View.

Targets


Для любого Transition можно задать множество элементов — целей, к которым оно будет применяться.
Это делается с помощью методов класса Transition:
  • addTarget(View target) — явно задать View, для которой нужно применять действие
  • addTarget(int targetId) — id View
  • addTarget(String targetName) — применять только для тех View для которых было указано соответствующее имя через TransitionManager.setTransitionName
  • addTarget(Class targetType) — применять только для View конкретного типа. например android.widget.TextView.class

Методы удаления ранее добавленных целей:
  • removeTarget(View target)
  • removeTarget(int targetId)
  • removeTarget(String targetName)
  • removeTarget(Class target)

Методы, с помощью которых можно применять действие «для всех, кроме»:
  • excludeTarget(View target, boolean exclude)
  • excludeTarget(int targetId, boolean exclude)
  • excludeTarget(Class type, boolean exclude)
  • excludeTarget(Class type, boolean exclude)

Методы, с помощью которых можно исключать всех детей некого layout:
  • excludeChildren(View target, boolean exclude)
  • excludeChildren(int targetId, boolean exclude)
  • excludeChildren(Class type, boolean exclude)

Задаем Transition через xml


Сами Transition можно загружать из xml файла. Его кладем в папку res/anim. Пример

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:app="http://schemas.android.com/apk/res-auto"
               app:transitionOrdering="together"
               app:duration="400">
    <changeBounds/>
    <changeImageTransform/>
    <fade
        app:fadingMode="fade_in"
        app:startDelay="200">
        <targets>
            <target app:targetId="@id/transition_title"/>
        </targets>
    </fade>
</transitionSet>

Мы создали набор операций TransitionSet, состоящий из трех других Transition. Заодно применили тут ранее не рассмотренное свойство startDelay — время задержки перед началом выполнения анимации. Аналог для вызова из кода — Transition.setStartDelay(long startDelay).

Можно загружать из xml файла целый TransitionManager, в котором описывать разные правила для смен сцен в зависимости от того, в каком порядке они происходят. Простой пример:

<?xml version="1.0" encoding="utf-8"?>
<transitionManager xmlns:app="http://schemas.android.com/apk/res-auto">
    <transition
        app:toScene="@layout/scene3"
        app:transition="@anim/scene3_transition"/>
</transitionManager>

Примечание
Два рассмотренных выше примера подправлены для работы с моей библиотекой Transitions Everywhere. Если использовать системные Transition API, то сами xml файлы нужно класть в директорию res/transition и для всех атрибутов необходимо использовать namespace android: вместо app:

Кастомные Transitions


Чтобы ещё больше убедиться какая крутая штука эти Transitions, давайте напишем свой собственный Transition.
Допустим, мы хотим анимировано изменять размер шрифта у TextView. (Как подсказал в комментариях bimeg, этот пример немного оторван от реальности, потому что не стоит анимировать свойства, каждое изменение которых во время анимации вызывает requestLayout())
От нас требуется переопределить методы captureStartValues, captureEndValues и createAnimator. Первые два отвечают за снятие значений необходимых параметров View до и после смены сцен. А метод createAnimator, собственно, создает аниматор для этого изменения параметров.

private class TextSizeTransition extends Transition {

    // имя параметра, для сохранения его в HashMap значений 
    private static final String PROPNAME_TEXT_SIZE = "textSizeTransition:textSize";

    private void captureValues(TransitionValues transitionValues) {
        // нас интересуют только TextView
        if (transitionValues.view instanceof TextView) {
            TextView textView = ((TextView) transitionValues.view);
            // сохраняем значение размера шрифта, заранее приведя его в dip
            transitionValues.values.put(PROPNAME_TEXT_SIZE, textView.getTextSize() /
                    getResources().getDisplayMetrics().density);
        }
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
                                   TransitionValues endValues) {
        // применяем аниматор только тогда, когда у нас были сняты значения и до и после
        // смены сцен и мы имеем дело с TextView.
        // endValues будет равно null, например, если во второй сцене эта View исчезает
        if (startValues != null && endValues != null && endValues.view instanceof TextView) {
            TextView textView = (TextView) endValues.view;
            // берем значения
            float start = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
            float end = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
            if (start != end) {
                // сначала возвращаем во View изначальный размер
                textView.setTextSize(start);
                // и анимируем его до требуемого
                return ObjectAnimator.ofFloat(textView, "textSize", start, end);
            }
        }
        return null;
    }
}

Результат:
image

PS


Добавим плавные переходы в Android!
Ещё раз ссылка на библиотеку:
github.com/andkulikov/transitions-everywhere
Tags:
Hubs:
+28
Comments 20
Comments Comments 20

Articles