Это вторая часть лекции Дмитрия Свирихина — разработчика из команды мобильной Яндекс.Почты.
— Мы с вами продолжаем рассматривать типичные проблемы 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.
Я думаю, вы согласитесь, что иногда бывают такие случаи, когда мы точно знаем, что все элементы внутри иерархии нам анимировать не нужно. Для этого существует набор специальных фильтров, например, это метод excludeTarget, в который мы можем передать ID, который мы точно знаем, что в данной иерархии нам анимировать не нужно. Мы передаем ID, и в качестве второго параметра true.
Также мы можем передать и сам объект, если мы не хотим, чтобы он использовался в этой анимации. Мы можем не только точечно указывать анимации, которые мы не хотим, чтобы у нас работали, но можем, например, какую-то целую иерархию исключить из анимации, также по ID, либо передав сам экземпляр view. Если же в какое-то время мы вдруг передумали, думаешь: «Нет, все-таки здесь, наверное, нам нужна анимация для этого view», мы можем вызвать этот метод excludeTarget, только уже в качестве второго параметра передать false. Также подходит он и для ID, и прямо для самих view. И работает как для метода excludeTarget, так и для excludeChildren.
У нас может быть обратная ситуация — мы точно знаем, какие view у нас должны проанимироваться, и не делать фильтр, а прямо указать конкретно списком: вот мы хотим, чтобы у нас точно анимировалось view с айдишником root. Если мы вызвали этот метод addTarget, мы тем самым сообщаем Transition, что только эту view нам и нужно анимировать.
Затем мы можем это делать не только через ID, но и передавая саму view. Опять-таки, если через какое-то время мы передумали, то можем вызвать метод removeTarget.
Еще один плюс Transition Framework в том, что мы можем переиспользовать наши Transitions, просто забив internet- в XML. Появляется такая разметка, с помощью которой мы можем указывать, как должны работать наши Transition. Затем с помощью TransitionInflater в Java-коде получать из XML Transition. Тоже довольно хороший вариант для переиспользования Transition.
И еще Transition Framework добавляет для нас функциональность сцен. Мы можем анимироваться из одной сцены в другую, которая создана у нас с помощью некоторых layout.
Какие здесь элементы будут анимироваться? Если у нас в двух XML существуют view с одинаковыми ID, соответственно, по ним Transition Framework будет определять, что эта view должна в итоге превратиться в такую, и происходит анимация. Для этого нужно вызвать метод go, он как раз переведет наш layout из состояния одной сцены в другую.
Также здесь может использоваться не только ID для сопоставления каких-то элементов между сценами, но и так называемый атрибут TransitionName, который появился, насколько помню, с Android 5, и он для Transition Framework является более приоритетным представлением списка анимируемых view, нежели ID.
Проблему использования Transition Framework на старых версиях пробовали решить и сами разработчики Android. Они поняли, что они сделали довольно неплохую штуку для создания сложных анимаций, и попробовали портировать его в виде библиотеки в составе Support Library, и действительно сделали это. Таким образом Fade и ChangeBounds у нас оказываются доступными не только для Android старше версии 4.4, но и для версии с 4.0 и по 4.3. Также была портирована функциональность сцен. Но в текущем виде библиотека должна использоваться с осторожностью. Связано это с тем, что в ней еще довольно много багов, которые, конечно, постепенно фиксятся, но, тем не менее, они все еще есть, и не знаю, умышленно это было сделано или нет, но данная библиотека отсутствует в описании Overview Support Library. Возможно, разработчики сами еще недовольны тем, что получилось, и не добавили ее туда, а возможно, просто забыли, так что вариантов здесь может быть много.
Итак, какой же из способов анимации каких-то глобальных элементов в layout мы можем выбрать: LayoutTransition или Transition Framework? LayoutTransition мы можем использовать, как мы уже с вами определили, для каких-то простых иерархий, где у нас анимируется не такое большое количество элементов, и при этом данный вид анимации будет доступен еще и на старых версиях, начиная с версии 4.0, насколько я помню. Transition Framework, напротив, должен использоваться для всех остальных случаев, для каких-то сложных анимаций и на новых версиях Android.
Казалось бы, мы уже рассмотрели такое огромное количество способов задать анимацию. Казалось бы, бери что хочешь и пользуйся, радуй юзеров, получай от них какие-то лайки, плюшки и так далее, но все еще остаются такие кейсы, которые не покрыты. Например, это могут быть те действия, которые пользователь инициирует сам, например, свайпом или какими-то другими своими действиями.
Вот в качестве примера давайте приведем анимацию, которая у нас следует после свайпа. Если присмотреться, то можно увидеть, что после того, как юзер отпускает палец, у нас происходит такой небольшой лаг, и выглядит это не очень красиво. Терпимо, конечно, но мы знаем, что мы можем лучше.
Давайте разберемся, почему такое происходит. В тот момент, когда пользователь заносит пальцев и спайпает объект до какой-то позиции, обычно все это реализуется очень много. Сама view просто следует за пальцем пользователя. Здесь никакого rocket science нет. Затем, когда пользователь отпускает палец, и мы уже сами запускаем анимацию, обычно здесь применяется ValueAnimator.
Когда мы запускаем ValueAnimator, что мы можем указать? Мы можем указать продолжительность, с которой у нас будет длиться анимацию, интерполятор, который определит ее скорость, но аниматор не будет учитывать тот импульс, который пользователь задал, сдвигая этот объект по экрану. Он не знает о том, с какой скоростью вел пользователь этот объект. Из-за этого получается такие лаги в тот момент, когда пользователь отпускает палец.
И специально для этого кудесники из Android придумали библиотеку для нас, которая называется Dynamic animation, или также известна как Physic-based animation. Как уверяется, они базируются на законах физики, и действительно они идеально подходят для тех анимаций, которые инициируются жестами пользователя.
К плюсам данной библиотеки также можно отнести, что мы можем динамически менять конечное значение в процессе анимации, и на текущий момент данная библиотека включает в себя два вида анимаций, соответственно, FlingAnimation и SpringAnimation. FlingAnimation должен использоваться для тех случаев, когда пользователь своими жестами инициирует какую-то анимацию, например, этот случай со свайпом, когда пользователь отпускает палец, и у нас элемент должен улететь примерно с той же самой скоростью, которой пользователь его вел. А SpringAnimation — это анимация возврата к какому-то начальному значению в виде воображаемой пружинки.
Давайте посмотрим, какие параметры нам нужно задать для того, чтобы Dynamic animation у нас работал.
И чтобы полностью понять его разницу от тех аниматоров, которые мы рассмотрели ранее, давайте вспомним, какими параметрами у нас задается ValueAnimator или ObjectAnimator. Это конечная позиция, продолжительность, интерполятор.
Последних двух у Dynamic animation просто нет.
Вы можете спросить здесь: «Как же так? Как может не быть продолжительности времени анимации?». Все очень просто. Дело в том, что продолжительность анимации определяется тем расстоянием, которое наша анимация должна проделать. Все довольно логично.
В Dynamic animation из общих параметров у нас остается только конечная позиция и начальная скорость. Начальная скорость — это как раз тот параметр, который нам позволяет избавляться от лагов в процессе перехода от действия пользователя до самой анимации.
И FlingAnimation также содержит параметр «трение», который определяет, с какой скоростью у нас эта анимация будет замедляться в итоге. И SpringAnimation определяет жесткость нашей воображаемой пружинки, также определяет начальную скоростью нашей анимации, а чем жестче пружинка, тем, соответственно, меньше будет скорость. И определяет параметр «затухание».
С помощью затухания мы показываем, сколько у нас будет анимация колебаться около начального значения, когда она уже до него дойдет.
Итак, давайте посмотрим, как будет у нас выглядеть свайп с помощью FlingAnimation. Вжух! И все, отлично заработало. Никаких лагов у нас больше нет. Отлично!
Давайте теперь посмотрим на код, как это будет выглядеть в коде.
Когда мы говорим про скайпы, то мы сразу же понимаем, что нам придется обрабатывать пользовательские touch-ивенты. Способов для этого есть много, начиная от кастомной view, заканчивая специальным Listener у RecyclerView, onTouchListener (не помню точно, как он называется), но у RecyclerView есть Listener, который помогает нам следить за тачами.
И затем нам нужно с помощью velocityTracker — это такой объект в Android, который помогает нам вычислять скорость, — мы должны пробросить в него все пользовательские тачи, которые он сделал, и затем, когда юзер поднимает палец, то есть срабатывает у нас ActionUp, либо ActionCancel, когда какая-то другая view забирает на себя фокус, и, соответственно, обработку тачей. В этот момент мы вычисляем скорость с помощью velocityTracker, вызываем метод computeCurrentvelocity, затем создаем FlingAnimation, в нем указываем стартовую скорость, которую мы получаем от velocityTracker, указываем трение, задаем также максимальное значение этой анимации, потому что когда у нас view улетает уже за пределы экрана, нам больше не нужна эта анимация, можно определить тем самым в конец анимации.
И также задаем EndListener — это некоторые действия, которые у нас произойдут уже после окончания анимации.
Кстати, обратите внимание, мы также храним ссылку на эту анимацию, чтобы в случае чего ее отменить в тех же методах onStop.
И, в конце концов, мы начинаем-таки эту анимацию, вызываем метод Start.
Что же делать с анимацией возврата, если пользователь задал недостаточный импульс для view? То есть он начал двигать view, затем в какой-то момент отпустил. Мы определяем, что пользователь постарался недостаточно, возвращаем view в начальное положение.
Давайте посмотрим, как это будет выглядеть с помощью Dynamic animation.
Когда пользователь отпускает палец, у нас также отрабатывает Fling. Когда он заканчивается, мы уже можем запустить SpringAnimation, который вернет нашу view в начальное положение. Так она выглядит, как пружинка возвращается в начальное положение.
В целом данный способ анимации выглядит несколько дольше, но, тем не менее, кажется, он более естественный и более понятен для пользователя.
Давайте посмотрим, как это сделать в коде. (Ссылка на GitHub: bit.ly/2uQPiSY.)
По окончанию FlingAnimation в EndListener мы проверяем, что если текущее значение анимации меньше, чем то конечное значение, которое мы определили, то мы запускаем SpringAnimation, в него мы передаем объект SpringForce, который содержит данные о финальной позиции, также содержит коэффициенты жесткости и затухания для нашей пружинки, и, соответственно, на эту анимацию мы стартуем.
Я понимаю, что здесь этого кода может быть недостаточно для того, чтобы понять, как вообще это может в действительности работать, поэтому я набросал небольшой тестовый проектик, который вы можете по этой ссылочке найти, если кто-то захочет поближе познакомиться с Dynamic Animation.
Давайте подведу небольшой итог по поводу Dynamic animation. FlingAnimation вам нужно использовать для того, чтобы у вас получилась более естественная анимация, которая инициирована пользователем, а SpringAnimation, соответственно, вы можете использовать для того, чтобы сделать красивую анимацию возврата элемента к какому-то начальному состоянию.
Теперь мы уже действительно обговорили очень многое про анимации, и пришло время каких-то общих советов.
Первое, что мы должны понимать при наличии у нас анимации — то, что анимация в итоге должна упрощать жизнь пользователя, а не усложнять ее. Она должна быть информативной. Если же пользователь начинает думать: «Когда уже эта анимация закончится?», наверное, мы сделали что-то не так.
Во-вторых, если вы решились сделать анимацию, для этого стоит использовать современные методы, такие как Transition Framework, ObjectAnimation, ViewPropertyAnimator, Dynamic animation.
Третье, что нужно знать: Android 4.3 и младше сейчас работает на довольно слабых девайсах. Устройство может просто не потянуть все художества, которые мы сотворим с помощью описанных ранее фреймворков. Будет тормозить. Поэтому на данных версиях стоит все-таки делать что-то попроще. Я не хочу сказать, что на данных версиях не стоит делать анимацию вообще. Просто она должна быть несколько проще.
И последнее. Наверное, важное. Если вы сделали анимацию, то протестируйте ее с замедляющим коэффициентом. Заходите в Developer Settings, выставляете коэффициент замедления. Например, коэффициент 5x — вообще замечательный, вы сможете увидеть все детали вашей анимации. И — тестируете то, что может произойти, когда пользователь каким-нибудь способом попытается отменить анимацию.
Итак, мы уже рассмотрели довольно многое, но по-прежнему не все, что может касаться анимации в Android. Еще существует ItemAnimator, который входит в состав RecyclerView. Вы его тоже уже рассматривали в одной из лекций. И ItemAnimator — по сути, такой интерфейс, в который мы можем поместить любые анимации.
Если вы захотите реализовать свой ItemAnimator, то чтобы посмотреть, как вообще это сделать, вы можете просто открыть исходники DefaultItemAnimator, который по умолчанию работает в RecyclerView. Там все довольно понятно.
Есть еще AnimatedVectorDrawable, который умеет анимировать векторные изображения друг между другом. Он тоже появился в пятом Android, но доступен с помощью compatibility-библиотеки в составе Support Library. И если вы все-таки будете его использовать, хорошенько тестируйте его на Android 4, потому что там тоже много багов. StackOverflow весь усыпан различными багами, которые AnimatedVectorDrawableCompat может вам доставить.
Плюс Activity и Fragment Transition — способ анимации между несколькими activity или несколькими фрагментами. Он работает на основе Transition Framework, поэтому API довольно близки.
И давайте подведем общий итог доклада про анимации, который я за последний час вам прочитал. Ничего нового я вам не скажу. Используйте современные анимации, чтобы повысить уровень интерактивности и информативности вашего приложения. Спасибо вам большое за внимание.
Контакты автора: почта, Telegram
Автор: Леонид Клюев