Лекция Яндекса: Advanced UI, часть вторая
Это вторая часть лекции Дмитрия Свирихина — разработчика из команды мобильной Яндекс.Почты.
— Мы с вами продолжаем рассматривать типичные проблемы Android-разработчика и способы их решения. Мы уже рассмотрели, как решить проблему неконсистентности UI у нас в приложении, проблемы, которые могут возникнуть при взаимодействии с клавиатурой, и проблемы потери state, а также узнали, как мы можем эффективно применять кастомные view. Всю вторую часть мы посвятим ещё одной проблеме — она называется «недостаточная интерактивность». Посмотрим, как мы можем сделать наше приложение более интерактивным и понятным для пользователя.
Сначала я приведу пример недостаточной интерактивности. Вернемся к приложению с основными тезисами доклада, который я вам сейчас рассказываю. Вот пользователь его открывает, тапает на вторую часть, и что-то происходит, абсолютно непонятное для пользователя.
Еще раз посмотрим. Тапает на вторую часть, и бах, внезапно что-то произошло. Пользователю понадобится некоторое время, чтобы понять, осознать, что вообще сейчас случилось, как я могу опять вернуться и посмотреть первую часть. Поэтому мы можем подсказать этому пользователю нужный порядок действий с помощью анимации, примерно таким образом.
Оп, вторая часть у нас раскрылась, а первая уехала наверх. При этом пользователь понимает, что если мы затем проскроллим наверх, мы опять вернемся к первой части, и это для него за счет такой анимации становится очевидным.
Как вы уже догадались, мы сейчас с вами поговорим про анимации, про то, какие в 2017 году существуют способы создания анимации, и какими из них нам в реальности действительно можно и нужно пользоваться.
Вообще способов создать что-то движущееся в 2017 году очень много. Вот такой список. И вы уже, возможно, в ваших лекциях частично каких-то из них касались. Но сейчас мы совершим более глубокое погружение в каждый из них, и поймем, какие из них действительно могут нам понадобиться.
Начнем мы с View animation. Это основной способ анимации view, который у нас был до Android с версией 2.3. И тогда в 2009–2010 годах никто особо не задумывался вообще об анимациях, лишь бы что-то там крутилось. Поэтому у View animation есть абсолютно маленький набор тех изменений, которые мы можем сделать с помощью анимации. Но у него есть один большой минус — то, что он меняет всего лишь представление view на этапе отрисовки. То есть если мы сделаем какую-то кнопку, которая у нас, скажем, вылетает за пределы экрана, если в процессе анимации вы нажмете на то место, где кнопка находилась изначально, то это будет рассчитываться как клик на эту кнопку. Поэтому мы можем сказать, что данный способ анимации устарел, пользоваться им в 2017 году не стоит, поэтому не будем его подробно рассматривать, сразу же перейдем к следующему.
Drawable animation — это такой способ создания анимации, чтобы его понять, проще всего посмотреть на XML, который его представляет. По сути, это обычная покадровая анимация, и больше ничего. И должна использоваться только для каких-то очень сложных случаев, которые мы не можем сделать какими-то другими методами. Данная анимация довольно сложна для системы, потому что нам нужно новый drawable грузить, возможно, даже на каждый кадр. Поэтому использовать ее тоже стоит только в крайних случаях.
И мы переходим к самому интересному — ValueAnimator, который появился у нас в третьей версии Android. Это базовый движок для анимации. Плюс его состоит в том, что он абсолютно никак не привязан к view. К ValueAnimator есть своя собственная иерархия вызовов в процессе формирования кадра. И что нам нужно, чтобы что-то проанимировать с помощью ValueAnimator? Нам нужно указать начальное значение, конечное значение, указать Listener, в котором нам будут приходить промежуточные значения, и мы сможем их применять для каких-то из наших view.
Вообще в клиентской разработке ValueAnimator используется не очень часто, но мы все-таки посмотрим, какой код нам нужно написать, чтобы он у нас заработал.
Смотреть мы будем на самой простой и скучной анимации, которую можно себе только представить — это crossfade. Хоть она простая и скучная, но, тем не менее, она хорошо выполняет свою работу, когда нужно, когда нужно сгладить какие-то углы при смене экрана.
Итак, давайте посмотрим сразу код, который нам нужен от ValueAnimator для того, чтобы у нас заработал этот crossfade. Нам нужно создать два ValueAnimator, соответственно, проанимировать значение из нуля в единицу и из единицы в ноль. Затем задать для каждого из них UpdateListener, в котором мы будем из аниматора получать текущее значение анимации, прямо зайти с помощью метода setAlpha в наши view. Также нам понадобится AnimatorSet, который определяет множество аниматоров, которые могут выполняться либо последовательно, либо параллельно. Мы это сами определяем. В нашем случае они выполняются одновременно, что называется, метод playTogether.
Также мы задаем продолжительность этой анимации, и некоторые listeners, которые у нас вызовутся, методы, которые у нас вызовутся перед стартом анимации и после ее окончания. Соответственно, перед стартом анимации мы сделаем видимой ту view, которая у нас должна появиться. После окончания анимации мы скроем view, которая у нас должна, соответственно, исчезнуть.
А теперь давайте рассмотрим, как вообще работает движок аниматора на примере анимации целого числа от 0 до 255 с помощью ValueAnimator.ofint.
Время в Animator представляется как вещественное значение от 0 до 1. То есть если брать в учет, что кадр в Android должен у нас отрисовываться за 16 и 2/3 мс, и, допустим, у нас анимация будет продолжительностью 167 мс, то данная анимация должна выполниться за 10 фреймов. То есть по 1/10 от общего времени, то, как она представляется в движке, для каждого фрейма.
Давайте посмотрим, как у нас будет выглядеть расчет анимации для четвертого фрейма, то есть со значением времени аниматора 0.4.
Сначала значение 0.4 попадает в TimeInterpolator — это некоторая функция, которая определяет, с какой скоростью мы хотим, чтобы у нас происходила анимация. Например, если мы хотим, чтобы скорость анимации у нас увеличивалась, мы можем использовать, как пример, AccelerateInterpolator. Он определяет возрастающую функцию обычную квадратичную, и еще стоит заметить, что TimeInterpolator должен на вход получать какое-то значение от 0 до 1, и, соответственно, его же и возвращать.
Итак, TimeInterpolator в данном случае возвратит нам значение 0,16, это будем называть интерполируемым временем, и оно уже подается на вход в TypeEvaluator. TypeEvaluator определяет, как у нас должен изменяться объект в процессе анимации относительно времени. То есть, например, в случае анимации простого int здесь все рассчитывается очень просто: мы берем конечное значение, вычитаем из него начальное, и умножаем на время. В нашем случае это 0.16. Получившееся значение 40 возвращается в Animator, сетится в Animator, и после этого этот Animator прокидывается в UpdateListener. Мы можем из него получить все что нам нужно — это анимируемые значения — и применить какую-то из view. Это было у нас сделано в примере кода.
Самое главное, что здесь стоит понимать, что TimeInterpolator и TypeEvaluator мы можем подменять и делать ровно такими, какими нам это нужно в каждом конкретном случае.
Давайте сначала рассмотрим, какие в системе есть TimeInterpolators.
Для начала рассмотрим самые классические. Вот их три, которые в основном и чаще всего используются — это AccelerateInterpolator, который определяет функцию с увеличивающейся скоростью. Он, как правило, должен использоваться для элементов, которые у нас собираются пропасть с экрана. Если посмотреть на желтый мячик, он вылетает за пределы экрана.
Почему здесь должна быть функция с увеличивающейся скоростью? Чтобы бо́льшую часть времени этот элемент просто-напросто провел на экране, чтобы пользователь его точно заметил. Если бы мы для пропадающего элемента использовали, например, DecelerateInterpolator, который, наоборот, использует повышающуюся скорость, то user может этого даже и не заметить.
Соответственно, DecelerateInterpolator (синий мячик) мы должны использовать для элементов, которые у нас на экране появляются. И AccelerateDecelerateInterpolator определяет функцию, скорость которой сначала увеличивается, а потом, соответственно, понижается. Он должен использоваться для анимации, которая связана с view, которая изменяется просто на экране, меняет свои размеры или положения, и остается при этом на экране.
Также в Android 5 появились такие модерновые интерполяторы, которые абсолютно аналогичны тем, которые мы сейчас уже рассмотрели. Считается, что они работают более плавно и естественно. Действительно, выглядят несколько симпатичнее, и мы можем использовать их, кстати, не только начиная с Android 5, а они присутствуют в Support библиотеке, то есть мы можем использовать их абсолютно на любых версиях. И кажется, что для текущих проектов данные инртерполяторы являются более предпочтительными.
Теперь вернемся к нашему коду. Оказывается, мы еще в прошлый раз его не написали, эту огромную пачку кода, тут еще недоставало интерполяторов.
Что же до TypeEvaluators, как правило, вручную их нам задавать приходится нечасто, потому что мы анимируем чаще всего какие-то простые значения, либо это просто какие-то числа, либо целые, либо вещественные, и там интерполятор задавать не требуется, аниматор сам все это осознает. Но, тем не менее, если какая-то отрисовка вашей кастомной view будет зависеть от каких-то сложных объектов, например, от Point или от Rect, вы можете использовать также предопределенные в системе PointFEvaluator или RectEvaluator для того, чтобы проанимировать эти значения.
Также кастомный TypeEvaluator может использоваться для какого-то нетипичного изменения примитивов. Например, когда мы анимируем цвет.
Давайте вспомним вообще, что такое цвет. Это просто обычный integer, который по 2 байта задает цвет каждого канала: alpha, red, green и blue. Если мы будем анимировать его как обычный int, у нас получится не красивый переход из одного цвета в другой, а светофор или какая-нибудь радуга.
Также допустимо создание собственных TypeEvaluators, этому никто не может помешать.
Давайте рассмотрим пример реализации типичного TypeEvaluator, например, ArgbEvaluator. Он в системе существует, поэтому нам его реализовывать не нужно, но он может служить отличной иллюстрацией.
Интерфейс TypeEvaluator следующий. Там есть всего один метод Evaluate, в качестве параметра в него передаются fraction — это интерполированное время анимации. И стартовое и конечное значения.
Что нам нужно сделать? Нам нужно достать из стартового и конечного значения цвет каждого канала. Это мы делаем из стартового, это из конечного.
И затем на основе fraction, который будет иметь значение от 0 до 1, найти значение, как будет выглядеть цвет в текущем значении для анимации. Вот таким образом его рассчитать.
Перейдем к ObjectAnimator. И здесь вы можете мне задать такой вопрос:, а зачем мы вообще рассматривали, как работает ValueAnimator, если я сказал, что он в клиентской разработке почти не используется? Да все дело в том, что ObjectAnimator является прямым расширением ValueAnimator, и все, что я сейчас сказал про ValueAnimatorбет верно и для него — для ObjectAnimator, кроме разве что того, что нам не нужно использовать UpdateListener. Вместо этого в ObjectAnimator представлено новое понятие property, которое и инкапсулирует какие-то изменяемые параметры в течение анимации. Мы сейчас разберемся, что это такое.
Давайте снова посмотрим код, как будет выглядеть CrossFade с помощью ObjectAnimators. AnimatorSet у нас здесь сохраняются, но появляются ObjectAnimators уже внутри. И давайте посмотрим, с какими параметрами ObjectAnimator должны создаваться. В качестве первого параметра передается соответственно анимируемый объект, а в качестве второго параметра будет передаваться property, который мы должны анимировать. И дальше изменяемые значения.
Также остается ввод продолжительности анимации.
И — тот же самый Listener, никуда мы от него не денемся, который раздувает нам весь код.
А теперь давайте посмотрим, как внутри выглядит Property. Вручную Property Alpha вам уже реализовывать не нужно, потому что она есть в системе, но по ней можно отлично посмотреть, что это вообще такое, и что содержится вообще в этом Property.
Как можно увидеть, для Property нам нужно переопределить два метода, чтобы он у нас корректно работал. Соответственно, метод setValue, который показывает, как мы должны сетить все новые значения, которые нам вычисляет typeevaluator. Таким образом они будут задаваться в наш анимируемый объект. И метод Get — это когда аниматору необходимо узнать текущее значение анимации, данного свойства. Метод Get может использоваться тогда, когда аниматор хочет получить начальное значение анимации.
Давайте теперь определим, чем отличается ValueAnimator от ObjectAnimator.
Мы уже с вами рассматривали, что то значение, которое мы получаем в итоге для ValueAnimator, оно сетится в Animator и передается в UpdateListener. Как раз ObjectAnimator своим API избавляет нас от этого, он сам, используя Property, сетит все нужные значения.
Отлично, нам плюс — у нас меньше кода.
А теперь давайте разберемся, как мы можем задавать данные Property, какими способами.
В первую очередь, с помощью наследника Property, как и было в примере нашего кода. Но также мы можем задавать это с помощью строки. Например, если у нас будет в качестве анимируемого property задана строка alpha, то в процессе анимации аниматор будет ожидать, что в нашем объекте View заданы такие методы, как setAlpha и getAlpha. Если такие методы в объекте будут отсутствовать, соответственно, мы получим crash.
Также стоит понимать, что когда мы анимируем какие-то наши view с помощью строчки, то мы входим в нашу view с помощью Reflection, и это также дополнительно еще и замедляет у нас процесс работы и изменения нашего объекта в процессе анимации.
Итак, теперь посмотрим, какие Property по умолчанию уже есть в системе, которыми мы можем пользоваться. Это Alpha (прозрачность), это параметры, связанные с позиционированием view на экране, это параметры, связанные с поворотом экрана и масштабированием. И, конечно же, можем сами создавать Property какие хотим, и использовать их для анимации наших view.
Идем дальше. На очереди у нас ViewPorpertyAnimator, он тоже работает на основе ValueAnimator, и главный его плюс, что он предоставляет очень удобный API для анимации каких-то view с простыми property. Он даже может быть незначительно быстрее ObjectAnimator, когда у нас анимируется несколько значений. Но в этом же заключается и его минус — простота. Мы не можем сделать какие-то кастомные атрибуты, которые бы анимировались с помощью ViewPorpertyAnimator. Там есть только самые простые, те же самые Alpha, позиционирование, масштаб и поворот.
Давайте посмотрим, как у нас будет выглядеть метод crossfade с помощью ViewPorpertyAnimator. Это, кстати, единственный код crossfade, который влез в один слайд, что тому хорошо.
Думаю, здесь все понятно. Метод Animate, который вызывается у view, возвращает ViewPorpertyAnimator. Дальше мы его настаиваем, вызываем метод Alpha, показываем конечное значение, которое у нас должно быть у этого view, указываем продолжительность анимации, указываем интерполятор. И здесь можно заметить еще один метод withEndAction, который определяет, какие действия у нас должны произойти после выполнения анимации.
Итак, мы уже с вами рассмотрели довольно немаленькое количество способов, с помощью которых мы можем проанимировать какие-то наши view. Давайте определим, когда и какой нам вообще стоит использовать из них.
C ViewAnimation и DrawableAnimation все примерно должно быть понятно. ViewAnimation стоит использовать никогда, DrawableAnimation только для очень сложных случаев, если мы не можем реально сделать анимацию с помощью других методов. Мы говорим нашему дизайнеру: «А запили-ка мне покадровую анимацию», дизайнер старается, и мы используем ее. А вот самое интересное мы детально рассмотрим.
ValueAnimator в клиентской разработке, наверное, стоит использовать только в тех случаях, когда нам не удается написать Property. Например, к анимируемому объекту мы не можем написать Property из-за того, что у него отсутствует getter, но мы точно знаем, что вызываться он не будет, мы анимируем, начиная от какого-то начального значения до конечного, и нам достаточно просто определить DateListener, и раздавать новое значение, которое получено в процессе анимации. Тогда можно использовать ValueAnimator.
Во всех остальных случаях и для очень сложных или сложных анимаций, когда у нас есть много анимируемых свойств или даже анимируемых объектов, мы должны использовать ObjectAnimator. При этом, скорее всего, для сложных анимаций он у вас будет использоваться внутри AnimatorSet.
И для каких-то совершенно простых анимаций вы можете использовать ViewPropertyAnimator.
Как быть с теми ситуациями, когда нам нужно досрочно завершить анимацию?
У начинающих разработчиков часто бывает такая ошибка. Они запускают какую-то анимации с помощью ViewPropertyAnimator, и затем, когда хотят ее отменить, вызывают метод clearAnimation. Данный метод не сработает, потому что clearAnimation относится только к отмене анимации, которую мы задаем с помощью ViewAnimation, а так как мы с вами договорились, что вы ViewAnimation не используете, он вам не пригодится никогда.
Если же вы стартовали вашу анимацию с помощью ViewPropertyAnimator, то вам нужно отменять его. Также сначала вызываем метод Animate у вашей view, и затем вызываем метод cancel.
Если вы хотите отменить анимацию, которую стартовали с помощью Value или ObjectAnimator, вам нужно хранить на него ссылку, и затем в нужный момент вызвать cancel.
Когда нам может понадобиться отмена текущей анимации, которая у нас в данный момент происходит? В первую очередь, пользователь какими-то своими действиями дал вам знать, что он отменяет текущее действие, для которого вы запустили анимацию. Тогда его можно отменить, и вернуть view в какое начальное состояние. Но есть другая ситуация.
Например, пользователь решил вообще закрыть этот activity, уйти с этого экрана, и если у нас останется работать Animator, то у нас может просто утечь контекст, что всегда не очень хорошо. Поэтому, даже если вам кажется, что в этот раз прокатит, важно нужно хранить ссылку на Value либо ObjectAnimator, должен вас расстроить: хранить ссылку на Animator нужно практически всегда, и в методах onStop, если это activity или fragment, или в методе onDetachFromWindow, если мы говорим про view, нужно отменять текущую анимацию.
В лекции про view вам упоминали, что методы Measure/Layout, которые вызываются для view в процессе анимации это плохо. Давайте же разберемся, почему это плохо. Для этого опять-таки вспомним, что кадр в Android у нас отрисовывается за 16 2/3 мс, и за это время что у нас должно успеть выполниться? У нас должен успеть обработаться пользовательский ввод, у нас должны отработать все аниматоры, у нас должен пройти layout pass, то есть вызов методов Measure и Layout, если он был инициирован, у нас должна пройти отрисовка, и даже рендеринг у нас тоже должен успеть выполниться за это время. Соответственно, если в процессе анимации случилось так, что у вас вызовется метод Measure и Layout, произойдет весь этот процесс, то есть немаленькая вероятность того, что фрейм у вас просто будет потерян, он не успеет отрисоваться за нужное время. Думаю, все с этим согласятся, что количество потерянных фреймов напрямую коррелирует с тем, как пользователь относится к вашему приложению, не в лучшую сторону, кстати говоря.
И, кстати, это не единственная проблема. Если бы это было единственной проблемой, почему Measure и Layout во время анимации — это зло, то, может быть, было все не так плохо.
Что еще может произойти? Может произойти такая некрасивая анимация. Если вдруг мы решили анимировать изменение размера view с помощью такого сета ObjectAnimators, анимируя значение Top и Bottom, которые подразумевают, что у нас будут вызываться методы set Top и Bottom для анимации, то может произойти следующее. У нас анимация начнется, начнет раскрываться наш элемент, затем, если вызовется Layout, он внезапно перейдет в свое конечное состояние, и затем, как ни в чем не бывало, со следующего фрейма опять продолжит анимацию с того места, где он, по сути, остановился.
Из этого мы должны сделать два вывода. В первую очередь, Measure и Layout во время анимации — это зло, а во-вторых что вот таким образом, как представлено, с помощью данного кода нам не стоит анимировать иерархию. Дело в том, что мы не всегда можем контролировать этот процесс — вызов Measure/Layout, — и в действительности много вариантов, когда он может вызваться у нас в процессе какой-то анимации. Например, к нам могут прийти какие-то новые данные, список, и это вообще очень тяжело контролировать, чтобы не вызывался Measure/Layout. Но все-таки не стоит так делать, раз мы не можем это полностью контролировать. Для этого есть другие способы, которые мы рассмотрим далее.
Итак, один из таких способов называется LayoutTransition, который также известен как флаг animateLayoutChanges, который сделает за нас всю грязную работу. Он тоже работает на основе ValueAnimator. И как вообще это работает? Мы задаем некоторый объект LayoutTransition для какого-то ViewGroup, и затем все изменения внутри прямых childs этого ViewGroup, если у них заменилось либо положение, либо видимость, все эти изменения будут анимированы. И еще LayoutTransition решает проблему временного прерывания анимации, как я уже сказал, которую мы рассматривали буквально один слайд назад.
Когда говорят про LayoutTransition, обычно в разных документациях показывают какие-то совершенно простые примеры, когда у нас есть, например, простой LinearLayout, в него задан атрибут animateLayoutChanges, значение true, и затем все эти примеры так простенько добавляют элементы, и действительно, они у нас появляются с анимацией, все просто, хорошо вроде как. Используйте это. Но на деле получается так, что такие плоские иерархии у нас появляются практически никогда. Все наши реальные layout намного сложнее, поэтому давайте посмотрим пример использования LayoutTransition на каком-нибудь более приближенном к реальности примере.
Например, у нас есть такая иерархия. Опять то самое приложение для просмотра основных тезисов текущей лекции. Мы хотим сделать эту анимацию, которую вы сейчас видите. По тапу на какой-нибудь заголовок у нас, во-первых, будет изменяться видимость текстового поля, в котором описаны все тезисы лекций, и во-вторых, будет изменяться размер контейнера, внутри которого она находится.
Давайте начнем с изменения размера. Что нам для этого нужно сделать?
Создать объект LayoutTransition, указать, что нам нужен от LayoutTransition тип Changing. Это значит, что все childs этого layout, в который мы зададим LayoutTransition, если у них изменился размер, все это должно быть анимировано. И задаем его в наш корневой LinearLayout, в котором у нас находятся именно эти LinearLayouts с текстами. И тогда любые изменения, которые изменят размер тех вложенных LinearLayouts, они запустят анимацию, и все будет выглядеть вот так.
В целом уже неплохо, но есть визуальный недостаток, он особенно проявляется, когда у нас элементы на экране скрываются. Сейчас, посмотрите, резко пропадает, текст не очень красиво выглядит. Поэтому, раз все так хорошо вышло с корневым layout, может, у нас и с текстом также хорошо получится? Давайте добавим по LayoutTransition в каждый вложенный LinearLayout вот таким образом.
Кода немного. Что может произойти плохого?
И вот что у нас получается. Кажется, что все выглядит неплохо до каких-то определенных пор. Сейчас особенно видно. Происходит что-то вообще непонятное, что-то очень странное. И не получилось, к сожалению. Сейчас при закрытии первой части особенно будет видно, такой шлейф остается за этим элементом. Почему такое происходит? Давайте разберемся.
Когда мы задаем LayoutTransition для какой-то иерархии, нужно понимать, что анимируются не только его прямые childs, но и сам контейнер, для которого мы задаем LayoutTransition, он также будет изменяться, если его размеры изменились, также будет анимироваться. И все это происходит, потому что у нас отрабатывает свойство, которое задано по умолчанию — setAnimateParentHierarchy, по умолчанию оно задано в значении true.
В данном случае, когда мы задавали для корневого LinearLayout, ничего плохого у нас не происходило, потому что он не изменялся, соответственно, анимация для него не запускалась.
А когда мы стали задавать для вложенных LinearLayouts, то учитывая, что свойства AnimateParentHierarchy у нас изначально в состоянии true, у нас на некоторые view действовало сразу несколько LayoutTransition, соответственно, для них создавалось несколько аниматоров. Именно это и создавало такой странный эффект, так как аниматоры, которые создаются внутри LayoutTransition, у них есть некоторые отношения друг к другу, некоторые из них могут вызываться с некоторым delay, и чтобы решить данную проблему, мы можем просто вызвать данный метод setAnimateParentHierarchy со значением false, и тогда у нас будут анимироваться непосредственно только текстовые поля.
Так это будет выглядеть в коде. А вот так это будет выглядеть в результате в нашем layout. Все у нас наконец-то получилось, мы добились своей цели.
Давайте теперь рассмотрим, какие вообще виды LayoutTransition у нас существуют на текущий момент. Это Appearing и Disappearing — добавление view в иерархию либо изменение ее видимости.
Также есть Changeappearing и Changedisappearing — это именно те виды transitions, которые мы отменяли в нашем случае. Это когда parent изменяется под непреодолимой силой действия childs этого parent.
И также существует еще один вид Changing, мы его тоже использовали, он анимирует изменение размеров view.
Думаю, вы уже поняли, что LayoutTransition в целом подходит для того, чтобы использовать его в каких-то совершенно простых случаях. Стоит нашей иерархии стать сложнее, если в ней нужно много всего анимировать, нам для каждого ViewGroup нужно будет отдельно задавать LayoutTransition, что не очень приятно, что требует очень много кода. В этом заключается его минус — он анимирует только прямых детей указанного контейнера.
Также у него недостаточно классов анимации. По сути, их всего три: появление, исчезновение view, изменение размеров. Больше ничего нет. Например, матрицу ImageView мы не сможем проанимировать, к сожалению.
И еще у него есть ряд ограничений, связанных с изменением сразу нескольких view в иерархии. Мы этого не рассматривали, но, тем не менее, я просто скажу, что может произойти. Если в одной иерархии мы захотим и добавить, и удалить view, то анимация запустится только для последнего действия, то есть для удаления view, и это будет выглядеть явно некрасиво.
Кудесники из Android подумали-подумали, все эти минусы как-то проанализировали, и сделали такую замечательную штуку, как Transition Framework.
У Transition Framework отсутствует большинство из тех минусов, которые мы сейчас рассмотрели для LayoutTransition. В первую очередь, он вводит такие понятия, как Transition, который инкапсулирует целый класс анимаций, также он анимирует всю иерархию внутри контейнера, которую мы скажем. Не только прямых childs, а вообще всю иерархию. И ровно так же, как и LayoutTransition решает проблему временного прерывания во время анимации при изменении размеров. Но не могут быть только одни плюсы. Есть у Transition Framework и минусы. Самый главный минус, что он работает только Android 4.4. Базовые transitions появились только с этой версии Android. А большинство самых модерновых и интересных появились вместе с материальным дизайном в Android 5.
Но давайте не будем о грустном, и посмотрим, какой код нам нужно написать.
Давайте мысленно вспомним этот пример, в котором у нас изменялся размер view, которые раскрывались, и мы могли посмотреть расписание лекций.
Что нам нужно сделать по клику? Нам нужно создать TransitionSet, внутрь него поместить два transition. ChangeBounds — это Transition, который будет анимировать у нас все изменения размеров layout, позиций layout и так далее.
И Fade, который будет анимировать изменение видимости с помощью Fade.
TransitionSet — это некоторый аналог AnimatorSet, только для Transition.
Затем мы вызываем волшебный метод beginDelayedTransition, указываем в него наш контейнер и сам Transition. И затем все, что у нас будет вызвано после вызова этого метода, и то, что может повлиять на те свойства, на которые у нас завязаны наши Transition, которые мы указали, все это будет анимировано. Вообще все, вся иерархия.
И этого кода достаточно для того, чтобы у нас все работало так, как надо. Не нужно нам никаких прописываний LayoutTransition для которого ViewGroup, все у нас будет с таким простым кодом работать красивенько.
Я вновь напомню, что когда мы задаем BeginDelayedTransition, анимации подлежат все view, которые находятся внутри этого layout, для которого мы его задали.
И какие же transitions у нас определены по умолчанию в системе? Их довольно много. ChangeBounds и Fade мы уже разобрали. Начиная с Android 5 появилось также еще множество связанных с материальным дизайном, и они продолжают появляться по сей день. Но если вам сильно хочется, вы точно также можете реализовать какой-то свой Transition, и использовать его, радовать юзеров вашей красивой анимацией.
Вообще, сделать свой Transition — дело не такое уж и простое, но в документации есть отличный гайд про то, как это сделать. Если вы заинтересуетесь, можете его прочесть. А мы тем временем давайте посмотрим, какие самые интересные Transition у нас есть из тех, что предопределены у нас внутри платформы.
Для начала — Slide. Это еще один способ изменить видимость элементов, только не с помощью Fade, а с помощью таких прилетающих элементов из какой-то части экрана. И, опять-таки, если у нас view удаляется, они будут улетать в какую-то другую сторону, или можно сделать, чтобы они улетали в эту же сторону. Все это настраивается для Transition Slide.
Так вы можете, например, сделать в вашем приложении погоды, которое вы делаете. Это переход к расширенному просмотру погоды. Здесь также используется Slide, для изменения размера солнышка используется в свою очередь TransitionChangeBounds.
Есть еще интересный Transition, называется Explode. Это еще один способ проанимировать изменение видимости, только для какого-то немаленького количества объектов. Раньше он использовался довольно часто, как только появился, но в последнее время его что-то в новых приложениях и не видно. Видимо, мода уже прошла на Explode, но, я думаю, вы согласитесь, выглядит довольно необычно. Эпицентр взрыва точно также можно настраивать с помощью специального API.
ChangeTransform — это еще один Transition, который позволяет нам анимированно менять поворот и масштаб view.
И вот еще один интересный способ проанимировать матрицу ImageView. Transition, который называется ChangeImageTransform. Если вы приглядитесь, то у той маленькой preview-фоточки мы используем один SkillType для ImageView, а для финальной, которая появляется после увеличения, уже другой, то есть данный Transition умеет анимировать матрицу ImageView.
Вообще в данном случае используется не только ChangeImageTransform, но и для изменения размеров самого ImageView используется вместе с ним ChangeBounds.
Я думаю, вы согласитесь, что иногда бывают такие случаи, когда мы точно знаем, что все элементы внутри иерархии нам анимирова