Воссоздание интерфейса приложения Apple Music во Framer

Прокачайтесь с нуля во Framer с помощью этой пошаговой инструкции (в комплекте с видео, файлами для скачивания, и советами по дизайну)

Tes Mat
Дизайн-кабак

--

Большое спасибо Sergey Voronov и Марату Тынчерову за помощь в переводе.

Есть также переводы этой инструкции на 🇬🇧 английский и 🇪🇸 испанский языки и предыдущая версия на 🇰🇷 корейском языке.

Вы знаете, что в Apple Music экран «Исполняется» можно прокрутить вниз, чтобы магически превратить его в мини-плеер? Я подумал, что было бы круто повторить этот пример во Framer.

Работая над этим, я сделал прокручиваемыми ещё несколько экранов (и пролистываемыми — это совсем не сложно). А в качестве бонуса, почему бы, в самом деле, ещё и музыку не проигрывать?

Вот видео готового прототипа (щёлкните по ссылке, чтобы посмотреть или открыть его):

👀 просмотреть в браузере — 🖥 открыть во Framer

Для начала

Очевидно, прототип немаленький (более 500 строк), но я настолько подробно опишу его, что он станет отличной стартовой площадкой для новичков Framer. Если вы никогда раньше не использовали Framer, имейте в виду, что они предлагают 14-дневную бесплатную пробную версию. Загрузите её, чтобы иметь возможность попробовать среду и понять для себя её возможности.

Совсем не обязательно что-либо знать о Framer, чтобы приступить к похождению этого практического урока. Я добавил ссылки на соответствующие разделы в руководствах Framer Get Started. Ссылки с серым фоном кода, как вот эта — layer (слой), ведут на документацию Framer.

В конце каждого раздела есть ссылки на 👀 просмотр прототипа в Safari, кроме того, есть возможность сразу 🖥 открыть прототип во Framer.

Экраны созданы в Sketch, но мы для быстрого воссоздания мини-плеера будем использовать Framer Design.

Будем комбинировать анимационные приемы (с разными таймингами) для перехода от полноэкранного проигрывателя к мини-плееру и обратно.

Другие вещи, которые мы научимся делать в процессе этого урока:

  • импортировать из Sketch;
  • применять фильтры, чтобы представить слои в оттенках серого или инвертировать их цвета;
  • использовать эффект размытия фона (Background Blur);
  • как показать некоторые элементы (панель вкладок и строка состояния) на всех экранах;
  • использовать модуль для создания музыкального проигрывателя с управляемым индикатором проигрывания, регулятором громкости и таймерами проигранного и оставшегося времени;
  • использовать текстовый слой (Text Layer);
  • создавать функции, использующие фрагменты JavaScript для отображения текущего дня в этом текстовом слое:
  • использовать ScrollComponents (компоненты прокрутки) внутри других ScrollComponents …
  • … и использовать Direction Lock (блокировка направления), чтобы они не прокручивались одновременно;
  • оборачивать (wrap) маскированные группы из Sketch в ScrollComponent;
  • использовать PageComponent (компонент страницы);
  • использовать родительские слои для изменения размера страниц в этом PageComponent;

1. Импорт файла из Sketch

Этот Sketch файл содержит экраны, которые нам понадобятся для создания прототипа.

В нашем Sketch файле пять артбордов

Кстати, в этом файле я использовал новые шрифты SF Pro.

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

  • Экран для вкладки «Library» («Медиатека»; также включает строку состояния и панель вкладок)
  • Экран для вкладки «For You» («Для вас»)
  • Артборд со второй карточкой для экрана «For You»: Favourites Mix
  • И еще один с третьей карточкой: Chill Mix
  • Экран «Исполняется» (“Now Playing”)

Экран «Library» на самом деле намного выше. Его список недавно добавленных альбомов спрятан под маской и его можно найти на странице «Symbols» (Символы).

Я сделал это, чтобы упростить редактирование альбомов. И я сделал то же самое с недавно воспроизведёнными альбомами (Recently Played) на экране «For You».

Страница «Symbols» в Sketch файле, с недавно добавленными альбомами для «Library» и недавно воспроизведёнными альбомами для «For You»

Начнём!

Создайте новый проект в Framer, сохраните его (например, под названием «Apple Music») и импортируйте файл из Sketch.

Дизайн выполнен с масштабированием 1x. Это значит, что экраны на iPhone 8 имеют размер 375 x 667 пунктов интерфейса. Но импорт во Framer с двукратным масштабированием, 2x, расширит их до размера 750 x 1334 пикселей.

Импорт файла с разрешением 2x «retina», смена рамки устройства на «iPhone 8» и сокращение «sketch» до «$»

В верхней части вашего проекта появится эта строка:

sketch = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)

Переименование переменной sketch в $ в дальнейшем позволит нам набирать меньше символов …

$ = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)

… потому что теперь мы можем написать, например, $.Status_Bar вместо sketch.Status_Bar.

👀 просмотреть — 🖥 открыть во Framer

2. Делаем экран «Library» прокручиваемым

Сейчас мы видим только экран «Library», потому что другие артборды находятся за пределами экрана справа (с тем же расстоянием между ними, что и в файле Sketch). Мы передвинем их позже, по мере надобности.

Переходя к нижней части панели слоёв, вы увидите, что наш экран Library (теперь это, очевидно, слой) содержит три дочерних слоя: Status_Bar, Tabs, and Library_content. (У двух последних есть свои собственные «дочки».)

В панели слоёв: «Library» и её дочерние слои

Что ж, содержимое самого экрана находится в Library_content, и с помощью функции wrap() из ScrollComponent, мы сделаем его прокручиваемым:

scroll_library = ScrollComponent.wrap $.Library_content

Наш новый компонент прокрутки, scroll_library, по умолчанию будет прокручиваться во всех направлениях, в том числе по горизонтали. Это легко исправить, отключив свойство scrollHorizontal.

scroll_library.scrollHorizontal = no

Конец страницы частично скрыт панелью вкладок, поэтому придётся добавить немного contentInset (вставка содержимого):

scroll_library.contentInset =
bottom: $.Tabs.height + 80

Я использовал height (высоту) из $.Tabs, но добавил дополнительные 80 пунктов, чтобы освободить место для мини-плеера.

👀 просмотреть — 🖥 открыть во Framer

3. Делаем активной только первую вкладку

В настоящее время все вкладки — красные, но активной должна быть только первая вкладка, а неактивные должны быть серыми.

Я же оставил их красными нарочно, потому что цвет слоя во Framer можно настроить. А удаление цвета (с использованием grayscale или saturate) делается совсем просто.

Используем цикл for…in, чтобы изменить насыщенность цвета всех children (детей) $.Tabs до нуля, что сделает их серыми.

Они всё ещё будут слишком тёмными, но уменьшая их opacity (непрозрачность) до 60%, мы придадим им правильный оттенок серого.

for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = 0.6

А затем мы можем вернуть $.Tab_Library изначальные настройки этих свойств, потому что это — наша первая вкладка, она должна быть активной.

$.Tab_Library.saturate = 100
$.Tab_Library.opacity = 1
👀 просмотреть — 🖥 открыть во Framer

4. Делаем экран «For You» прокручиваемым

Артборд $.For_you находится за пределами экрана, справа, поэтому переносим его, изменяя его положение x:

$.For_you.x = 0

Чтобы сделать его прокручиваемым, завернём (to wrap) его так, как сделали это с экраном «Library» ранее.

scroll_for_you = ScrollComponent.wrap $.For_you

Несколько настроек в ScrollComponent:

scroll_for_you.props = 
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40

(Вместо того, чтобы писать отдельные строки для каждого свойства, можно сразу прописать их в props.)

👀 просмотреть — 🖥 открыть во Framer

5. Размещаем строки состояния и панели вкладок поверх всего

Как вы уже заметили, мы потеряли панель вкладок, а также строку состояния. Это нормально, потому как обе они являются «детьми» артборда «Library».

Мы можем вытащить их из $.Library, отменив их родителей:

$.Status_Bar.parent = null
$.Tabs.parent = null

Установка их свойства parent в null сделает их… сиротами, полагаю. Их родитель теперь — основной экран, и они также переместятся в верхнюю часть списка слоёв.

Панель вкладок и строка состояния на панели слоёв

Это как раз то, что мы хотели!

С этим, однако, есть одна проблема. Дополнительные слои, которые будут созданы на следующих этапах (один ScrollComponent здесь, одна прозрачная серая накладка там…), также будут размещены поверх всех существующих слоёв.

Таким образом, понадобится снова вывести на передний план строку состояния и панель вкладок (и снова и снова):

$.Status_Bar.bringToFront()
$.Tabs.bringToFront()

Решение: мы отменим их родителей после того, как сделаем всё остальное, размещая эти строки в конце нашего проекта.

Так что я сделал фолд, который содержит этот код …

# Размещение строки состояния и панели вкладок поверх всего
$.Status_Bar.parent = null
$.Tabs.parent = null

… и убедился, что он остался в конце документа.

👀 просмотреть — 🖥 открыть во Framer

6. Делаем «Недавно прослушанные» альбомы прокручиваемыми

Весь экран «For You» сейчас прокручивается, но это не мешает нам сделать прокручиваемой также его часть.

Раздел «Recently Played» содержит гораздо больше альбомов, чем можно видеть в настоящий момент. Давайте, добавим прокрутку по горизонтали.

recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20

Благодаря этим 20 пунктам contentInset (вставка содержимого) последний альбом выровняется с кнопкой «See All» (просмотреть все).

Список «Недавно воспроизведённые» теперь также прокручивается

Ограничение перемещения прокрутки

Есть, однако, одна мелочь, которую нужно исправить. Легко заметить, что при прокрутке влево или вправо можно нечаянно крутануть вверх или вниз. В оригинальном приложении такого нет.

Когда вы начинаете прокрутку в определенном направлении, прокрутка в другом направлении должна блокироваться. Для этого нам нужно включить directionLock для обоих ScrollComponents. Они должны выглядеть так:

# Компонент прокрутки для всего артборда
scroll_for_you = ScrollComponent.wrap $.For_you
scroll_for_you.props =
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40
directionLock: yes
# Компонент прокрутки для раздела недавно воспроизведённого
recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20
directionLock: yes
👀 просмотреть — 🖥 открыть во Framer

7. Компонент страницы для карточек «New Music Mix», «Favourites Mix», и «Chill Mix»

Мы хотим иметь возможность прокручивать между «New Music Mix», «Favourites Mix» и «Chill Mix», причём одна карточка должна всегда оказываться в центре экрана (карусель). Поэтому будем использовать PageComponent (компонент страницы).

mixes = new PageComponent
frame: $.New_Music_mix.frame # Снова используя «frame» карточки
parent: $.For_you
scrollVertical: no
directionLock: yes

Свойство слоя frame (кадр) содержит как размеры слоя (ширину и высоту), так и его положение (x и y). Таким образом, PageComponent будет занимать то же место в своём родительском слое ($.For_you), что и оригинальная карточка.

Компонент страницы для карточек «mixes», он прозрачный и серый, потому что ещё пуст

Теперь мы можем использовать функцию addPage() для добавления карточек, вот так:

mixes.addPage $.New_Music_mix
mixes.addPage $.Favourites_mix
mixes.addPage $.Chill_mix
👀 просмотреть — 🖥 открыть во Framer

8. Отображение частей других карточек

Есть небольшая деталь: часть второй карточки уже должна быть видна, чтобы дать понять пользователю, что её можно пролистать. (Так же, как с недавно воспроизведёнными альбомами, где третий альбом также немного виден.)

Поэтому наши карточки должны быть меньше. Нужно отрезать кусочек правой части первой карточки, уменьшить карточку «Favourites Mix» с обеих сторон и срезать немного с левой части «Chill Mix». Это можно сделать, поместив каждую карточку в отдельный слой, который будет служить в качестве маски.

(Кстати, строки addPage(), которые мы использовали, можно удалить.)

Во-первых, wrapper (обёртка) для первой карточки:

wrapper1 = new Layer
width: $.New_Music_mix.width - 15
height: $.New_Music_mix.height
backgroundColor: null
clip: yes

Используем ту же самую height (высоту) карточки, но отнимаем 15 пунктов из её width (ширины). Избавляемся от backgroundColor (цвета фона), установленного по умолчанию, делая его равным null, а включая clip, делаем так, что слой будет действовать как маска.

Затем мы помещаем в него $.New_Music_mix:

$.New_Music_mix.parent = wrapper1
$.New_Music_mix.y = 0

Однако, теперь нужно установить вертикальное положение. Раньше это не требовалось, потому что addPage() автоматически исправляет позиции x и y.

И теперь мы добавляем нашу обёртку в виде страницы в компоненте страницы mixes.

mixes.addPage wrapper1
Карточка «New Music Mix» теперь замаскирована родительским слоем

Для второй карточки «Favourites Mix», делаем то же самое:

wrapper2 = new Layer
width: $.Favourites_mix.width - 30 # Обрезать с обеих сторон
height: $.Favourites_mix.height
backgroundColor: null
clip: yes
$.Favourites_mix.parent = wrapper2
$.Favourites_mix.y = 0 # Сброс позиции y
$.Favourites_mix.x = -15 # Изменение положения
mixes.addPage wrapper2

С одним отличием: перемещаем его на 15 пунктов влево.

Таким образом, его родительский слой, wrapper2, сократится на 15 пунктов с левой стороны и на 15 пунктов с правой стороны.

И третья карточка, «Chill Mix», уменьшается на 15 пунктов с левой стороны:

wrapper3 = new Layer
width: $.Chill_mix.width - 15 # Обрезать с левой стороны
height: $.Favourites_mix.height
backgroundColor: null
clip: yes
$.Chill_mix.parent = wrapper3
$.Chill_mix.y = 0 # Сброс позиции y
$.Chill_mix.x = -15 # Изменение положения
mixes.addPage wrapper3
👀 просмотреть — 🖥 открыть во Framer

9. Устанавливаем динамическую дату

В верхней части экрана «For You» показана сегодняшняя дата. Это всего лишь картинка, и, если только вы не читаете это 17 июня 2023 года (который обещает быть приятной субботой 😀), — картинка неправильная. Но это можно легко исправить с помощью текстового слоя и нескольких строк пользовательского кода.

Добавление текстового слоя

Давайте, сначала сделаем textLayer (текстовый слой) с правильным размером шрифта, весом, положением и цветом. А затем сделаем его текст динамическим с помощью функции.

Наш текстовый слой:

today = new TextLayer
text: "SATURDAY, JUNE 17"
fontSize: 13.5
color: "red"
parent: $.Header_For_You
x: $.Today_s_date.x
y: $.Today_s_date.y

Нет необходимости устанавливать его fontFamily(семейство шрифтов), потому что на вашем Mac, как и на iOS шрифтом по умолчанию будет Сан-Франциско. Eго fontSize(размер шрифта), составляет, по-видимому, 13.5 пунктов. (Это будет 27 пикселей.)

Я использовал "red" (красный) как контрастный (и временный) цвет текста, чтобы легче было найти правильную позицию.

Существующая дата находится в отдельном слое, $.Today_s_date, и его родитель—$.Header_For_You. Предоставив нашему текстовому слою тот же самый parent, можно повторно использовать позицию $.Today_s_date.

Как видите, нашему текстовому слою необходимо немного подвинуться вверх. Уменьшив его y–позицию на 7 пикселей, расположим его в правильном месте.

y: $.Today_s_date.y - 3.5

Функция, которая выводит сегодняшнюю дату

Теперь функция, которая выдает текущую дату в виде текстовой строки:

todaysDate = ->
days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']

now = new Date()

dayOfTheWeekNumber = now.getDay() # = число от 0 до 6
monthNumber = now.getMonth() # = число от 0 до 11

theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()

return theDateAsText

Я объясню это по строкам.

Первая строка создает функцию todaysDate.

todaysDate = ->

Стрелка -> означает: «Это функция, и следующие строки должны запускаться при её вызове».

Первая строка в нашей новой функции просто создает array (массив), days, который содержит названия дней недели, …

days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']

… а вторая строка делает то же самое для названий месяцев.

months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']

Затем мы создаём now, объект даты JavaScript, используя new Date().

now = new Date()

Мы не даём конструктору Date() какой-либо другой информации, поэтому по умолчанию now будет содержать текущую дату (а также время, фактически, с точностью до миллисекунды).

Объект Date имеет множество встроенных функций, мы будем использовать три из них:

  • getDay(), чтобы получить текущий день недели. Она возвращает число от 0 до 6. Думаете, что первым днем недели должен быть понедельник (или суббота)? … Но в этом случае 0 означает воскресенье.
  • getMonth(), чтобы получить номер текущего месяца. Здесь никаких проблем: первым всегда будет январь.
  • getDate(), чтобы получить день в месяце. Эта функция не использует нумерацию от нуля (zero-based numbering), как предыдущие, поэтому числа просто начинаются с 1.

Сначала мы получаем числа текущего дня недели и месяца и сохраняем их в dayOfTheWeekNumber и monthNumber.

dayOfTheWeekNumber = now.getDay()         # = число от 0 до 6
monthNumber = now.getMonth() # = число от 0 до 11

Теперь мы можем собрать всё это вместе и построить текстовую строку.

theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()

Первая часть, days[dayOfTheWeekNumber], выбирает правильный день недели из массива, созданного ранее, а вторая часть, months[monthNumber], делает то же самое с названием месяца.

Мы объединяем их (с запятой и пробелом, ", ", между ними) и ставим день месяца в конец с помощью now.getDate().

И тогда последняя строка нашей функции возвращает эту текстовую строку с return.

return theDateAsText

Когда вы вызовете функцию и распечатаете (print) её, например, так …

print todaysDate()

… в Консоли появится сегодняшняя дата.

Использование функции в текстовом слое

Теперь можно использовать todaysDate() для установки свойства text нашего текстового слоя.

today = new TextLayer
text: todaysDate()
fontSize: 13.5
color: "#929292"
parent: $.Header_For_You
x: $.Today_s_date.x
y: $.Today_s_date.y - 3.5
textTransform: "uppercase"

Я сделал ещё два изменения: корректный цвет текста (color) на самом деле "#929292", а с помощью textTransform (преобразование текста) регистр текста изменится на верхний.

Все выглядит хорошо, так что можно скрыть исходный слой, установив его visible (видимость) на no:

$.Today_s_date.visible = no
👀 просмотреть — 🖥 открыть во Framer

Я предпочитаю ставить свои функции в начале проекта. Вот и в приведённом выше проекте я сделал отдельный фолд «Functions» сразу под импортом из Sketch.

10. Переключение между вкладками

Теперь, когда экран «For You» тоже готов, сделаем возможным переключение между двумя экранами.

При нажатии на вкладку «Library» этот экран должен стать видимым, а экран «For You» должен быть скрыт.

$.Tab_Library.onTap ->
scroll_library.visible = yes
scroll_for_you.visible = no

А при нажатии на вкладку «For You» должно произойти обратное.

$.Tab_For_You.onTap ->
scroll_for_you.visible = yes
scroll_library.visible = no

Кстати, вот так можно быстро добавить «event» (событие) в любой импортированный слой:

Pro tip: вы можете добавить событие, анимацию, или state (состояние) к любому слою, щёлкнув правой кнопкой мыши на его названию на панели слоёв

Но ещё нужно активировать правильную вкладку. Сделаем это, добавляя следующие строки к обоим обработчикам события (event handlers):

# Сделать все вкладки серыми
for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = .6
# Кроме этой
@saturate = 100
@opacity = 1

Цикл for…in делает все вкладки серыми, как и ранее, а две последние строки снова делают текущую вкладку красной.

В этих последних двух строках мы на самом деле пишем вот что:

this.saturate = 100
this.opacity = 1

В которой «this» — это вкладка, получившая событие, та, что была нажата. Вместо «this.» можно также использовать «@».

Ниже этих обработчиков onTap добавляем ещё одну строку, потому как при загрузке прототипа экран «For You» должен быть скрыт:

# Первоначально скрыть экран «For You» 
scroll_for_you.visible = no
👀 просмотреть — 🖥 открыть во Framer

11. Экран «Исполняется»

Единственный артборд, который мы ещё не использовали, это экран «Исполняется». Это ещё один прокручиваемый экран, который, ко всему прочему, содержит текст песни и список следующих песен.

scroll_now_playing = new ScrollComponent
width: Screen.width
height: Screen.height - 33
y: 33
scrollHorizontal: no
directionLock: yes

Поскольку в верхней части экрана есть зазор, компонент прокрутки расположен на 33 пункта ниже. Экран «Исполняется» расположен на 13 пунктов ниже строки состояния, вертикальный размер которой 20 пунктов.

И поскольку экран расположен ниже, также вычитаем 33 пункта из Screen.height, когда задаём его высоту (height).

Кстати, вот так это должно выглядеть в результате:

Другой экран на заднем плане и элемент управления показывают, что можно протянуть вниз, чтобы вернуться к предыдущему экрану

Direction lock включен, так как мы не хотим, чтобы экран прокручивался при изменении громкости воспроизведения или при переходе к другому месту воспроизведения в песне.

Компонент прокрутки для экрана «Исполняется»

Теперь артборд. Перенесём его, придав его x свойству значение 0, и добавим его в слой content компонента прокрутки (так это делается без использования wrap()).

$.Now_Playing.x = 0
$.Now_Playing.parent = scroll_now_playing.content

Легко заметить, что внизу страницы остается довольно много места.

Дополнительное пространство в конце экрана «Исполняется»

Это сделано намеренно. Теперь, установив отрицательное значение contentInset (в нижней части, bottom), пользователь будет иметь возможность прокручивать дальше конца страницы (это называется overdrag), не видя экран, который находится под ней.

Добавьте эти строки в свойства компонента прокрутки:

    contentInset:
bottom: -100
Дополнительное пространство «overdrag»

Ага, панель вкладок всё ещё мешает. Мы сдвинем её вниз.

Добавьте эту строку, желательно выше, внутри фолда The Tab Bar:

$.Tabs.y = Screen.height

Панель вкладок разместится чуть ниже экрана.

Позже, переходя от экрана «Исполняется» к мини-плееру, сделаем её появление анимированным.

👀 просмотреть — 🖥 открыть во Framer

12. Прозрачный серый оверлей позади экрана «Исполняется»

Верх текущего экрана («Library» или «For You»), должен быть виден из-под экрана «Исполняется», и должен быть накрыт серым наложением (оверлей).

Этот оверлей может быть просто слоем по размеру экрана, залитым чёрным цветом с прозрачностью 50%, наподобие вот этого:

overlay = new Layer
frame: Screen.frame
backgroundColor: "rgba(0,0,0,0.5)"

С помощью функции placeBehind() мы перемещаем его под экран «Исполняется»:

overlay.placeBehind scroll_now_playing

Однако, некоторые детали отсутствуют.

Экран «Исполняется» должен иметь закруглённые углы, и у него есть…, но не при прокрутке вверх.

Экран «Исполняется» не имеет закругленных углов

А экран, расположенный на заднем плане, должен выглядеть как карта, которая находится в колоде, вот так:

Экран «Исполняется» и тот, что под ним, напоминают карты в колоде

Как добавить закруглённые углы? Это очевидно. Компонент прокрутки требует закругления углов (borderRadius) в 10 пунктов.

scroll_now_playing = new ScrollComponent
width: Screen.width
height: Screen.height - 33
y: 33
scrollHorizontal: no
directionLock: yes
contentInset:
bottom: -100
borderRadius:
topLeft: 10
topRight: 10

(Можно установить радиус закругления для каждого из углов. Для нижних углов используйте bottomRight и bottomLeft.)

Теперь экран, находящийся в данный момент под экраном «Исполняется», scroll_library, тоже должен выглядеть как карта.

Устанавливаем для него такое же значение borderRadius и перемещаем на 20 пунктов вниз так, чтобы он оказался чуть ниже строки состояния.

Он должен быть немного сжат, но только в одном направлении: правильным представляется горизонтальный масштаб (scaleX) в 93 процента.

scroll_library.props =
borderRadius: 10
y: 20
scaleX: 0.93

Из-за более тёмного фона при просмотре экрана «Исполняется» строка состояния должна быть светлой. На помощь приходит другой фильтр — с помощью 100% invert (инвертирование) мы делаем её белой.

$.Status_Bar.invert = 100
👀 просмотреть — 🖥 открыть во Framer

13. Воспроизведение музыки с помощью модуля Framer Audio

Benjamin den Boer из Framer создал модуль, с помощью которого создание музыкального проигрывателя во Framer становится очень простым.

Загрузите модуль Framer Audio в виде ZIP-файла:

Разархивируйте его, найдите файл audio.coffee (он находится в папке ‘src’) и перетащите его в окно вашего проекта.

Вы увидите, что в начало вашего проекта добавилась эта строка:

audio = require 'audio'

(И файл будет автоматически скопирован в папку «modules» внутри папки вашего проекта.)

Следуя указаниям на странице модуля, изменим строку на:

{Audio, Slider} = require "audio"

Создадим с помощью этого модуля аудиоплеер, оборачивая существующие кнопки воспроизведения и паузы.

Кстати, кнопку «Play» мы уже импортировали, но группа была скрыта в Sketch, поэтому её видимость была отключена.

Давайте покажем её.

$.Button_Play.visible = yes

Теперь можно wrap() (обернуть) кнопки:

audio = Audio.wrap($.Button_Play, $.Button_Pause)

Полученный проигрыватель с именем audio будет занимать то же положение, что и кнопки, а также их место в иерархии. Так, аудиопроигрыватель теперь тоже ребёнок $.Now_Playing.

Нам нужна музыка. Это может быть онлайн-музыка, поэтому воспользуемся этим 90-секундным аудиоклипом Apple Music песни Місто (город) от ONUKA.

audio = Audio.wrap($.Button_Play, $.Button_Pause)
audio.audio = "http://audio.itunes.apple.com/apple-assets-us-std-000001/AudioPreview30/v4/a2/3c/57/a23c57a3-09b2-4742-c720-8fa122ab826c/mzaf_6357632044803095145.plus.aac.ep.m4a"

Как получить ссылку на такой фрагмент? Я использовал поиск по каталогу Apple Music с помощью их онлайн-инструмента.

А затем использовал веб-инспектор Safari чтобы узнать какой файл .m4a загружается при воспроизведении музыки, и скопировал этот URL-адрес.

Теперь вы можете проигрывать музыку. Попробуйте. Нажмите кнопку воспроизведения!

👀 просмотреть — 🖥 открыть во Framer

14. Анимация обложки альбома

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

Для справки: Тень в оригинальном приложении на самом деле — размытая копия обложки альбома, но, раз наша обложка чёрная, поступим проще и используем тень.

Для анимации перехода между этими двумя состояниями будем, конечно, использовать Состояния — States.

Но сначала необходимо настроить несколько вещей.

Настройка

Позже мы покажем ту же обложку альбома уменьшенной в мини-плеере… и сделаем так, что весь экран «Исполняется» исчезнет. Вот почему мы должны изъять слой обложки альбома из его родительского слоя и поместить его прямо в компонент прокрутки.

Это легко сделать одной строкой:

$.Album_Cover.parent = scroll_now_playing.content

Теперь $.Album_Cover всё ещё находится в компоненте прокрутки, но независимо, как родственный элемент экрана «Исполняется». (И нам даже не пришлось исправлять его положение.)

Далее, нужно избавиться от существующей (статичной) тени. Это была отдельная группа в документе Sketch, поэтому можно просто сделать этот слой $.Album_Cover_shadow невидимым.

$.Album_Cover_shadow.visible = no

Создание состояний «воспроизводится» и «на паузе»

Теперь мы можем установить значения для состояний.

Когда музыка воспроизводится, обложка альбома должна выглядеть так:

Так должна выглядеть обложка альбома при проигрывании музыки
  • она показана в полном размере в 311 х 311 пунктов с масштабом (scale) 1;
  • цвет тени на 40% чёрный — "rgba(0,0,0,0.4)";
  • тень проецируется вниз — shadowY 20 пунктов …
  • … и наружу во всех направлениях — shadowSpread (распространение тени) 10 пунктов;
  • (нет горизонтальной тени, shadowX;)
  • размытость тени также высока — 50 пунктов размытия Гаусса (shadowBlur);

А когда музыка приостановлена, обложка должна выглядеть так:

А так должна выглядеть обложка альбома, когда музыка приостановлена
  • 249 х 249 — scale составит 0.8;
  • тень очень светлая: только 10% чёрного цвета — "rgba(0,0,0,0.1)";
  • вертикальная тень, shadowY19 пунктов;
  • нет shadowSpread;
  • shadowBlur37 пунктов;

(Тень на самом деле окажется на 20% меньше из-за изменения масштаба.)

Для простоты мы будем называть наши состояния "playing" и "paused". Оба их можно определить одновременно:

$.Album_Cover.states =
playing:
scale: 1
shadowType: "outer"
shadowColor: "rgba(0,0,0,0.4)"
shadowY: 20
shadowSpread: 10
shadowBlur: 50
frame: $.Album_Cover.frame
animationOptions:
time: 0.8
curve: Spring(damping: 0.60)
paused:
scale: 0.8
shadowType: "outer"
shadowColor: "rgba(0,0,0,0.1)"
shadowY: 19
shadowSpread: 0
shadowBlur: 37
frame: $.Album_Cover.frame
animationOptions:
time: 0.5

Я также включил первоначальное значение свойства frame слоя в каждое состояние для того, чтобы позже можно было добавить третье состояние мини-плеера, в котором мы изменим положение обложки.

Также я включил animationOptions (параметры анимации):

  • Анимация перехода в состояние "playing" занимает 0.8 секунды, но она выглядит более быстрой, потому что заканчивается мягким отскоком.
  • В анимации обратного перехода в состояние "paused" никакого отскока нет (мы используем кривую по умолчанию — Bezier.ease), а её продолжительность — 0.5 секунды.

Для проверки эффектов анимации можно выполнить stateCycle() между ними, нажав на обложку альбома:

$.Album_Cover.onTap ->
this.stateCycle "paused", "playing"

(Включив в код имена состояний, сделаем так, что состояние "default" будет проигнорировано.)

Проверка анимации состояний обложки альбома

Выглядит как надо.

Можно удалить событие onTap(), потому что анимация состояний будет запускаться вместе с включением и остановкой музыки.

С помощью stateSwitch() можно переключить слой в определенное состояние без анимации. Используем эту функцию, чтобы сделать "paused" начальным состоянием.

$.Album_Cover.stateSwitch "paused"

Анимация между состояниями, когда музыка включается и останавливается

Для запуска этих анимаций можно было бы использовать onTap-события на кнопках воспроизведения и паузы, как например …

$.Button_Play.onTap ->
$.Album_Cover.animate "playing"

… но позднее у нас будет ещё две кнопки: те, что в мини-плеере.

Так что сделаем это по-другому. Будем отслеживать события playing и pause аудиоплеера.

В аудиоплеере есть объект player, представляющий собой ни что иное, как аудио-элемент HTML5, фактически воспроизводящий музыку. И, судя по всему, можно добавить к нему функции, которые будут выполняться при возникновении события. Делаем это, создавая функцию в player с on перед именем события.

Таким образом playing и pause становятся onplaying и onpause.

# Когда музыка начала воспроизводиться
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Когда музыка приостановлена
audio.player.onpause = ->
$.Album_Cover.animate "paused"
👀 просмотреть — 🖥 открыть во Framer

15. Создание индикатора воспроизведения и таймеров

Добавление индикатора воспроизведения

Для индикатора воспроизведения и ползунка громкости модуль Framer Audio использует слайдеры.

SliderComponent (компоненту слайдера) можно придать любой желаемый вид (или создать его в Design), а затем передать его аудиоплееру.

Попробуйте этот слайдер:

progressBar = new SliderComponent
width: 311
height: 3
backgroundColor: "#DBDBDB"
knobSize: 7
x: Align.center
y: 363
parent: $.Now_Playing

По ширине он такой же, как обложка альбома, 311 пунктов, и он тонкий, всего 3 пункта в высоту. Цвет слайдера светло-серый "#DBDBDB".

Размер ручки, knobSize, также довольно мал, всего 7 пунктов.

Сделав $.Now_Playing его родительским элементом, parent, поместим его внутри экрана «Исполняется». Используем Align.center, чтобы центрировать его по горизонтали и размещаем его на уровне 363 пунктов от верхнего края.

Левая часть компонента слайдера — это его заполнение, fill. Это дочерний слой, поэтому его цвет придётся установить отдельно:

progressBar.fill.backgroundColor = "#8C8C91"

То же самое справедливо и для knob, который получит тот же цвет, что и заполнение:

progressBar.knob.props = 
backgroundColor: progressBar.fill.backgroundColor
shadowColor: null

(Избавляемся от тени по умолчанию, устанавливая shadowColor равным null.)

Теперь, чтобы активировать слайдер, передайте его функции аудиоплеера showProgress().

audio.showProgress progressBar

Добавление таймера воспроизведения

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

timePlayed = new TextLayer
fontSize: 14
color: progressBar.fill.backgroundColor
x: progressBar.x
y: progressBar.y + 5.5
parent: $.Now_Playing

Шрифт в нём используется по умолчанию (на iOS и Mac) — Сан-Франциско размером 14, и он должен быть расположен на 5.5 пунктов ниже progressBar. Цвет текста (color) совпадает с цветом fill индикатора воспроизведения.

Затем, чтобы обновлять его в процессе воспроизведения музыки, передаём его функции showTime() аудиоплеера.

audio.showTime timePlayed

Добавление таймера оставшегося времени

Также есть таймер, отсчитывающий оставшееся время.

Он имеет те же свойства текста, что и таймер timePlayed, поэтому можно просто скопировать это …

timeRemaining = timePlayed.copy()

… а затем изменить несколько свойств, чтобы переместить его вправо:

  • Его правый край, maxX, должен быть выровнен с той же стороной progressBar.
  • Его текст должен быть выровнен по правому краю.
  • И, чтобы работало выравнивание текста по правому краю, ширина его должна быть фиксированной.
timeRemaining.props =
textAlign: Align.right
width: 60
maxX: progressBar.maxX
parent: $.Now_Playing

Передаём его аудиоплееру с showTimeLeft().

audio.showTimeLeft timeRemaining

Можно заметить, что теперь всё размещено точно поверх оригинального $.Progress_bar из Sketch, поэтому его можно скрыть:

$.Progress_bar.visible = no

Кстати, теперь можно видеть, когда музыка загрузилась. Когда таймер оставшегося времени покажет правильную продолжительность, -1:29, это будет означать, что файл готов. Если это занимает слишком много времени, можно загрузить файл .m4a и сохранить его в папке проекта (лучше всего в новой папке «sounds»). Конечно, в этом случае необходимо изменить URL на локальный.

👀 просмотреть — 🖥 открыть во Framer

16. Воссоздание ползунка громкости

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

volumeSlider = new SliderComponent
width: 266
height: 3
backgroundColor: progressBar.backgroundColor
knobSize: 28
x: 50
y: 559
parent: $.Now_Playing
value: 0.75

Цвет фона (backgroundColor) этого слайдера такой же, как у progressBar.

Громкость в первоначальном дизайне была установлена на уровне 75%, следуя этому, установим соответствующее значение value компонента слайдера.

Его fill имеет тот же цвет, что и progressBar.

volumeSlider.fill.backgroundColor = progressBar.fill.backgroundColor

knob слайдера сохраняет белый цвет, установленный по умолчанию, но имеет другую тень и очень тонкую обводку — лишь 0.5 пункта.

volumeSlider.knob.props = 
borderColor: "#ccc"
borderWidth: 0.5
shadowY: 3
shadowColor: "rgba(0,0,0,0.2)"
shadowBlur: 4

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

Прежде чем добавлять его, установим фактическую громкость аудиоплеера также на 75%.

audio.player.volume = 0.75

Функция showVolume() аналогична рассмотренным ранее функциям showProgress(), showTime() и showTimeLeft():

audio.showVolume volumeSlider

Теперь можно скрыть первоначальный слой из Sketch:

$.Volume_slider.visible = no
👀 просмотреть — 🖥 открыть во Framer

17. Рисование мини-плеера в Framer Design

При перетаскивании вниз экран «Исполняется» должен превратиться в маленький мини-плеер, расположенный прямо над панелью вкладок.

Мини-плеера нет в нашем Sketch файле, однако, он делается просто: прозрачный фон с мини-версией обложки альбома, название песни, и несколько кнопок.

Что ж… перерыв от кодирования! Нажмите ⌘1, чтобы перейти к Design.

Ваш Design экран по-прежнему будет пустым. Начните с добавления фрейма ‘Apple iPhone 8’.

В качестве шаблона для установки правильных размеров и положения я сделал скриншот, который вы можете найти здесь. Просто перетащите его во фрейм.

В него добавлена кнопка «Play», она нам тоже понадобится.

Он будет слишком большим из-за его разрешения retina, но, как и в Sketch (или Framer Code), можно использовать вычисления в полях свойств. Изменим его ширину с 750 на 750/2, и он приобретет правильный размер.

Лучше всего заблокировать шаблон, чтобы нельзя было случайно его выбрать или перетащить. Выберите его и нажмите ⌘L (или щёлкните по нему правой кнопкой мыши и выберите «Lock»).

Фреймы против форм

Раньше все объекты, нарисованные в Design, просто становились слоями, но, начиная с Версии 107, появились Frames (фреймы) и Shapes (формы).

Короче говоря:

  • формы — для точного рисования, а фреймы — для представлений;
  • только фреймы могут иметь layout constraints (настройки расположения);
  • фреймы становятся общими слоями layers в мире кода, но формы — это нечто новое: SVGLayers;
  • на HTML жаргоне: фреймы будут <div> элементами, а формы — <svg> элементами.

Дополнительная информация — в справочной статье Framer.

Давайте увеличим масштаб и начнём с кнопки воспроизведения.

Кнопка «Play»

Нам нужен треугольник. Можно использовать инструмент «Многоугольник» (Polygon), сделать трёхгранный многоугольник и повернуть его, но, вероятно, проще перейти прямо к инструменту «Кривая» (Path). Это даст нам больше контроля. (Можно также сделать многоугольник, дважды щёлкнуть по нему, чтобы превратить в кривую, а затем внести изменения.)

Заполните треугольник чёрным.

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

Нарисуйте фрейм, который повторяет очертания синего контура. Его размер должен быть 40 пунктов.

Так как он больше (а также потому, что он — ни форма и ни кривая), он автоматически становится родительским элементом треугольника, а это как раз то, что нам нужно.

Больший родительский слой, для большей кнопки

Измените название фрейма на Mini Button Play и сделайте его прозрачным, отключив его заполнение (Fill).

Кнопка «Pause»

Кнопка паузы проста: два прямоугольника с небольшим радиусом закругления углов.

Вы можете нарисовать её с помощью инструмента «Фрейм», но лучше использовать «Прямоугольник» (Rectangle), так как положение формы может быть задано дробными числами. Заметим, что правильное у-позиционирование для этих прямоугольников будет 576.5 пунктов.

Радиус закругления углов, по-видимому, составляет около 1 пункта.

Как положение, так и размеры форм можно задавать с помощью дробных значений, в отличии от фреймов

Так же, как и в случае с кнопкой «Play», расширяем рабочую область, рисуя сверху фрейм, который назовем Mini Button Pause (и так же сделаем его прозрачным, отключив Fill).

Кнопка «Next»

Два треугольника. Чтобы начать с чего-то, можно сдвоить ⌘D треугольники кнопки «Play».

Мы не будем использовать эту кнопку в нашем прототипе, но раз уж мы взялись за дело, выберем эти два элемента и нажав ⌘↩, поместим их в родительский фрейм …

Выбирать «Добавить фрейм» в меню правой кнопки мыши

… который мы назовём Mini Button Next.

Название песни

Название песни набрано шрифтом SF Pro Text (или SF UI Text) в начертании Regular, с размером шрифта 17 пунктов и с расстоянием между буквами (трекинг) -0.4.

Обложка альбома

При переходе от экрана «Исполняется» к мини-плееру обложка альбома будет уменьшаться, так что в мини-плеере на самом деле обложка не нужна. Но, рисуя её здесь в Design, мы получим правильное расположение и параметры тени в Code.

Изображение обложки альбома имеет размер в 48 пунктов и радиус закругления углов 3 пункта. Можно просто нарисовать фрейм и оставить параметры заливки по умолчанию (мы всё равно скроем его позже).

Его тень должна иметь чёрную заливку в 30%, с у-смещением на 3 пункта и размытием в 10 пунктов.

Назовите фрейм Mini Album Cover.

Фон мини-плеера

Понадобится отдельный фрейм для фона мини-плеера. Позже станет понятно, зачем.

Фон должен иметь размер 375 на 64 пункта, цвет его должен быть очень светлым, почти белым #F6F6F6, непрозрачным на 50%. Назовите его Mini Player Background.

Скорее всего, Mini Player Background только-что стал родителем всех других объектов, поэтому лучше выбрать его дочерние элементы в списке слоёв и снова вынести их наружу.

Линия сверху

У мини-плеера есть тонкая линия вверху. Вы можете нарисовать её с помощью инструмента Path, но проще придать Mini Player Background верхнюю границу. Она должна иметь толщину 0.5 пункта и иметь цвет #AEAEAE.

Родительский слой мини-плеера

Теперь мы можем выбрать все объекты, нажать ⌘↩ «Добавить фрейм» и назвать новый фрейм Mini Player.

Настройка мишеней

Нам нужно установить мишени (targets) для фреймов, которые мы хотим использовать в Code. Устанавливаем мишени для перечисленных ниже фреймов, кликнув соответствующие указатели (маленькие синие круги в панели слоёв):

  • Mini Player
  • Mini Album Cover
  • Mini Button Pause
  • Mini Button Play
  • Mini Button Background

Теперь наш список слоёв должен выглядеть примерно так:

Всё готово. Шаблон больше не нужен, поэтому можно сделать Mini player.png невидимым, щёлкнув на нем правой кнопкой мыши и выбрав ⌘; «Hide» (скрыть).

Не нужен также и белый (установленный по умолчанию) фон фрейма Apple iPhone 8. Сделаем его прозрачным, установив его Fill на 0%.

18. Доводка мини-плеера в коде

Как переключаться между экраном «Исполняется» и мини-плеером

Хорошо, вот в чем фокус. Мини-плеер будет постоянно находиться внутри нашего экрана «Исполняется». Мы просто спрячем его, когда экран «Исполняется» активен:

Мини-плеер скрыт в экране «Исполняется», и экран «Исполняется» никогда полностью не убирается с экрана

Как известно, обложка альбома — это отдельный слой, который перемещается и изменяет свой размер при переходах между большим и малым плеерами.

Размещение мини-плеера

Поместим Mini_Player внутри компонента прокрутки экрана «Исполняется» и изменим его y-позицию на ноль, чтобы он находился вверху экрана.

Mini_Player.props =
parent: scroll_now_playing.content
y: 0
Мини-плеер теперь находится в компоненте прокрутки экрана «Исполняется»

Размещение кнопок воспроизведения и паузы

Мы должны переставить кнопки. Вначале должна быть видна кнопка «Play», на том самом месте, где сейчас находится кнопка «Pause».

Первым делом, придаём Mini_Button_Play то же положение по горизонтали, что и Mini_Button_Pause

Mini_Button_Play.x = Mini_Button_Pause.x

… а затем скрываем Mini_Button_Pause.

Mini_Button_Pause.visible = no
Кнопка «Play» теперь расположена правильно, а кнопка «Pause» скрыта

Делаем кнопки воспроизведения и паузы кликабельными

Придадим этим кнопкам способность проигрывать и приостанавливать музыку, используя функции play() и pause() (из HTML5 аудио) в объекте player аудиоплеера:

Mini_Button_Play.onTap ->
audio.player.play()
Mini_Button_Pause.onTap ->
audio.player.pause()

Можно было бы использовать те же обработчики событий onTap, чтобы кнопки отображались и исчезали (для переключения между кнопками «Play» и «Pause»).

Но мы уже «слушали» некоторые события player‘a и знаем, когда воспроизведение музыки началось или остановилось. Если помните, они используются для увеличения и уменьшения обложки альбома.

Вернёмся к фолду Animating the Album Cover и добавим следующие строки:

# Когда музыка начала воспроизводиться
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# Когда музыка приостановлена
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no

Таким образом, при нажатии на большие кнопки на экране «Исполняется» маленькие кнопки тоже изменятся.

Чтобы сделать возможным обратное действие (большие кнопки должны меняться при нажатии на маленькие), добавим похожие строки для $.Button_Play и $.Button_Pause.

# Когда музыка начала воспроизводиться
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … а также большие кнопки
$.Button_Play.visible = no
$.Button_Pause.visible = yes
# Когда музыка приостановлена
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no
# … а также большие кнопки
$.Button_Play.visible = yes
$.Button_Pause.visible = no

Теперь все кнопки будут меняться в одно и то же время, независимо от того, какая кнопка «Play» или «Pause» (большая или малая) была нажата.

👀 просмотреть — 🖥 открыть во Framer

19. Маленькая версия обложки альбома в мини-плеере

Сделаем теперь дополнительное состояние для обложки альбома, в котором она будет маленькой и будет располагаться в мини-плеере с корректными параметрами тени.

Но, во-первых, как можно заметить при воспроизведении музыки, обложка альбома находится за мини-плеером. Это легко исправить с placeBefore():

$.Album_Cover.placeBefore Mini_Player

Для этого нового состояния, "mini", мы можем скопировать свойства Mini_Album_Cover, той крошечной обложки альбома, которую мы создали в Design. Воспользуемся её frame, shadowColor, shadowY и shadowBlur

$.Album_Cover.states.mini =
frame: Mini_Album_Cover.frame
shadowColor: Mini_Album_Cover.shadowColor
shadowY: Mini_Album_Cover.shadowY
shadowBlur: Mini_Album_Cover.shadowBlur
shadowSpread: Mini_Album_Cover.shadowSpread
scale: Mini_Album_Cover.scale

… а также установим shadowSpread и scale, потому что эти свойства были изменены другими состояниями. (Mini_Album_Cover имеет значения по умолчанию: shadowSpread = 0, а scale = 1.)

На самом деле Mini_Album_Cover не должна быть видна; просто нужно было скопировать её свойства. Теперь мы можем скрыть её:

Mini_Album_Cover.visible = no

Для проверки можно активировать анимацию в новое "mini" состояние нажатием на обложку альбома:

$.Album_Cover.onTap ->
$.Album_Cover.animate "mini"
👀 просмотреть — 🖥 открыть во Framer

20. Переход с экрана «Исполняется» в мини-плеер

Всё выглядит как надо. На данный момент мини-плеер можно скрыть.

Mini_Player.opacity = 0

Используем opacity (непрозрачность), потому что хотим его анимировать.

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

Mini_Player.visible = no

Позже потребуется знать, используется ли мини-плеер (вы поймёте, почему). Создадим для этого переменную miniPlayerActive, которая в настоящий момент всё равно будет иметь значение ‘no’.

miniPlayerActive = no

Слежение за движением прокрутки

Чтобы узнать, когда пользователь перетащил экран «Исполняется» вниз, проследим за его событием onScrollEnd. Это событие запускается в тот момент, когда пользователь перестает прокручивать.

scroll_now_playing.onScrollEnd ->

Теперь нужно проверить, на достаточное ли расстояние прокрутил пользователь экран вниз. Если это не так, просто позволим компоненту прокрутки отскочить назад.

В оригинальном приложении пользователь должен перетащить экран «Исполняется» на 121 пункт или более, считая от верха экрана, чтобы перейти к мини-плееру.

Экран «Исполняется» уже размещен на расстоянии в 33 пункта от верхнего края экрана, поэтому анимацию запустим, когда пользователь прокрутит на 88 пунктов вниз.

Но, так как мы прокручиваем вниз, а не вверх, как обычно (что также связано с сопротивлением прокрутки), мы проверяем отрицательное значение scrollY, расстояния прокрутки.

(Вы можете поместить print maxi_player.scrollY в обработчике события, чтобы проверить это.)

scroll_now_playing.onScrollEnd ->

if scroll_now_playing.scrollY < -88

Фиксируем положение прокрутки

Когда пользователь прокрутил вниз на достаточное расстояние, мы можем начать переключение. Но встает проблема: экран «Исполняется» будет находиться в состоянии «прокрутки вниз».

Разрешим её, быстро сбросив компонент прокрутки в исходное состояние, вот так:

Перед началом анимации переместим компонент прокрутки вниз, а его содержимое — вверх. Делаем это мгновенно, без анимации.

Хорошо, шаг за шагом:

# Заставляем компонент перейти в такое же положение,
# что и его контентный слой
scroll_now_playing.y = scroll_now_playing.content.y + 33

(Обратите внимание, что здесь используется не scrollY, а у-позиция содержимого слоя, которая увеличивается при прокрутке вниз.)

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

Теперь мы перемещаем содержимое обратно вверх:

    # … и устанавливаем содержимое в исходное положение 
scroll_now_playing.scrollToPoint
y: 0
no

Функция scrollToPoint() выполняет то, что указано в её названии: она позволяет прокручивать до определенной точки. Установив её «animate» аргумент в состояние no сделаем так, что это будет происходить мгновенно, без анимации.

Всё вместе это должно выглядеть так:

scroll_now_playing.onScrollEnd ->

if scroll_now_playing.scrollY < -88 # 121 пунктов минус 33

# Заставляем компонент перейти в такое же положение,
# что и его контентный слой
scroll_now_playing.y = scroll_now_playing.content.y + 33

# … и устанавливаем содержимое в исходное положение
scroll_now_playing.scrollToPoint
y: 0
no

Попробуйте. Можно прокручивать вверх и вниз всё, что захочется, но как только вы потянете достаточно далеко вниз, прокрутка остановится на том месте, где вы отпустили экран.

Прокрутка останавливается после смещения более чем на 88 пунктов

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

Первый набор анимаций

Первый набор из шести анимаций запускается сразу, а длительность всех их будет — треть секунды.

# -- Первый набор анимаций, в течение одной трети секунды -- #
firstSetDuration = 0.3

За 0.3 секунды мы:

  • покажем мини-плеер (opacity);
  • скроем за ним прозрачный серый оверлей за ним (opacity);
  • расположим экран «Library» на заднем плане (scaleX, y, borderRadius) …
  • … и сделаем то же самое для экрана «For You»;
  • снова сделаем строку состояния чёрной (invert);
  • и переместим панель вкладок вверх (y).

Поехали.

Отображение мини-плеера: снова делаем его visible и анимируем его opacity обратно до 1.

Mini_Player.visible = yesMini_Player.animate
opacity: 1
options:
time: firstSetDuration

Скроем прозрачный серый оверлей, анимировав его opacity до нуля.

overlay.animate
opacity: 0
options:
time: firstSetDuration

Затем переместим scroll_library и scroll_for_you обратно в верхнюю часть экрана, установим их первоначальный горизонтальный масштаб и радиус закругления углов.

scroll_library.animate
scaleX: 1
y: 0
borderRadius: 0
options:
time: firstSetDuration
scroll_for_you.animate
scaleX: 1
y: 0
borderRadius: 0
options:
time: firstSetDuration

(Первоначально мы только изменили scroll_library, но после использования прототипа один из них может оказаться на заднем плане.)

Ранее мы сделали строку состояния белой, изменив её invert; теперь возвращаем параметру этого фильтра его значение по умолчанию: 0.

$.Status_Bar.animate
invert: 0
options:
time: firstSetDuration

Нижняя часть панели вкладок — maxY. Сделав её такой же, как высота экрана, Screen.height, вернём панель вкладок обратно в видимую часть экрана.

$.Tabs.animate
maxY: Screen.height
options:
time: firstSetDuration

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

Установим их длительность в 3 секунды …

# -- Первый набор анимаций, в течение одной трети секунды -- #
firstSetDuration = 0.3 * 10

… как я сделал для GIF ниже:

👀 просмотреть — 🖥 открыть во Framer

Второй набор анимаций

Следующие две анимации также начинаются сразу, но они медленнее, и у них есть еле заметный отскок.

# -- Второй набор анимаций: 0,7 секунды -- #
secondSetDuration = 0.7

За 0.7 секунды мы:

  • переместим весь экран «Исполняется» (который включает мини-плеер) вниз (y, borderRadius);
  • сделаем обложку альбома соответствующей мини-плееру (анимация состояния).

Мы не хотим смещать scroll_now_playing полностью за пределы экрана, так как мини-плеер должен быть видимым, поэтому перемещаем верхнюю часть экрана «Исполняется» на высоту панели вкладок + высоту мини-плеера.

scroll_now_playing.animate
y: Screen.height - $.Tabs.height - Mini_Player.height + 1
borderRadius:
topLeft: 0
topRight: 0
options:
time: secondSetDuration
curve: Spring(damping: 0.77)

По-видимому, необходимо добавить 1 дополнительный пункт, чтобы избежать возникновения зазора.)

Избавляемся также от радиуса закругления углов, потому что, если не сделать этого, получится мини-плеер с закругленными углами.

Добавленная кривая Spring обладает лишь небольшой упругостью с damping (демпфированием) равным 0.77 вместо 0.5 по умолчанию.

Используем эту же кривую при сжатии $.Album_Cover в его "mini" состояние:

$.Album_Cover.animate "mini",
time: secondSetDuration
curve: Spring(damping: 0.77)

При создании "mini" в него не были включены animationOptions (как и для "playing" и "paused" состояний), но здесь можно добавить желаемые длительность и кривую.

Ниже приведено GIF всех восьми анимаций, замедленных в 10 раз:

👀 просмотреть — 🖥 открыть во Framer

Последняя анимация: скрытие экрана «Исполняется»

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

$.Now_Playing.animate
opacity: 0
options:
delay: 0.5
time: 0.5

(Здесь мы анимируем непрозрачность слоя $.Now_Playing, который находится внутри нашего компонента прокрутки.)

Размытие фона

Теперь, когда видна прозрачность мини-плеера, можно заметить, что чего-то не хватает — размытия фона. Всё, что находится под мини-плеером, должно быть размыто.

Вернитесь к Design, выберите мини-плеер, и добавьте Blur со значением 25, а затем измените тип размытия с Layer на Background.

Вот результат:

👀 просмотреть — 🖥 открыть во Framer

Ах да, теперь, когда мы перешли в мини-плеер, можно «щёлкнуть выключателем»:

# Мини-плеер активен
miniPlayerActive = yes

21. Переход от мини-плеера обратно к экрану «Исполняется»

Теперь желательно вернуть предыдущее состояние. При нажатии на мини-плеер он должен превратиться в экран «Исполняется»

Мы прослушаем событие onTap на слое заднего плана мини-плеера.

Mini_Player_Background.onTap ->

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

Сначала в обработчике события сделаем экран «Исполняется» видимым:

# Показывать экран «Исполняется»,
# чтобы ему не пришлось появляться в процессе
$.Now_Playing.opacity = 1

В любом случае, он находится под мини-плеером, и, сдвигая его, мы не хотим анимировать его непрозрачность.

Первый набор анимаций

Теперь наши анимации. Есть быстрый набор (третья часть секунды) и более медленный набор (полсекунды). Сначала быстрый набор:

# -- Первый набор анимаций, в течение одной трети секунды -- #
firstSetDuration = 0.3

Мы скрываем мини-плеер …

# Исчезает мини-плеер
Mini_Player.animate
opacity: 0
options:
time: firstSetDuration

… и в течение тех-же 0.3 секунды мы опускаем панель вкладок:

# Опустить панель вкладок
$.Tabs.animate
y: Screen.height
options:
time: firstSetDuration

В любом случае, это небольшое движение (по сравнению с всплывающим экраном «Исполняется»).

Вот GIF-анимация того, что происходит (также на скорости в 1/10 от реальной):

👀 просмотреть — 🖥 открыть во Framer

Второй набор анимаций

Второй набор начинается в то же время, но эти анимации выполняются медленнее: 0.5 секунды. Как и в первом наборе, они используют кривую Bezier.ease, установленную по умолчанию.

# -- Второй набор анимаций: полсекунды -- #
secondSetDuration = 0.5

Наиболее заметная анимация — движущийся обратно вверх экран «Исполняется»:

# Анимируем компонент прокрутки вверх
scroll_now_playing.animate
y: 33
borderRadius:
topLeft: 10
topRight: 10
options:
time: secondSetDuration

(Заодно восстанавливаем радиус закругления на верхних углах.)

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

Как известно, объект player в аудиоплеере предоставляет доступ к своему элементу HTML5-аудио. Когда музыка не воспроизводится, одно из свойств этого элемента, paused, будет иметь значение ‘true’ (верно).

if audio.player.paused
$.Album_Cover.animate "paused",
time: secondSetDuration
else
$.Album_Cover.animate "playing",
time: secondSetDuration
curve: Bezier.ease

Указывая параметр time, перезаписываем длительность, заданную при создании состояний.

Кроме того, анимационная кривая в "playing" не должна быть «пружинистой», поэтому замещаем её кривой Bezier.ease.

👀 просмотреть — 🖥 открыть во Framer

Что остаётся? Всё, что происходит на заднем плане под экраном «Исполняется»:

  • прозрачный серый оверлей появляется снова;
  • экран на заднем плане снова становится картой;
  • строка состояния становится белой.

Серый оверлей:

# Показывать прозрачный серый оверлей
overlay.animate
opacity: 1
options:
time: secondSetDuration

Разбираемся с экранами «Library» и «For You»:

# Сжать и переместить экраны на заднем плане
scroll_library.animate
scaleX: 0.93
y: 20
borderRadius: 10
options:
time: secondSetDuration
scroll_for_you.animate
scaleX: 0.93
y: 20
borderRadius: 10
options:
time: secondSetDuration

(Опять же, только один из них будет виден на данный момент.)

Строка состояния:

# Сделать строку состояния белой
$.Status_Bar.animate
invert: 100
options:
time: secondSetDuration

Всё настроено. Теперь надо отключить мини-плеер, чтобы он не запускался непреднамеренно, когда пользователь скролит экран вниз …

Mini_Player.visible = no

… и указываем, что мини-плеер неактивен.

miniPlayerActive = no
👀 просмотреть — 🖥 открыть во Framer

Запретить анимацию обложки альбома, когда мини-плеер активен

Сейчас вы можете спросить, зачем вообще нам нужна эта переменная miniPlayerActive.

Что ж, нажмите кнопку «Воспроизвести» в мини-плеере.

Эти анимации не должны происходить, когда мы в мини-плеере.

Вернитесь к фолду # Animating the album cover.

До сих пор функции onplaying() и onpause() выглядели так:

# Когда музыка начала воспроизводиться
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … а также большие кнопки
$.Button_Play.visible = no
$.Button_Pause.visible = yes

(Функция onpause() содержит похожий код.)

С помощью дополнительной строки с if проверим miniPlayerActive, и только если он не активен (no), изменим состояние $.Album_Cover.

# Когда музыка начала воспроизводиться
audio.player.onplaying = ->
if miniPlayerActive is no
$.Album_Cover.animate "playing"
# Показать и скрыть маленькие кнопки
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … а также большие кнопки
$.Button_Play.visible = no
$.Button_Pause.visible = yes

(Добавьте такую же строку к функции onpause().)

👀 просмотреть — 🖥 открыть во Framer

Готово!

Надеюсь, вам 👏 понравился этот практический урок.

Если да, обратите внимание на мою книгу. В ней содержатся похожие руководства для ещё двух приложений, и многое другое о Framer Code. Есть также бесплатная ознакомительная версия!

The Framer book

--

--