Предлагаю читателям «Хабрахабра» перевод статьи «Using Framer to Prototype iOS Animations» с сайта raywenderlich.com.
Статичные, неподвижные прототипы, мягко говоря, отстой. Со статичными прототипами можно показать визуальный дизайн, но не дизайн взаимодействия.
Размышляя о важности дизайна взаимодействия для приложений, можно сказать, что статичный прототип — это как пазл с недостающими кусочками. Так почему бы всем не создавать интерактивные прототипы вместо всего этого? Что ж, с помощью утилит вроде After Effects прототипирование может занять слишком много времени. А сам прототип может так и не понадобиться.
Попробуйте Framer: утилита для дизайнеров и разработчиков довольно проста в использовании.
В этом уроке по Framer мы создадим анимированное меню, созданное Voleg:
![](https://habrastorage.org/files/844/c7c/6d5/844c7c6d58c24ca4bf49f76dfb2a5813.gif)
Сфокусируемся на самой интересной части — прототипировании анимации сворачивания и разворачивания меню.
Во-первых, скачайте и установите следующие программы (для этого урока можно использовать бесплатные пробные версии):
Откройте Framer. Появится экран приветствия:
![](https://habrastorage.org/files/ba2/35e/35a/ba235e35a30844c28d7efdc6bcb86a5d.png)
Кликнем на Animate (самая левая иконка), и увидим проект-образец.
![](https://habrastorage.org/files/0d8/a95/eed/0d8a95eedcbc4b1bb26fec47af084d5d.png)
Слева находится Панель Кода, справа — Панель Прототипа. Между ними — Панель Слоев.
Побродите по коду, попробуйте разобраться в том, что он делает. Не волнуйтесь, если что-то останется непонятным.
Закройте этот проект. Мы будем создавать свой собственный.
Создайте новый файл во Framer: File->New. Далее — Insert->Layer для создания нового слоя.
![](https://habrastorage.org/files/8f9/729/208/8f97292089a745c7a9cf94ab4191163c.png)
Кликните по пустому месту в редакторе кода для отмены просмотра атрибутов слоя. Теперь мы можем увидеть новый слой в каждой панели:
![](https://habrastorage.org/files/044/d08/3da/044d083dabcf481d8d75cfca6b05ff65.png)
Наведите курсор мыши на имя слоя в Панели Слоев, чтобы увидеть его расположение на прототипе.
![](https://habrastorage.org/files/c85/bf2/d5a/c85bf2d5ac81441a88536ef9314fef31.png)
Измените имя на square в панели кода.
![](https://habrastorage.org/files/893/dbe/3e1/893dbe3e171f4ecab24f182b86433381.png)
Кликните на квадратик слева от строчки с кодом, чтобы просмотреть и изменить атрибуты слоя в Панели Слоев и чтобы перемещать его по Панели Прототипов.
![](https://habrastorage.org/files/1cb/1ba/c32/1cb1bac328ca4440a084401039d7d032.png)
Перетащите квадрат в центр прототипа, и посмотрите на изменения в Панелях Кода и Слоев.
![](https://habrastorage.org/files/fb3/c0f/840/fb3c0f8401f74d90ae9af560c736b59d.png)
Изменения в слое путем взаимодействия с прототипом тут же отображаются в коде — и наоборот. Возможность использовать как код, так и визуальный редактор для внесения изменений дает огромное преимущество прототипирования во Framer в противопоставлении с Xcode и Swift.
Удалите существующий код, и впишите следующий.
И снова в прототипе все поменялось. Довольно аккуратно, не так ли?
Замечание. Вы пишите код во Framer, используя CoffeeScript — простой язык, который компилируется в Javascript. Не волнуйтесь, если вы никогда его не использовали — вы можете много узнать о его синтаксисе из этого урока.
Обратите внимание, что отступы имеют значение в CoffeeScript. Так что убедитесь, что ваши отступы совпадают с моими, иначе код не будет работать. Отступы в CoffeScript заменяют {}.
Табуляция и пробелы — не одно и то же. По умолчанию Framer использует табуляцию, так что если вы видите код с пробелами, как этот:
![](https://habrastorage.org/files/f1f/118/14a/f1f11814a8714d45b82129951f028a6f.png)
Удалите пробелы до самого начала строки и замените их табуляцией:
![](https://habrastorage.org/files/fd5/cf2/ea6/fd5cf2ea63364405a4bcca2a2969ab72.png)
Когда вы копипастите код и переходите на новую строку, всегда удаляйте все до начала строки. Иначе ваш код может быть интерпретирован как часть чего-то другого.
Настало время магии. Мы заставим красный квадрат по нажатию превратиться в оранжевый круг. Добавьте пустую строку после описания слоя, затем вставьте следующее:
![](https://habrastorage.org/files/6ef/282/3f9/6ef2823f9c84484a800919d65f905cf8.gif)
Наибольшее преимущество прототипирования с Framer перед Xcode и Swift — возможность взаимодействовать с прототипом сразу же после внесения изменений. Отсутствие затратного построения и запуска проекта в Xcode сильно увеличивает скорость прототипирования.
Хорошо, я знаю, о чем вы думаете. Надо бы замедлить анимацию — переход слишком внезапный, да и пользователь не может вернуться обратно к красному квадрату. Это легко исправить.
Вместо того, чтобы уточнять, что должно происходить с квадратом после нажатия, мы добавим новое состояние.
Замените только что добавленный код на этот:
Давайте разберемся.
Нажмите на квадрат, чтобы увидеть новый переход.
![](https://habrastorage.org/files/7ca/210/330/7ca2103303654e999d79a0142b5690ff.gif)
Фигура теперь анимированно переходит в состояние orangeCircle и обратно в изначальное. Все потому, что мы добавили состояние в цикл состояний слоя, так что следующее состояние после orangeCircle — состояние по умолчанию.
Попробуем добавить еще одно состояние. Добавьте эту строку после секции 1:
Теперь нажмите на квадрат в Панели Прототипов. Вы увидите цикл из всех трех состояний.
![](https://habrastorage.org/files/060/305/58b/06030558b771471d94f5b05c7172f34c.gif)
Всего несколькими строками кода и почти никакой установкой мы создали гладкую (slick) анимацию.
Думаю, теперь мы можем перейти к кое-чему посложнее и круче, как, например, UI.
![](https://habrastorage.org/files/118/440/80a/11844080a9c441c690185a1815a84f71.png)
Взгляните еще раз на анимацию, которую мы будем создавать:
![](https://habrastorage.org/files/844/c7c/6d5/844c7c6d58c24ca4bf49f76dfb2a5813.gif)
Самая важная часть в создании анимации — разбить ее на простые компоненты. Это поможет не только в понимании анимации, но и в создании пошаговой инструкции для самого себя. В этой анимации предстоит решить три проблемы: selected-состояние, deselected, и переход между ними.
![](https://habrastorage.org/files/1a3/9aa/b59/1a39aab595174f0cac150d5aa71946a4.png)
Что видит пользователь изначально:
![](https://habrastorage.org/files/865/e08/93c/865e0893c9b84a24a29b1d65b16afc07.png)
Что видит пользователь после нажатия на баннер:
Пользователь выбирает переход между двумя состояниями. В deselected-состоянии пользователь тапает на один из цветных баннеров. В selected-состоянии — на верхнюю панель.
![](https://habrastorage.org/files/e78/7d8/058/e787d80583b144f583451dd1c2d80309.gif)
Вот что происходит в момент перехода:
Теперь, когда вся анимация разбита, можно приступать к самой веселой части — ее построении.
Замечание. Конкретно эту анимацию было нетрудно разбить, но обычно довольно полезно записать анимацию с помощью QuickTime и затем замедлить ее с After Effects или Adobe Photoshop, чтобы выделить мелкие детали. Например, это методика помогла мне распознать, следует ли делать исчезновение иконки немедленным по тапу на баннер, или оно должно происходить постепенно.
Во-первых, загрузите набор картинок для урока. В архиве находится все, что понадобится для создания анимации — иконки, шрифт, текст и представления. Установите Archive font. Это важно сделать перед открытием Sketch-файла, иначе он не будет корректно отображаться. Далее, откройте SweetStuff.sketch и посмотрите, что мы создали.
![](https://habrastorage.org/files/cca/678/f79/cca678f791df480a9de66cec9f27410f.png)
Вернемся к Framer и создадим новый файл File > New. Далее — Import.
![](https://habrastorage.org/files/e57/e22/b96/e57e22b96858403a924cdb9ce5eb8b75.png)
Оставьте размер @1x и снова нажмите Import. Увидим следующее:
![](https://habrastorage.org/files/ff8/bb3/311/ff8bb33115794f5bbaa243a05300b472.png)
Framer создал переменную sketch, которая содержит ссылки на все слои в нашем Sketch-файле. В панели Слоев можно их всех увидеть.
Замечание. Убедитесь, что Sketch-файл был открыт до нажатия Import, иначе Framer не сможет его распознать.
Наш прототип должен работать на множестве устройствах. Поэтому создадим переменную device как ссылку на выбранное устройство, и будем располагать все необходимое относительно размера его экрана.
Добавьте следующее после переменной sketch:
Для более простого использования добавим константы для цветов.
Далее создадим слой-контейнер для хранения всего остального.
Здесь мы создаем слой container и устанавливаем размер, равный размеру экрана устройства. Потом устанавливаем backgroundColor в белый — это будет задним фоном для представлений. Все остальные слои будем добавлять в этот слой. Устанавливая clip в true, мы указываем, что ничего за пределами контейнера показываться не будет.
Начнем с установки deselected-экрана.
![](https://habrastorage.org/files/520/f9a/78a/520f9a78aa6b428f8277f1656d63c302.png)
Слои меню. Так как мы знаем ширину и высоту каждого слоя, определим их как константы:
Добавьте код ниже и посмотрите, что получится.
Тут мы создаем новый слой для меню с печенькой, устанавливаем голубой задний фон, x, y-координаты и высоту с шириной.
Теперь нужно проделать то же самое с остальными меню, но помните — y-координата начинается с конца предыдущего слоя: y: prevousMenu.y + previousMenu.height.
Теперь добавим тени для создания иллюзии того, что каждое меню начинается на верху другого.
В конце описания каждого слоя нужно добавить следующее:
Ура! Но стоп, оно не так отображается. Все потому, что слои были добавлены поверх друг друга сверху вниз, с iceCreamMenu наверху, а не наоборот.
![](https://habrastorage.org/files/b20/96a/e89/b2096ae8920740aea3e0a249c135d1e0.png)
Создадим функцию для изменения положения меню и их отображения в правильном порядке. Сигнатура определения функции во Framer выглядит следующим образом:
Добавьте следующую функцию для перестановки меню после определения слоев:
Функция repositionMenus не принимает аргументов и снизу вверх переносит слои меню наверх. Далее идет вызов функции.
Посмотрите на Панель Прототипа — теперь тени отображаются в правильном порядке.
![](https://habrastorage.org/files/399/e4c/6c1/399e4c6c1b9e46b6b01bbeb4f5afbf3f.png)
Начнем с cookieMenu. Добавим следующие строки кода в конце нашего файла:
Здесь создаются две переменные: cookieIcon и cookieText, которые инициализируются соответствующими объектами из Sketch — Cookie и CookieText. Затем свойству superLayer присваиваем слой cookieMenu.
Следующая задача — расположить эти слои. cookieIcon должен располагаться в центре слоя-родителя (superLayer), а cookieText выровнен по центру горизонтально, но по вертикали смещен вниз относительно слоя-родителя на 4/5.
Пропишите, чтобы центрировать иконку:
Для установления положения текста добавьте следующее:
Теперь просто повторите все это для оставшихся пунктов меню.
Теперь это более-менее похоже на deselected-состояние.
![](https://habrastorage.org/files/d46/5cf/fa5/d465cffa53ae42989708d913d03a63c8.png)
Однако, оглядываясь назад, понимаем, что как-то на экране слишком много кода.
![](https://habrastorage.org/files/dd7/e47/2b3/dd7e472b3a414eccb00f9790f2e09659.jpg)
Только задумайтесь — каким громоздким будет код, когда придется прописывать все состояния в деталях.
Вместо того, чтобы создавать для каждого пункта меню отдельно слой, иконку и текст, для упрощения читаемости и аккуратности напишем функцию.
Замените все, что мы написали после определения menuHeight и menuWidth на следующее:
Что этот код делает:
И это, дамы и господа, и есть чистый код. Идем дальше.
Первый шаг — добавить новоре состояние с именем collapse в главный цикл с menuItems. Обдумайте это. Что нужно сделать с каждым menuItem, когда оно переходит в состояние collapse?
![](https://habrastorage.org/files/ce0/899/25b/ce089925b7d4470e92c6efec8fd35db7.gif)
Нужно сделать переход от expanded-состояния к collapsed-состоянию
![](https://habrastorage.org/files/92d/dc6/b66/92ddc6b6667b437e8eada092498c5d3b.png)
Обзор предстоящих изменений:
Для начала сосредоточьтесь на простых вещах: высота и y-координата menuItem. Закомментируйте 2 строки в цикле for, но не удаляйте — они позже понадобятся.
Замечание. Чтобы закомментировать строку, нажмите Command + /.
Добавим константу collapsedMenuHeight к остальным константам после слоя container.
Добавим collapsed-состояние перед menuItems.push(menuItem):
Теперь нужно заставить menuItems реагировать на событие нажатия. Объявите событие для каждого menuItem, сразу после цикла, перед repositionMenus.
При каждом нажатии на menuItem, цикл проходит по всем элементам menuItems и переводит каждый из них в свое следующее состояние. This.bringToFront() выставляет выбранное menuItem поверх остальных слоев. Позже события можно будет легко изменить, так как мы объявили их отдельно друг от друга.
Замечание. Ключевое слово this может быть полезным, когда нужно обратиться к обрабатываемому на данный момент объекту, вместо использования его имени напрямую. Это чище, в большинстве случаев короче, и улучшает читаемость кода.
Проверьте, как работают нажатия.
![](https://habrastorage.org/files/293/aa7/596/293aa75963a744d580635834fae58417.gif)
Осталось вернуть иконки и заголовки, и пофиксить несколько проблем. Для этого нам нужно отслеживать, когда menuItem было выбрано. Добавим переменную после цикла, перед событиями onTap.
Инициализируем selected в false, потому что сначала у нас ничего не выбрано.
Теперь мы можем написать функцию для переключения между selected- и deselected-состояниями. Перед функцией repositionMenus добавим следующее:
Теперь можно использовать эту функцию в имплементации onTap. Для каждого пункта меню измените onTap:
Круто. Теперь, если посмотреть внимательно, можно заметить, что когда пункты меню сжимаются, тень смотрится как-то слишком жирно. Все потому, что все четыре тени от слоев накладываются друг на друга.
![](https://habrastorage.org/files/5dc/74b/f1d/5dc74bf1d28f4d84885fbd0cb565731b.png)
Для исправления, в menuStateChange измените цикл:
Если слой не является выбранным — тень убирается, когда слои сжимаются. Даже сейчас анимация смотрится довольно классно, но остутствуют две вещи: иконка и заголовок. Раскомментируйте те две строки в цикле menuItems (убедитесь, что они — последние строки в цикле).
Помните, когда мы добавляли имена дочерним слоям в addIcon и addTitle? Сейчас это пригодится. Эти имена помогут нам различать слои в menuItem.
Добавим следующие строки для сжатия меню, после menuStateChange():
Пройдемся по коду.
Теперь добавим еще одну функцию, сразу после предыдущей добавленной.
Добавьте вызовы функций collapse() и expand() в menuStateChange().
Проверьте Панель Прототипа — анимация иконок и заголовков работает, как надо.
![](https://habrastorage.org/files/458/de8/82d/458de882d7be4687b232c08177f4fe1c.gif)
Мы почти закончили. Осталось немного! :]
Во Framer любой слой можно анимировать. Настройка анимации определяется следующими параметрами:
Curve и сurve options выглядят довольно запутанно, не так ли? Их можно использовать для создания прототипа кривой анимации с опциями Framer.js Animation Playground
Так как мы не определили кривую анимации для нашего прототипа, по умолчанию Framer использует кривую easy:
![](https://habrastorage.org/files/3c8/5d6/833/3c85d68336c947069fa3b6c0b267db9b.gif)
Выглядит жестко и неестественно. Нам больше подойдет кривая spring — она позволит лучше контролировать все происходящее на шаге перехода.
Теперь переведем все вышесказанное в цифры настроек curveOptions.
Кривой spring (пружина) требуются следующие параметры:
Даже если вы не знаете этих цифр, просто поиграйтесь с ними. В нашем прототипе хотелось бы видеть две анимации с разными скоростями:
Анимация меню: стартует медленно, значительно ускорятся и резко замедляется.
![](https://habrastorage.org/files/3fb/209/d9c/3fb209d9c30c414dbb15e446600aa3f5.gif)
Анимация иконок и заголовков: размер иконки меняется от 100% до 0%, и анимация довольно внезапная. Она стартует быстро, но резко затухает. По сравнению с предыдущей анимацией, у этой меньше напряженности, и переходы между разными скоростями происходят быстрее.
Перед menuItems.push(menuItem) добавьте следующее:
Тут мы присваиваем spring-анимацию для пунктов меню и выставляем tension = 200, friction = 25, velocity = 8. Теперь анимация движется быстрее иконок и заголовоков.
Найдите все sublayer.animate, и добавьте после строки time в секции properties следующее:
Здесь добавятся одинаковые spring-анимации для заголовков и иконок.
Мы добавим этот код четыре раза: дважды в collapse и дважды в expand для иконок, и для заголовков. Для сравнения результатов — образец функции:
Вот как все выглядит в итоге:
![](https://habrastorage.org/files/2e0/71b/87b/2e071b87bac9442999ecf7eb6962e0d3.gif)
Все получилось! Поздравляю с первым прототипом Framer. Проект можно скачать тут. Он идет немного дальше этого урока — показывает, как отобразить представления в каждом пункте меню. Полный проект вместе с таким же прототипом, создан в Xcode + Swift.
Статичные, неподвижные прототипы, мягко говоря, отстой. Со статичными прототипами можно показать визуальный дизайн, но не дизайн взаимодействия.
Размышляя о важности дизайна взаимодействия для приложений, можно сказать, что статичный прототип — это как пазл с недостающими кусочками. Так почему бы всем не создавать интерактивные прототипы вместо всего этого? Что ж, с помощью утилит вроде After Effects прототипирование может занять слишком много времени. А сам прототип может так и не понадобиться.
Попробуйте Framer: утилита для дизайнеров и разработчиков довольно проста в использовании.
В этом уроке по Framer мы создадим анимированное меню, созданное Voleg:
![](https://habrastorage.org/files/844/c7c/6d5/844c7c6d58c24ca4bf49f76dfb2a5813.gif)
Сфокусируемся на самой интересной части — прототипировании анимации сворачивания и разворачивания меню.
Начало
Во-первых, скачайте и установите следующие программы (для этого урока можно использовать бесплатные пробные версии):
Откройте Framer. Появится экран приветствия:
![](https://habrastorage.org/files/ba2/35e/35a/ba235e35a30844c28d7efdc6bcb86a5d.png)
Кликнем на Animate (самая левая иконка), и увидим проект-образец.
![](https://habrastorage.org/files/0d8/a95/eed/0d8a95eedcbc4b1bb26fec47af084d5d.png)
Слева находится Панель Кода, справа — Панель Прототипа. Между ними — Панель Слоев.
Побродите по коду, попробуйте разобраться в том, что он делает. Не волнуйтесь, если что-то останется непонятным.
Закройте этот проект. Мы будем создавать свой собственный.
Создаем новый прототип
Создайте новый файл во Framer: File->New. Далее — Insert->Layer для создания нового слоя.
![](https://habrastorage.org/files/8f9/729/208/8f97292089a745c7a9cf94ab4191163c.png)
Кликните по пустому месту в редакторе кода для отмены просмотра атрибутов слоя. Теперь мы можем увидеть новый слой в каждой панели:
- Как код в Панели Кода
- Как ссылку в Панели Слоев
- Как серый квадрат в Панели Прототипа
![](https://habrastorage.org/files/044/d08/3da/044d083dabcf481d8d75cfca6b05ff65.png)
Наведите курсор мыши на имя слоя в Панели Слоев, чтобы увидеть его расположение на прототипе.
![](https://habrastorage.org/files/c85/bf2/d5a/c85bf2d5ac81441a88536ef9314fef31.png)
Измените имя на square в панели кода.
![](https://habrastorage.org/files/893/dbe/3e1/893dbe3e171f4ecab24f182b86433381.png)
Кликните на квадратик слева от строчки с кодом, чтобы просмотреть и изменить атрибуты слоя в Панели Слоев и чтобы перемещать его по Панели Прототипов.
![](https://habrastorage.org/files/1cb/1ba/c32/1cb1bac328ca4440a084401039d7d032.png)
Перетащите квадрат в центр прототипа, и посмотрите на изменения в Панелях Кода и Слоев.
![](https://habrastorage.org/files/fb3/c0f/840/fb3c0f8401f74d90ae9af560c736b59d.png)
Изменения в слое путем взаимодействия с прототипом тут же отображаются в коде — и наоборот. Возможность использовать как код, так и визуальный редактор для внесения изменений дает огромное преимущество прототипирования во Framer в противопоставлении с Xcode и Swift.
Удалите существующий код, и впишите следующий.
square = new Layer
x: 250
y: 542
height: 250
width: 250
backgroundColor: "rgba(255,25,31,0.8)"
И снова в прототипе все поменялось. Довольно аккуратно, не так ли?
Замечание. Вы пишите код во Framer, используя CoffeeScript — простой язык, который компилируется в Javascript. Не волнуйтесь, если вы никогда его не использовали — вы можете много узнать о его синтаксисе из этого урока.
Обратите внимание, что отступы имеют значение в CoffeeScript. Так что убедитесь, что ваши отступы совпадают с моими, иначе код не будет работать. Отступы в CoffeScript заменяют {}.
Табуляция и пробелы — не одно и то же. По умолчанию Framer использует табуляцию, так что если вы видите код с пробелами, как этот:
![](https://habrastorage.org/files/f1f/118/14a/f1f11814a8714d45b82129951f028a6f.png)
Удалите пробелы до самого начала строки и замените их табуляцией:
![](https://habrastorage.org/files/fd5/cf2/ea6/fd5cf2ea63364405a4bcca2a2969ab72.png)
Когда вы копипастите код и переходите на новую строку, всегда удаляйте все до начала строки. Иначе ваш код может быть интерпретирован как часть чего-то другого.
Первая Framer-анимация
Настало время магии. Мы заставим красный квадрат по нажатию превратиться в оранжевый круг. Добавьте пустую строку после описания слоя, затем вставьте следующее:
square.onTap ->
square.backgroundColor = "rgba(255,120,0,0.8)"
square.borderRadius = 150
![](https://habrastorage.org/files/6ef/282/3f9/6ef2823f9c84484a800919d65f905cf8.gif)
Наибольшее преимущество прототипирования с Framer перед Xcode и Swift — возможность взаимодействовать с прототипом сразу же после внесения изменений. Отсутствие затратного построения и запуска проекта в Xcode сильно увеличивает скорость прототипирования.
Хорошо, я знаю, о чем вы думаете. Надо бы замедлить анимацию — переход слишком внезапный, да и пользователь не может вернуться обратно к красному квадрату. Это легко исправить.
Вместо того, чтобы уточнять, что должно происходить с квадратом после нажатия, мы добавим новое состояние.
Замените только что добавленный код на этот:
# 1
square.states.add
orangeCircle:
backgroundColor: "rgba(255,120,0,0.8)"
borderRadius: 150
# 2
square.onTap ->
square.states.next()
Давайте разберемся.
- Это описывает новое состояние orangeCircle для square. Это новое состояние устанавливает атрибут backgroundColor в оранжевый и borderRadius в 150. Полный список свойств можно увидеть в framer.js документации.
- Здесь настраивается событие — нажатие, по которому square переходит в следующее состояние.
Нажмите на квадрат, чтобы увидеть новый переход.
![](https://habrastorage.org/files/7ca/210/330/7ca2103303654e999d79a0142b5690ff.gif)
Фигура теперь анимированно переходит в состояние orangeCircle и обратно в изначальное. Все потому, что мы добавили состояние в цикл состояний слоя, так что следующее состояние после orangeCircle — состояние по умолчанию.
Попробуем добавить еще одно состояние. Добавьте эту строку после секции 1:
square.states.add
greenRounded:
backgroundColor: ("green")
borderRadius: 50
Теперь нажмите на квадрат в Панели Прототипов. Вы увидите цикл из всех трех состояний.
![](https://habrastorage.org/files/060/305/58b/06030558b771471d94f5b05c7172f34c.gif)
Всего несколькими строками кода и почти никакой установкой мы создали гладкую (slick) анимацию.
Думаю, теперь мы можем перейти к кое-чему посложнее и круче, как, например, UI.
![](https://habrastorage.org/files/118/440/80a/11844080a9c441c690185a1815a84f71.png)
Используем Framer для создания анимации
Взгляните еще раз на анимацию, которую мы будем создавать:
![](https://habrastorage.org/files/844/c7c/6d5/844c7c6d58c24ca4bf49f76dfb2a5813.gif)
Самая важная часть в создании анимации — разбить ее на простые компоненты. Это поможет не только в понимании анимации, но и в создании пошаговой инструкции для самого себя. В этой анимации предстоит решить три проблемы: selected-состояние, deselected, и переход между ними.
Deselected
![](https://habrastorage.org/files/1a3/9aa/b59/1a39aab595174f0cac150d5aa71946a4.png)
Что видит пользователь изначально:
- 4 баннера, по которым можно тапать.
- У каждого баннера есть свой цвет, иконка и заголовок.
- Каждый баннер отбрасывает тень на ниже расположенный баннер.
Selected
![](https://habrastorage.org/files/865/e08/93c/865e0893c9b84a24a29b1d65b16afc07.png)
Что видит пользователь после нажатия на баннер:
- Верхняя панель с цветом, заголовком и небольшим значком.
- Белый задний фон.
- Верхняя панель отбрасывает на нижний слой тень.
- Четыре равномерно распределенные представления внизу — два вверху, два под ними.
- Цвет представлений зависит от выбранного баннера.
Пользователь выбирает переход между двумя состояниями. В deselected-состоянии пользователь тапает на один из цветных баннеров. В selected-состоянии — на верхнюю панель.
Переход от deselected к selected
![](https://habrastorage.org/files/e78/7d8/058/e787d80583b144f583451dd1c2d80309.gif)
Вот что происходит в момент перехода:
- Все четыре баннера расширяются и двигаются вниз так, что один начинается там, где заканчивается следующий. Каждый баннер в итоге занимает четверть экрана.
- Баннеры всегда упорядочены, поэтому баннер, на который нажали, не влияет на то, как баннеры появляются в deselected-состоянии.
- Анимация расширения баннера стартует медленно, затем значительно ускоряется, и заканчивается с затуханием.
- У каждого баннера своя тень.
- Иконки расширяются от 0 до 100% своего размера. Анимация начинается быстро, заканчивается с затуханием.
- Текст передвигается на 4/5 вниз расширяющегося баннера
Теперь, когда вся анимация разбита, можно приступать к самой веселой части — ее построении.
Замечание. Конкретно эту анимацию было нетрудно разбить, но обычно довольно полезно записать анимацию с помощью QuickTime и затем замедлить ее с After Effects или Adobe Photoshop, чтобы выделить мелкие детали. Например, это методика помогла мне распознать, следует ли делать исчезновение иконки немедленным по тапу на баннер, или оно должно происходить постепенно.
Расстановка
Во-первых, загрузите набор картинок для урока. В архиве находится все, что понадобится для создания анимации — иконки, шрифт, текст и представления. Установите Archive font. Это важно сделать перед открытием Sketch-файла, иначе он не будет корректно отображаться. Далее, откройте SweetStuff.sketch и посмотрите, что мы создали.
![](https://habrastorage.org/files/cca/678/f79/cca678f791df480a9de66cec9f27410f.png)
Вернемся к Framer и создадим новый файл File > New. Далее — Import.
![](https://habrastorage.org/files/e57/e22/b96/e57e22b96858403a924cdb9ce5eb8b75.png)
Оставьте размер @1x и снова нажмите Import. Увидим следующее:
![](https://habrastorage.org/files/ff8/bb3/311/ff8bb33115794f5bbaa243a05300b472.png)
Framer создал переменную sketch, которая содержит ссылки на все слои в нашем Sketch-файле. В панели Слоев можно их всех увидеть.
Замечание. Убедитесь, что Sketch-файл был открыт до нажатия Import, иначе Framer не сможет его распознать.
Наш прототип должен работать на множестве устройствах. Поэтому создадим переменную device как ссылку на выбранное устройство, и будем располагать все необходимое относительно размера его экрана.
Добавьте следующее после переменной sketch:
device = Framer.Device.screen
Для более простого использования добавим константы для цветов.
blue = "rgb(97,213,242)"
green = "rgb(150,229,144)"
yellow = "rgb(226,203,98)"
red = "rgb(231,138,138)"
Далее создадим слой-контейнер для хранения всего остального.
container = new Layer
width: device.width
height: device.height
backgroundColor: 'rgba(255, 255, 255 1)'
borderRadius: 5
clip: true
Здесь мы создаем слой container и устанавливаем размер, равный размеру экрана устройства. Потом устанавливаем backgroundColor в белый — это будет задним фоном для представлений. Все остальные слои будем добавлять в этот слой. Устанавливая clip в true, мы указываем, что ничего за пределами контейнера показываться не будет.
Deselected-состояние
Начнем с установки deselected-экрана.
![](https://habrastorage.org/files/520/f9a/78a/520f9a78aa6b428f8277f1656d63c302.png)
Слои меню. Так как мы знаем ширину и высоту каждого слоя, определим их как константы:
menuHeight = container.height/4
menuWidth = container.width
Добавьте код ниже и посмотрите, что получится.
cookieMenu = new Layer
height: menuHeight
width: menuWidth
x: 0
y: 0
backgroundColor: blue
Тут мы создаем новый слой для меню с печенькой, устанавливаем голубой задний фон, x, y-координаты и высоту с шириной.
Теперь нужно проделать то же самое с остальными меню, но помните — y-координата начинается с конца предыдущего слоя: y: prevousMenu.y + previousMenu.height.
cupcakeMenu = new Layer
height: menuHeight
width: menuWidth
x: 0
y: cookieMenu.y + cookieMenu.height
backgroundColor: green
fruitMenu = new Layer
height: menuHeight
width: menuWidth
x: 0
y: cupcakeMenu.y + cupcakeMenu.height
backgroundColor: yellow
iceCreamMenu = new Layer
height: menuHeight
width: menuWidth
x: 0
y: fruitMenu.y + fruitMenu.height
backgroundColor: red
Теперь добавим тени для создания иллюзии того, что каждое меню начинается на верху другого.
В конце описания каждого слоя нужно добавить следующее:
shadowY: 2
shadowBlur: 40
shadowSpread: 3
shadowColor: "rgba(25,25,25,0.3)"
Ура! Но стоп, оно не так отображается. Все потому, что слои были добавлены поверх друг друга сверху вниз, с iceCreamMenu наверху, а не наоборот.
![](https://habrastorage.org/files/b20/96a/e89/b2096ae8920740aea3e0a249c135d1e0.png)
Создадим функцию для изменения положения меню и их отображения в правильном порядке. Сигнатура определения функции во Framer выглядит следующим образом:
functionName = ([params]) ->
Добавьте следующую функцию для перестановки меню после определения слоев:
repositionMenus = () ->
iceCreamMenu.bringToFront()
fruitMenu.bringToFront()
cupcakeMenu.bringToFront()
cookieMenu.bringToFront()
repositionMenus()
Функция repositionMenus не принимает аргументов и снизу вверх переносит слои меню наверх. Далее идет вызов функции.
Посмотрите на Панель Прототипа — теперь тени отображаются в правильном порядке.
![](https://habrastorage.org/files/399/e4c/6c1/399e4c6c1b9e46b6b01bbeb4f5afbf3f.png)
Добавляем иконки и заголовки
Начнем с cookieMenu. Добавим следующие строки кода в конце нашего файла:
cookieIcon = sketch.Cookie
cookieIcon.superLayer = cookieMenu
cookieText = sketch.CookieText
cookieText.superLayer = cookieMenu
Здесь создаются две переменные: cookieIcon и cookieText, которые инициализируются соответствующими объектами из Sketch — Cookie и CookieText. Затем свойству superLayer присваиваем слой cookieMenu.
Следующая задача — расположить эти слои. cookieIcon должен располагаться в центре слоя-родителя (superLayer), а cookieText выровнен по центру горизонтально, но по вертикали смещен вниз относительно слоя-родителя на 4/5.
Пропишите, чтобы центрировать иконку:
cookieIcon.center()
Для установления положения текста добавьте следующее:
cookieText.centerX()
cookieText.y = cookieText.superLayer.height * 0.8
Теперь просто повторите все это для оставшихся пунктов меню.
cookieIcon = sketch.Cookie
cookieIcon.superLayer = cookieMenu
cookieIcon.center()
cookieText = sketch.CookieText
cookieText.superLayer = cookieMenu
cookieText.centerX()
cookieText.y = cookieText.superLayer.height * 0.8
cupcakeIcon = sketch.Cupcake
cupcakeIcon.superLayer = cupcakeMenu
cupcakeIcon.center()
cupcakeText = sketch.CupcakeText
cupcakeText.superLayer = cupcakeMenu
cupcakeText.centerX()
cupcakeText.y = cupcakeText.superLayer.height * 0.8
fruitIcon = sketch.Raspberry
fruitIcon.superLayer = fruitMenu
fruitIcon.center()
fruitText = sketch.FruitText
fruitText.superLayer = fruitMenu
fruitText.centerX()
fruitText.y = fruitText.superLayer.height * 0.8
iceCreamIcon = sketch.IceCream
iceCreamIcon.superLayer = iceCreamMenu
iceCreamIcon.center()
iceCreamText = sketch.IceCreamText
iceCreamText.superLayer = iceCreamMenu
iceCreamText.centerX()
iceCreamText.y = iceCreamText.superLayer.height * 0.8
Теперь это более-менее похоже на deselected-состояние.
![](https://habrastorage.org/files/d46/5cf/fa5/d465cffa53ae42989708d913d03a63c8.png)
Рефакторинг
Однако, оглядываясь назад, понимаем, что как-то на экране слишком много кода.
![](https://habrastorage.org/files/dd7/e47/2b3/dd7e472b3a414eccb00f9790f2e09659.jpg)
Только задумайтесь — каким громоздким будет код, когда придется прописывать все состояния в деталях.
Вместо того, чтобы создавать для каждого пункта меню отдельно слой, иконку и текст, для упрощения читаемости и аккуратности напишем функцию.
Замените все, что мы написали после определения menuHeight и menuWidth на следующее:
# 1
menuItems = []
colors = [blue, green, yellow, red]
icons = [sketch.Cookie, sketch.Cupcake, sketch. Raspberry, sketch.IceCream]
titles = [sketch.CookieText, sketch.CupcakeText, sketch.FruitText, sketch.IceCreamText]
# 2
addIcon = (index, sup) ->
icon = icons[index]
icon.superLayer = sup
icon.center()
icon.name = "icon"
# 3
addTitle = (index, sup) ->
title = titles[index]
title.superLayer = sup
title.centerX()
title.y = sup.height - sup.height*0.2
title.name = "title"
# 4
for menuColor, i in colors
menuItem = new Layer
height: menuHeight
width: menuWidth
x: 0
y: container.height/4 * i
shadowY: 2
shadowBlur: 40
shadowSpread: 3
shadowColor: "rgba(25,25,25,0.3)"
superLayer: container
backgroundColor: menuColor
scale: 1.00
menuItems.push(menuItem)
addIcon(i, menuItem)
addTitle(i, menuItem)
repositionMenus = () ->
menuItems[3].bringToFront()
menuItems[2].bringToFront()
menuItems[1].bringToFront()
menuItems[0].bringToFront()
repositionMenus()
Что этот код делает:
- Объявляем массивы для хранения пунктов меню, цветов, иконок и заголовков.
- Эта функция добавляет иконки на каждый слой пункта меню.
- То же самое, только с заголовками.
- Здесь мы проходим в цикле по каждому пункту меню, создаем новый слой и вызываем наши функции для добавления иконок и заголовков. Заметьте, что эти слои хранятся в массиве menuItems для быстрого доступа к ним в будущем.
И это, дамы и господа, и есть чистый код. Идем дальше.
Переход к selected-состоянию
Первый шаг — добавить новоре состояние с именем collapse в главный цикл с menuItems. Обдумайте это. Что нужно сделать с каждым menuItem, когда оно переходит в состояние collapse?
![](https://habrastorage.org/files/ce0/899/25b/ce089925b7d4470e92c6efec8fd35db7.gif)
Нужно сделать переход от expanded-состояния к collapsed-состоянию
![](https://habrastorage.org/files/92d/dc6/b66/92ddc6b6667b437e8eada092498c5d3b.png)
Обзор предстоящих изменений:
- Y-координата слоя становится равной 0.
- Высота уменьшается с ? экрана до 1/9 экрана.
- Иконка постепенно исчезает.
- Y-координата текста меняется так, что он движется вверх.
- Видна тень только от выбранного menuItem.
Для начала сосредоточьтесь на простых вещах: высота и y-координата menuItem. Закомментируйте 2 строки в цикле for, но не удаляйте — они позже понадобятся.
# addIcon(i, menuItem)
# addTitle(i, menuItem)
Замечание. Чтобы закомментировать строку, нажмите Command + /.
Добавим константу collapsedMenuHeight к остальным константам после слоя container.
collapsedMenuHeight = container.height/9
Добавим collapsed-состояние перед menuItems.push(menuItem):
menuItem.states.add
collapse:
height: collapsedMenuHeight
y : 0
Теперь нужно заставить menuItems реагировать на событие нажатия. Объявите событие для каждого menuItem, сразу после цикла, перед repositionMenus.
#onTap listeners
menuItems[0].onTap ->
for menuItem in menuItems
menuItem.states.next()
this.bringToFront()
menuItems[1].onTap ->
for menuItem in menuItems
menuItem.states.next()
this.bringToFront()
menuItems[2].onTap ->
for menuItem in menuItems
menuItem.states.next()
this.bringToFront()
menuItems[3].onTap ->
for menuItem in menuItems
menuItem.states.next()
this.bringToFront()
При каждом нажатии на menuItem, цикл проходит по всем элементам menuItems и переводит каждый из них в свое следующее состояние. This.bringToFront() выставляет выбранное menuItem поверх остальных слоев. Позже события можно будет легко изменить, так как мы объявили их отдельно друг от друга.
Замечание. Ключевое слово this может быть полезным, когда нужно обратиться к обрабатываемому на данный момент объекту, вместо использования его имени напрямую. Это чище, в большинстве случаев короче, и улучшает читаемость кода.
Проверьте, как работают нажатия.
![](https://habrastorage.org/files/293/aa7/596/293aa75963a744d580635834fae58417.gif)
Последние штрихи
Осталось вернуть иконки и заголовки, и пофиксить несколько проблем. Для этого нам нужно отслеживать, когда menuItem было выбрано. Добавим переменную после цикла, перед событиями onTap.
selected = false
Инициализируем selected в false, потому что сначала у нас ничего не выбрано.
Теперь мы можем написать функцию для переключения между selected- и deselected-состояниями. Перед функцией repositionMenus добавим следующее:
# 1
menuStateChange = (currentItem) ->
# 2
for menuItem in menuItems
menuItem.states.next()
# 3
if !selected
currentItem.bringToFront()
# 4
else
repositionMenus()
# 5
selected = !selected
- Принимает параметр currentItem — нажатый пункт меню menuItem.
- Проходит по всем menuItems и переводит каждый элемент в следующее состояние.
- Если ни один menuItem не был выбран, тогда selected равен false, поэтому выставляем currentItem вперед.
- Если menuItem был выбран, тогда selected равен true, поэтому нужно вернуть меню в изначальное состояние через функцию repositionMenus().
- Наконец, selected присваиваем значение, противоположное текущему.
Теперь можно использовать эту функцию в имплементации onTap. Для каждого пункта меню измените onTap:
menuItems[0].onTap ->
menuStateChange(this)
Круто. Теперь, если посмотреть внимательно, можно заметить, что когда пункты меню сжимаются, тень смотрится как-то слишком жирно. Все потому, что все четыре тени от слоев накладываются друг на друга.
![](https://habrastorage.org/files/5dc/74b/f1d/5dc74bf1d28f4d84885fbd0cb565731b.png)
Для исправления, в menuStateChange измените цикл:
for menuItem in menuItems
if menuItem isnt currentItem
menuItem.shadowY = 0
menuItem.shadowSpread = 0
menuItem.shadowBlur = 0
menuItem.states.next()
Если слой не является выбранным — тень убирается, когда слои сжимаются. Даже сейчас анимация смотрится довольно классно, но остутствуют две вещи: иконка и заголовок. Раскомментируйте те две строки в цикле menuItems (убедитесь, что они — последние строки в цикле).
addIcon(i, menuItem)
addTitle(i, menuItem)
Помните, когда мы добавляли имена дочерним слоям в addIcon и addTitle? Сейчас это пригодится. Эти имена помогут нам различать слои в menuItem.
Добавим следующие строки для сжатия меню, после menuStateChange():
collapse = (currentItem) ->
# 1
for menuItem in menuItems
# 2
for sublayer in menuItem.subLayers
# 3
if sublayer.name is "icon"
sublayer.animate
properties:
scale: 0
opacity: 0
time: 0.3
# 4
if sublayer.name is "title"
sublayer.animate
properties:
y: collapsedMenuHeight/2
time: 0.3
Пройдемся по коду.
- Перебираем все элементы menuItems.
- Для каждого menuItem перебираем его дочерние слои (subLayers).
- Если попался слой icon, анимируем до масштаба = 0, непрозрачности = 0, в течение 0.3 секунды.
- Если попался слой title — анимируем Y-координату до центра текущего пункта меню.
Теперь добавим еще одну функцию, сразу после предыдущей добавленной.
expand = () ->
# 1
for menuItem in menuItems
# 2
for sublayer in menuItem.subLayers
# 3
if sublayer.name is "icon"
sublayer.animate
properties:
scale: 1
opacity: 1
time: 0.3
# 4
if sublayer.name is "title"
sublayer.animate
properties:
y: menuHeight * 0.8
time: 0.3
- Перебираем элементы menuItems.
- Перебираем дочерние слои каждого menuItem.
- Если дочерний слой — icon, анимируем масштаю до 100%, непрозрачность — до 1, в течение 0.3 секунды.
- Если дочерний слой — title, анимируем Y-координату до menuHeight * 0.8.
Добавьте вызовы функций collapse() и expand() в menuStateChange().
menuStateChange = (currentItem) ->
# remove shadow for layers not in front
for menuItem in menuItems
if menuItem isnt currentItem
menuItem.shadowY = 0
menuItem.shadowSpread = 0
menuItem.shadowBlur = 0
menuItem.states.next()
if !selected
currentItem.bringToFront()
collapse(currentItem)
else
expand()
repositionMenus()
selected = !selected
Проверьте Панель Прототипа — анимация иконок и заголовков работает, как надо.
![](https://habrastorage.org/files/458/de8/82d/458de882d7be4687b232c08177f4fe1c.gif)
Мы почти закончили. Осталось немного! :]
Настройки анимации
Во Framer любой слой можно анимировать. Настройка анимации определяется следующими параметрами:
- properties: width, height, scale, borderRadius и т. д. Ширина, высота, масштаб и радиус границы соответственно. Слой трансформируется из любого состояния в тот, который вы здесь настроили.
- time: как долго анимация длится.
- repeat: сколько раз повторять анимацию.
- delay: нужна ли пауза перед анимацией, и сколько она должна длиться.
- curve: скорость анимации.
- сurve options: точная настройка скорости для кривой анимации.
Curve и сurve options выглядят довольно запутанно, не так ли? Их можно использовать для создания прототипа кривой анимации с опциями Framer.js Animation Playground
Так как мы не определили кривую анимации для нашего прототипа, по умолчанию Framer использует кривую easy:
![](https://habrastorage.org/files/3c8/5d6/833/3c85d68336c947069fa3b6c0b267db9b.gif)
Выглядит жестко и неестественно. Нам больше подойдет кривая spring — она позволит лучше контролировать все происходящее на шаге перехода.
Теперь переведем все вышесказанное в цифры настроек curveOptions.
Кривой spring (пружина) требуются следующие параметры:
- tension. Из какого «материала» она якобы сделана. Чем больше число — тем больше скорость и отскок.
- friction. Величина сопротивления. Чем больше число — тем быстрее анимация будет затухать.
- velocity. Начальная скорость анимации.
Даже если вы не знаете этих цифр, просто поиграйтесь с ними. В нашем прототипе хотелось бы видеть две анимации с разными скоростями:
Анимация меню: стартует медленно, значительно ускорятся и резко замедляется.
![](https://habrastorage.org/files/3fb/209/d9c/3fb209d9c30c414dbb15e446600aa3f5.gif)
Анимация иконок и заголовков: размер иконки меняется от 100% до 0%, и анимация довольно внезапная. Она стартует быстро, но резко затухает. По сравнению с предыдущей анимацией, у этой меньше напряженности, и переходы между разными скоростями происходят быстрее.
Перед menuItems.push(menuItem) добавьте следующее:
menuItem.states.animationOptions =
curve: "spring"
curveOptions:
tension: 200
friction: 25
velocity: 8
time: 0.25
Тут мы присваиваем spring-анимацию для пунктов меню и выставляем tension = 200, friction = 25, velocity = 8. Теперь анимация движется быстрее иконок и заголовоков.
Найдите все sublayer.animate, и добавьте после строки time в секции properties следующее:
curve: "spring"
curveOptions:
tension: 120
friction: 18
velocity: 5
Здесь добавятся одинаковые spring-анимации для заголовков и иконок.
Мы добавим этот код четыре раза: дважды в collapse и дважды в expand для иконок, и для заголовков. Для сравнения результатов — образец функции:
collapse = (currentItem) ->
for menuItem in menuItems
for sublayer in menuItem.subLayers
if sublayer.name is "icon"
sublayer.animate
properties:
scale: 0
opacity: 0
time: 0.3
curve: "spring"
curveOptions:
tension: 120
friction: 18
velocity: 5
if sublayer.name is "title"
sublayer.animate
properties:
y: collapsedMenuHeight/2
time: 0.3
curve: "spring"
curveOptions:
tension: 120
friction: 18
velocity: 5
Вот как все выглядит в итоге:
![](https://habrastorage.org/files/2e0/71b/87b/2e071b87bac9442999ecf7eb6962e0d3.gif)
Куда двигаться дальше?
Все получилось! Поздравляю с первым прототипом Framer. Проект можно скачать тут. Он идет немного дальше этого урока — показывает, как отобразить представления в каждом пункте меню. Полный проект вместе с таким же прототипом, создан в Xcode + Swift.
- 1. Больше узнать о Framer можно в официальной документации и их уроках
- Cлои в Framer могут реагировать и на многие другие события, как например pan-, swipe- и pinch-жесты. Узнать побольше о таких событиях можно тут.
- Что можно делать с состояниями.
- Про curves можно почитать в Framer's Easing Curves в разделе Animate.
- Об animation curves и других технологиях анимаций Framer посмотрите документацию по анимациям.
- Набраться вдохновения можно здесь.
- Наконец, узнать об анимациях со Swift и Xcode можно в книге iOS Animations by Tutorials.
Поделиться с друзьями
Viktorianec
Крайне интересно было бы почитать про возможность адаптации анимаций из Framer в xCode, если такое осуществимо. Я когда-то видел инструменты для перевода path-последоательностей в код, как, например: тут А если б ещё и анимации реально было б, то вообще шик.
Статья хороша, продолжайте! Команда Рея это практически топ-генераторы нового контента для начинающих и не только разработчиков iOS.
Кстати, если кому-то интересно кое-что об упрощении написания анимаций, обратите внимание на: