[Перевод] Прототипирование iOS-анимаций с Framer

Предлагаю читателям «Хабрахабра» перевод статьи «Using Framer to Prototype iOS Animations» с сайта raywenderlich.com.

Статичные, неподвижные прототипы, мягко говоря, отстой. Со статичными прототипами можно показать визуальный дизайн, но не дизайн взаимодействия.
Размышляя о важности дизайна взаимодействия для приложений, можно сказать, что статичный прототип — это как пазл с недостающими кусочками. Так почему бы всем не создавать интерактивные прототипы вместо всего этого? Что ж, с помощью утилит вроде After Effects прототипирование может занять слишком много времени. А сам прототип может так и не понадобиться.
Попробуйте Framer: утилита для дизайнеров и разработчиков довольно проста в использовании.

В этом уроке по Framer мы создадим анимированное меню, созданное Voleg:
844c7c6d58c24ca4bf49f76dfb2a5813.gif

Сфокусируемся на самой интересной части — прототипировании анимации сворачивания и разворачивания меню.

Начало


Во-первых, скачайте и установите следующие программы (для этого урока можно использовать бесплатные пробные версии):
  • Framer — версия 68+
  • Sketch — версия 39.1+

Откройте Framer. Появится экран приветствия:
ba235e35a30844c28d7efdc6bcb86a5d.png

Кликнем на Animate (самая левая иконка), и увидим проект-образец.
0d8a95eedcbc4b1bb26fec47af084d5d.png

Слева находится Панель Кода, справа — Панель Прототипа. Между ними — Панель Слоев.
Побродите по коду, попробуйте разобраться в том, что он делает. Не волнуйтесь, если что-то останется непонятным.
Закройте этот проект. Мы будем создавать свой собственный.

Создаем новый прототип


Создайте новый файл во Framer: FileNew. Далее — InsertLayer для создания нового слоя.
8f97292089a745c7a9cf94ab4191163c.png

Кликните по пустому месту в редакторе кода для отмены просмотра атрибутов слоя. Теперь мы можем увидеть новый слой в каждой панели:

  • Как код в Панели Кода
  • Как ссылку в Панели Слоев
  • Как серый квадрат в Панели Прототипа

044d083dabcf481d8d75cfca6b05ff65.png

Наведите курсор мыши на имя слоя в Панели Слоев, чтобы увидеть его расположение на прототипе.
c85bf2d5ac81441a88536ef9314fef31.png

Измените имя на square в панели кода.
893dbe3e171f4ecab24f182b86433381.png

Кликните на квадратик слева от строчки с кодом, чтобы просмотреть и изменить атрибуты слоя в Панели Слоев и чтобы перемещать его по Панели Прототипов.
1cb1bac328ca4440a084401039d7d032.png

Перетащите квадрат в центр прототипа, и посмотрите на изменения в Панелях Кода и Слоев.
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 использует табуляцию, так что если вы видите код с пробелами, как этот:
f1f11814a8714d45b82129951f028a6f.png

Удалите пробелы до самого начала строки и замените их табуляцией:
fd5cf2ea63364405a4bcca2a2969ab72.png

Когда вы копипастите код и переходите на новую строку, всегда удаляйте все до начала строки. Иначе ваш код может быть интерпретирован как часть чего-то другого.

Первая Framer-анимация


Настало время магии. Мы заставим красный квадрат по нажатию превратиться в оранжевый круг. Добавьте пустую строку после описания слоя, затем вставьте следующее:
square.onTap ->
	square.backgroundColor = "rgba(255,120,0,0.8)"
	square.borderRadius = 150

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()

Давайте разберемся.
  1. Это описывает новое состояние orangeCircle для square. Это новое состояние устанавливает атрибут backgroundColor в оранжевый и borderRadius в 150. Полный список свойств можно увидеть в framer.js документации.
  2. Здесь настраивается событие — нажатие, по которому square переходит в следующее состояние.

Нажмите на квадрат, чтобы увидеть новый переход.
7ca2103303654e999d79a0142b5690ff.gif

Фигура теперь анимированно переходит в состояние orangeCircle и обратно в изначальное. Все потому, что мы добавили состояние в цикл состояний слоя, так что следующее состояние после orangeCircle — состояние по умолчанию.

Попробуем добавить еще одно состояние. Добавьте эту строку после секции 1:

square.states.add
	greenRounded:
		backgroundColor: ("green") 
		borderRadius: 50

Теперь нажмите на квадрат в Панели Прототипов. Вы увидите цикл из всех трех состояний.
06030558b771471d94f5b05c7172f34c.gif

Всего несколькими строками кода и почти никакой установкой мы создали гладкую (slick) анимацию.
Думаю, теперь мы можем перейти к кое-чему посложнее и круче, как, например, UI.
11844080a9c441c690185a1815a84f71.png

Используем Framer для создания анимации


Взгляните еще раз на анимацию, которую мы будем создавать:
844c7c6d58c24ca4bf49f76dfb2a5813.gif

Самая важная часть в создании анимации — разбить ее на простые компоненты. Это поможет не только в понимании анимации, но и в создании пошаговой инструкции для самого себя. В этой анимации предстоит решить три проблемы: selected-состояние, deselected, и переход между ними.

Deselected


1a39aab595174f0cac150d5aa71946a4.png

Что видит пользователь изначально:

  • 4 баннера, по которым можно тапать.
  • У каждого баннера есть свой цвет, иконка и заголовок.
  • Каждый баннер отбрасывает тень на ниже расположенный баннер.

Selected


865e0893c9b84a24a29b1d65b16afc07.png

Что видит пользователь после нажатия на баннер:

  • Верхняя панель с цветом, заголовком и небольшим значком.
  • Белый задний фон.
  • Верхняя панель отбрасывает на нижний слой тень.
  • Четыре равномерно распределенные представления внизу — два вверху, два под ними.
  • Цвет представлений зависит от выбранного баннера.

Пользователь выбирает переход между двумя состояниями. В deselected-состоянии пользователь тапает на один из цветных баннеров. В selected-состоянии — на верхнюю панель.

Переход от deselected к selected


e787d80583b144f583451dd1c2d80309.gif

Вот что происходит в момент перехода:

  • Все четыре баннера расширяются и двигаются вниз так, что один начинается там, где заканчивается следующий. Каждый баннер в итоге занимает четверть экрана.
  • Баннеры всегда упорядочены, поэтому баннер, на который нажали, не влияет на то, как баннеры появляются в deselected-состоянии.
  • Анимация расширения баннера стартует медленно, затем значительно ускоряется, и заканчивается с затуханием.
  • У каждого баннера своя тень.
  • Иконки расширяются от 0 до 100% своего размера. Анимация начинается быстро, заканчивается с затуханием.
  • Текст передвигается на 4/5 вниз расширяющегося баннера

Теперь, когда вся анимация разбита, можно приступать к самой веселой части — ее построении.

Замечание. Конкретно эту анимацию было нетрудно разбить, но обычно довольно полезно записать анимацию с помощью QuickTime и затем замедлить ее с After Effects или Adobe Photoshop, чтобы выделить мелкие детали. Например, это методика помогла мне распознать, следует ли делать исчезновение иконки немедленным по тапу на баннер, или оно должно происходить постепенно.

Расстановка


Во-первых, загрузите набор картинок для урока.
В архиве находится все, что понадобится для создания анимации — иконки, шрифт, текст и представления.
Установите Archive font. Это важно сделать перед открытием Sketch-файла, иначе он не будет корректно отображаться.
Далее, откройте SweetStuff.sketch и посмотрите, что мы создали.
cca678f791df480a9de66cec9f27410f.png

Вернемся к Framer и создадим новый файл FileNew. Далее — Import.
e57e22b96858403a924cdb9ce5eb8b75.png

Оставьте размер @1x и снова нажмите Import. Увидим следующее:
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-экрана.
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 наверху, а не наоборот.
b2096ae8920740aea3e0a249c135d1e0.png

Создадим функцию для изменения положения меню и их отображения в правильном порядке. Сигнатура определения функции во Framer выглядит следующим образом:

functionName = ([params]) ->

Добавьте следующую функцию для перестановки меню после определения слоев:
repositionMenus = () ->
	iceCreamMenu.bringToFront()
	fruitMenu.bringToFront()
	cupcakeMenu.bringToFront()
	cookieMenu.bringToFront()
 
repositionMenus()

Функция repositionMenus не принимает аргументов и снизу вверх переносит слои меню наверх. Далее идет вызов функции.
Посмотрите на Панель Прототипа — теперь тени отображаются в правильном порядке.
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-состояние.
d465cffa53ae42989708d913d03a63c8.png

Рефакторинг


Однако, оглядываясь назад, понимаем, что как-то на экране слишком много кода.
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()

Что этот код делает:
  1. Объявляем массивы для хранения пунктов меню, цветов, иконок и заголовков.
  2. Эта функция добавляет иконки на каждый слой пункта меню.
  3. То же самое, только с заголовками.
  4. Здесь мы проходим в цикле по каждому пункту меню, создаем новый слой и вызываем наши функции для добавления иконок и заголовков. Заметьте, что эти слои хранятся в массиве menuItems для быстрого доступа к ним в будущем.

И это, дамы и господа, и есть чистый код. Идем дальше.

Переход к selected-состоянию


Первый шаг — добавить новоре состояние с именем collapse в главный цикл с menuItems. Обдумайте это. Что нужно сделать с каждым menuItem, когда оно переходит в состояние collapse?
ce089925b7d4470e92c6efec8fd35db7.gif

Нужно сделать переход от expanded-состояния к collapsed-состоянию
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 может быть полезным, когда нужно обратиться к обрабатываемому на данный момент объекту, вместо использования его имени напрямую. Это чище, в большинстве случаев короче, и улучшает читаемость кода.

Проверьте, как работают нажатия.
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

  1. Принимает параметр currentItem — нажатый пункт меню menuItem.
  2. Проходит по всем menuItems и переводит каждый элемент в следующее состояние.
  3. Если ни один menuItem не был выбран, тогда selected равен false, поэтому выставляем currentItem вперед.
  4. Если menuItem был выбран, тогда selected равен true, поэтому нужно вернуть меню в изначальное состояние через функцию repositionMenus ().
  5. Наконец, selected присваиваем значение, противоположное текущему.

Теперь можно использовать эту функцию в имплементации onTap. Для каждого пункта меню измените onTap:
menuItems[0].onTap ->
	menuStateChange(this)

Круто. Теперь, если посмотреть внимательно, можно заметить, что когда пункты меню сжимаются, тень смотрится как-то слишком жирно.
Все потому, что все четыре тени от слоев накладываются друг на друга.
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

Пройдемся по коду.
  1. Перебираем все элементы menuItems.
  2. Для каждого menuItem перебираем его дочерние слои (subLayers).
  3. Если попался слой icon, анимируем до масштаба = 0, непрозрачности = 0, в течение 0.3 секунды.
  4. Если попался слой 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

  1. Перебираем элементы menuItems.
  2. Перебираем дочерние слои каждого menuItem.
  3. Если дочерний слой — icon, анимируем масштаю до 100%, непрозрачность — до 1, в течение 0.3 секунды.
  4. Если дочерний слой — 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

Проверьте Панель Прототипа — анимация иконок и заголовков работает, как надо.
458de882d7be4687b232c08177f4fe1c.gif

Мы почти закончили. Осталось немного! :]

Настройки анимации


Во Framer любой слой можно анимировать. Настройка анимации определяется следующими параметрами:
  1. properties: width, height, scale, borderRadius и т. д. Ширина, высота, масштаб и радиус границы соответственно. Слой трансформируется из любого состояния в тот, который вы здесь настроили.
  2. time: как долго анимация длится.
  3. repeat: сколько раз повторять анимацию.
  4. delay: нужна ли пауза перед анимацией, и сколько она должна длиться.
  5. curve: скорость анимации.
  6. сurve options: точная настройка скорости для кривой анимации.

Curve и сurve options выглядят довольно запутанно, не так ли? Их можно использовать для создания прототипа кривой анимации с опциями Framer.js Animation Playground

Так как мы не определили кривую анимации для нашего прототипа, по умолчанию Framer использует кривую easy:
3c85d68336c947069fa3b6c0b267db9b.gif

Выглядит жестко и неестественно. Нам больше подойдет кривая spring — она позволит лучше контролировать все происходящее на шаге перехода.
Теперь переведем все вышесказанное в цифры настроек curveOptions.
Кривой spring (пружина) требуются следующие параметры:

  1. tension. Из какого «материала» она якобы сделана. Чем больше число — тем больше скорость и отскок.
  2. friction. Величина сопротивления. Чем больше число — тем быстрее анимация будет затухать.
  3. velocity. Начальная скорость анимации.

Даже если вы не знаете этих цифр, просто поиграйтесь с ними.
В нашем прототипе хотелось бы видеть две анимации с разными скоростями:
Анимация меню: стартует медленно, значительно ускорятся и резко замедляется.
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

Вот как все выглядит в итоге:
2e071b87bac9442999ecf7eb6962e0d3.gif

Куда двигаться дальше?


Все получилось! Поздравляю с первым прототипом Framer.
Проект можно скачать тут. Он идет немного дальше этого урока — показывает, как отобразить представления в каждом пункте меню. Полный проект вместе с таким же прототипом, создан в Xcode + Swift.
  1. 1. Больше узнать о Framer можно в официальной документации и их уроках
  2. Cлои в Framer могут реагировать и на многие другие события, как например pan-, swipe- и pinch-жесты. Узнать побольше о таких событиях можно тут.
  3. Что можно делать с состояниями.
  4. Про curves можно почитать в Framer’s Easing Curves в разделе Animate.
  5. Об animation curves и других технологиях анимаций Framer посмотрите документацию по анимациям.
  6. Набраться вдохновения можно здесь.
  7. Наконец, узнать об анимациях со Swift и Xcode можно в книге iOS Animations by Tutorials.

Комментарии (0)

© Habrahabr.ru