Воссоздание интерфейса приложения Apple Music во Framer
Прокачайтесь с нуля во Framer с помощью этой пошаговой инструкции (в комплекте с видео, файлами для скачивания, и советами по дизайну)
--
Большое спасибо Sergey Voronov и Марату Тынчерову за помощь в переводе.
Есть также переводы этой инструкции на 🇬🇧 английский и 🇪🇸 испанский языки и предыдущая версия на 🇰🇷 корейском языке.
Вы знаете, что в Apple Music экран «Исполняется» можно прокрутить вниз, чтобы магически превратить его в мини-плеер? Я подумал, что было бы круто повторить этот пример во 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 файл содержит экраны, которые нам понадобятся для создания прототипа.
Кстати, в этом файле я использовал новые шрифты SF Pro.
Он содержит пять артбордов:
- Экран для вкладки «Library» («Медиатека»; также включает строку состояния и панель вкладок)
- Экран для вкладки «For You» («Для вас»)
- Артборд со второй карточкой для экрана «For You»: Favourites Mix
- И еще один с третьей карточкой: Chill Mix
- Экран «Исполняется» (“Now Playing”)
Экран «Library» на самом деле намного выше. Его список недавно добавленных альбомов спрятан под маской и его можно найти на странице «Symbols» (Символы).
Я сделал это, чтобы упростить редактирование альбомов. И я сделал то же самое с недавно воспроизведёнными альбомами (Recently Played) на экране «For You».
Начнём!
Создайте новый проект в Framer, сохраните его (например, под названием «Apple Music») и импортируйте файл из Sketch.
Дизайн выполнен с масштабированием 1x. Это значит, что экраны на iPhone 8 имеют размер 375 x 667 пунктов интерфейса. Но импорт во Framer с двукратным масштабированием, 2x, расширит их до размера 750 x 1334 пикселей.
В верхней части вашего проекта появится эта строка:
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
.
2. Делаем экран «Library» прокручиваемым
Сейчас мы видим только экран «Library», потому что другие артборды находятся за пределами экрана справа (с тем же расстоянием между ними, что и в файле Sketch). Мы передвинем их позже, по мере надобности.
Переходя к нижней части панели слоёв, вы увидите, что наш экран Library
(теперь это, очевидно, слой) содержит три дочерних слоя: Status_Bar
, Tabs
, and Library_content
. (У двух последних есть свои собственные «дочки».)
Что ж, содержимое самого экрана находится в 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
пунктов, чтобы освободить место для мини-плеера.
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
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
.)
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
… и убедился, что он остался в конце документа.
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
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
), что и оригинальная карточка.
Теперь мы можем использовать функцию addPage()
для добавления карточек, вот так:
mixes.addPage $.New_Music_mix
mixes.addPage $.Favourites_mix
mixes.addPage $.Chill_mix
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
Для второй карточки «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
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
Я предпочитаю ставить свои функции в начале проекта. Вот и в приведённом выше проекте я сделал отдельный фолд «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» (событие) в любой импортированный слой:
Но ещё нужно активировать правильную вкладку. Сделаем это, добавляя следующие строки к обоим обработчикам события (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
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
Ага, панель вкладок всё ещё мешает. Мы сдвинем её вниз.
Добавьте эту строку, желательно выше, внутри фолда The Tab Bar
:
$.Tabs.y = Screen.height
Панель вкладок разместится чуть ниже экрана.
Позже, переходя от экрана «Исполняется» к мини-плееру, сделаем её появление анимированным.
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
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-адрес.
Теперь вы можете проигрывать музыку. Попробуйте. Нажмите кнопку воспроизведения!
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)"
; - вертикальная тень,
shadowY
—19
пунктов; - нет
shadowSpread
; shadowBlur
—37
пунктов;
(Тень на самом деле окажется на 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"
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 на локальный.
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
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()
(из 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» (большая или малая) была нажата.
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"
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
Попробуйте. Можно прокручивать вверх и вниз всё, что захочется, но как только вы потянете достаточно далеко вниз, прокрутка остановится на том месте, где вы отпустили экран.
Теперь приготовьтесь. У нас будет девять анимаций, с разными таймингами, которые будут работать одновременно.
Первый набор анимаций
Первый набор из шести анимаций запускается сразу, а длительность всех их будет — треть секунды.
# -- Первый набор анимаций, в течение одной трети секунды -- #
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: firstSetDurationscroll_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 ниже:
Второй набор анимаций
Следующие две анимации также начинаются сразу, но они медленнее, и у них есть еле заметный отскок.
# -- Второй набор анимаций: 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 раз:
Последняя анимация: скрытие экрана «Исполняется»
Эта последняя анимация начинается на 0.5
секунды позже, потому что, перед тем как погасить экран под мини-плеером, мы хотим быть уверены, что он находится на своем месте.
$.Now_Playing.animate
opacity: 0
options:
delay: 0.5
time: 0.5
(Здесь мы анимируем непрозрачность слоя $.Now_Playing
, который находится внутри нашего компонента прокрутки.)
Размытие фона
Теперь, когда видна прозрачность мини-плеера, можно заметить, что чего-то не хватает — размытия фона. Всё, что находится под мини-плеером, должно быть размыто.
Вернитесь к Design, выберите мини-плеер, и добавьте Blur со значением 25
, а затем измените тип размытия с Layer на Background.
Вот результат:
Ах да, теперь, когда мы перешли в мини-плеер, можно «щёлкнуть выключателем»:
# Мини-плеер активен
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 от реальной):
Второй набор анимаций
Второй набор начинается в то же время, но эти анимации выполняются медленнее: 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
.
Что остаётся? Всё, что происходит на заднем плане под экраном «Исполняется»:
- прозрачный серый оверлей появляется снова;
- экран на заднем плане снова становится картой;
- строка состояния становится белой.
Серый оверлей:
# Показывать прозрачный серый оверлей
overlay.animate
opacity: 1
options:
time: secondSetDuration
Разбираемся с экранами «Library» и «For You»:
# Сжать и переместить экраны на заднем плане
scroll_library.animate
scaleX: 0.93
y: 20
borderRadius: 10
options:
time: secondSetDurationscroll_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
Запретить анимацию обложки альбома, когда мини-плеер активен
Сейчас вы можете спросить, зачем вообще нам нужна эта переменная 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 Code. Есть также бесплатная ознакомительная версия!