Иллюстрация Вирджинии Полтрэк

Анимация по расписанию

Анимации в приложении ввода / вывода Google

Недавно я был частью отличной команды, работавшей над приложением Google I / O 2018 для Android. Это приложение-компаньон для конференций, позволяющее посетителям и удаленным людям находить сеансы, составлять индивидуальное расписание и резервировать места на месте (если вам посчастливилось быть там!). Мы встроили в приложение ряд интересных анимированных функций, которые, как мне кажется, значительно улучшили опыт. Код для этого приложения только что был открыт, и я хотел бы выделить некоторые из этих примеров и некоторые интересные детали реализации.

Некоторые анимированные элементы в приложении ввода / вывода

Обычно в приложении используются 3 типа анимации:

  1. Герой анимации - используется для усиления брендинга и приносит моменты восторга
  2. Переходы экрана
  3. Изменения состояния

Я хотел бы подробно остановиться на некоторых из них.

обратный отсчет

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

Обратный отсчет до начала конференции

Эта анимация была разработана дизайнером движений и передана в виде серии файлов Lottie json: каждая длина 1 секунда, показывающая число, анимирующее «in» затем «out». Формат Lottie облегчал перенос файлов в активы и даже предлагал удобные методы, такие как setMinAndMaxProgress, которые позволяли нам воспроизводить только первую или последнюю половину анимации (чтобы показать число, оживляющее или уменьшающее).

Интересная часть была на самом деле оркестровки этих нескольких анимаций в общий отсчет. Чтобы сделать это, мы создали собственный CountdownView, который является довольно сложным ConstraintLayout, содержащим несколько LottieAnimationViews. В этом мы создали делегат Kotlin для инкапсуляции запуска соответствующей анимации. Это позволило нам просто присвоить Int каждому делегату цифры, которую он должен отображать, и делегат установит и запустит анимацию (и). Мы расширили делегат ObservableProperty, который гарантирует, что мы запускаем анимацию только при изменении цифры. Затем наш цикл анимации просто каждую секунду публикует исполняемый файл (когда представление присоединено), который вычисляет, какая цифра должна отображаться в каждом представлении, и обновляет делегатов.

бронирование

Одним из ключевых действий приложения является предоставление участникам места зарезервировать места. Таким образом, мы отображали это действие в виде FAB на экране сведений о сеансе. Мы чувствовали, что важно сообщать только о том, что сеанс был зарезервирован после того, как он успешно завершился на бэкэнде (в отличие от менее важных действий, таких как запуск сеанса, когда мы оптимистично обновляем пользовательский интерфейс немедленно). Это может занять некоторое время, пока мы ждем ответа от бэкэнда, поэтому, чтобы сделать его более отзывчивым, мы использовали анимированный значок, чтобы предоставить отзыв о том, над чем мы работаем, и плавно перейти в новое состояние.

Обратная связь при резервировании места на сессии

Это усложняется тем фактом, что этот значок должен был отражать несколько состояний: сеанс может быть зарезервирован, возможно, он уже зарезервировал место, если сеанс заполнен, то может быть доступен список ожидания или они могут быть на список ожидания, или близко к сеансу начальные бронирования отключены Это привело ко многим перестановкам различных состояний для анимации между ними. Чтобы упростить эти переходы, мы решили всегда проходить через «рабочее» состояние; анимированные песочные часы выше. Поэтому каждый переход на самом деле представляет собой пару: состояние 1 → работает и работает → состояние 2. Это сильно упростило вещи. Мы создали каждую из этих анимаций, используя shapehifter; смотрите файлы avd_state_to_state здесь.

Чтобы отобразить это, мы использовали пользовательское представление и AnimatedStateListDrawable (ASLD). Если вы ранее не использовали ASLD, то это (как следует из названия) анимированная версия StateListDrawable, с которой вы, вероятно, столкнулись, что позволяет вам не только предоставлять различные элементы рисования для каждого состояния, но и переходы между состояниями (в форме AnimatedVectorDrawable). или AnimationDrawable). Вот чертеж, определяющий статические изображения и переходы в рабочее состояние и обратно для значка резервирования.

Мы создали собственный вид для поддержки наших собственных пользовательских состояний. Представления предлагают некоторые стандартные состояния, такие как нажатие или выделение. Точно так же вы можете определить свой собственный и иметь маршрут View, который отображается для любых Drawables, которые он отображает. Мы определили наши собственные state_reservable, state_reserved и т. Д. Затем мы создали перечисление этих различных состояний, инкапсулируя состояние представления плюс любые связанные атрибуты, такие как описание связанного содержимого. Тогда наша бизнес-логика могла бы просто установить соответствующее значение из этого перечисления в представлении (через привязку данных), которое обновило бы состояние рисованного объекта, которое запускало анимацию через ASLD. Комбинация пользовательских состояний и AnimatedStateListDrawable была отличным способом реализовать это, сохранив множество состояний в декларативных слоях, что привело к минимальному коду представления.

Динамик переход

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

Общий элемент перехода

Это довольно стандартный переход с использованием общих элементов с использованием классов платформы ChangeBounds и ArcMotion в ImageView.

Что было более интересно, так это то, как инициирование этого перехода вписалось в шаблон Event, который мы использовали для навигации. По сути, этот шаблон отделяет входные события (например, прикосновение к динамику) от событий навигации, в результате чего ViewModel отвечает за то, как реагировать на ввод. В этом случае это разделение означает, что ViewModel предоставил LiveData of Events, который знал только идентификатор говорящего, к которому нужно перейти. Инициирование перехода совместно используемого элемента требует общего Представления, которого у нас не было на данный момент. Мы решили эту проблему, сохранив идентификатор докладчика в виде тега на представлении, когда оно привязано, чтобы впоследствии можно было восстановить представление, когда нам нужно перейти к конкретному экрану сведений о докладчике.

фильтры

Основной частью приложения для конференций является фильтрация множества событий по интересующим вас вопросам. С каждой темой связан определенный цвет для легкого распознавания, и мы получили отличный дизайн для специального «чипа», который можно использовать при выборе фильтров:

Анимированные фильтры чипов

Мы посмотрели на Chip от Material Components, но решили реализовать собственный настраиваемый вид для большего контроля над отображением и анимацией между «проверенными» состояниями. Это реализовано с использованием рисования на холсте и StaticLayout для отображения текста. Представление имеет одно свойство прогресса [0–1], моделирование не проверено - проверено. Чтобы переключить состояние, мы просто анимируем это значение и делаем недействительным представление, и код рендеринга линейно интерполирует позиции и размеры элементов на основе этого.

Первоначально при реализации этого я заставил представление реализовать интерфейс Checkable и запустил анимацию, когда метод setChecked установил новое состояние. Поскольку мы отображаем несколько фильтров в RecyclerView, это приводит к неудачному эффекту запуска анимации, если выбранный фильтр прокручивается, а представление переходит в невыбранный прокручиваемый фильтр. К сожалению. Поэтому мы добавили отдельный метод для запуска анимации, позволяющий нам различать переключение между щелчком мыши и немедленное обновление при привязке новых данных к представлению.

Кроме того, когда мы представили эту анимацию переключения, мы обнаружили, что она дергает, то есть отбрасывает кадры. Был ли виноват мой код анимации? Эти фильтры отображаются в BottomSheet перед основным экраном расписания конференции. Когда фильтр переключается, мы запускаем логику фильтрации, которая будет применена к расписанию (и обновляем количество совпадающих событий в заголовке листа фильтра). После некоторого разбора в systrace мы определили, что проблема заключалась в том, что когда фильтры применялись, ViewPager из RecyclerViews, отображающий расписание, покорно отключался и обновлялся для вновь доставленных данных. Это вызвало раздувание и связывание ряда мнений. Вся эта работа уносила наш рамочный бюджет ... но график обновления не был виден, поскольку он был за фильтрующим листом. Мы приняли решение отложить выполнение фактической фильтрации до запуска анимации, мы обменяли некоторые дополнительные сложности на реализацию для более плавного взаимодействия с пользователем. Первоначально я реализовал это с помощью postDelayed, но это вызвало проблемы для тестов пользовательского интерфейса. Вместо этого мы переключили наш метод, который запустил анимацию, чтобы принимать лямбду, которая будет запускаться подряд. Это позволило нам лучше соблюдать пользовательские настройки анимации и правильно тестировать выполнение.

Получить анимацию

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