Last update: January 25, 2018.
There are also 🇪🇸 Spanish and 🇷🇺 Russian translations of this tutorial, and an older version in 🇰🇷 Korean.
You know how you can swipe down the “Now Playing” screen in Apple Music to magically transform it into a mini-player? I thought that would be a cool interaction to replicate in Framer.
While I was at it, I also made a few other screens scrollable (and pageable — that’s all easy, anyway). And for bonus points, why not have it actually play music?
Here’s a video of the finished prototype (click the links to view or open it):
Getting Started
Sure, it’s a big prototype (500+ lines), but since I’ll be going through it in meticulous detail, it makes for a great starting place for Framer newbies. If you’ve never used Framer before, know that they offer a 14-day free trial, so download it to follow along.
You don’t have to know anything about Framer before starting this tutorial. Within the post, I’ll link to relevant sections in Framer’s Get Started guides. And links with a gray code
background—like for this layer
—lead to explanations in the Framer docs.
At the end of every section, there are links to 👀 view the prototype in Safari or to directly 🖥 open it in Framer.
The screens are created in Sketch, but we’ll use Framer Design to quickly recreate the mini-player.
We’ll combine different animations (with different timings) to transition from the full-screen player to the mini-player, and back.
Other things we’ll learn how to do in this tutorial:
- Import from Sketch
- Use filters to make layers grayscale, or invert their colors
- Use the Background Blur effect
- Make some elements (Tab Bar and Status Bar) appear on all screens
- Use a module to create a music player that has a (scrubbable) progress bar, volume control, and time playing and time remaining readouts.
- Use a Text Layer
- Create a function that uses bits of JavaScript to show the current day in that Text Layer
- Use ScrollComponents, also inside other ScrollComponents …
- … and use Direction Lock to not have them scroll at the same time
- Wrap masked Sketch groups in a ScrollComponent
- Use a PageComponent
- Use parent layers to resize the pages in that PageComponent
1. Importing the Sketch file
This Sketch file contains the screens we’ll need to build the prototype.
By the way, I used the newer SF Pro fonts in this file.
It contains five artboards:
- The screen for the “Library” tab (also includes the Status Bar and Tab Bar)
- The screen for the “For You” tab
- An artboard with a second card for the “For You” screen: Favourites Mix
- And another one with a third card: Chill Mix
- The “Now Playing” screen
The Library screen is actually a lot higher. Its list of “Recently Added” albums is masked and can be found on the “Symbols” page.
I did this to make it easier to edit the albums. And I did the same with the “Recently Played” albums on the “For You” screen.
Let’s start!
Create a new Framer project, save it (naming it “Apple Music,” maybe), and import the Sketch file.
The design is made at 1x, which means the iPhone 8 screens measure 375 x 667 interface points. But importing into Framer at @2x will render them at 750 x 1334 pixels.
You’ll now have this line at the top of your project:
sketch = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)
By renaming the sketch
variable to $
we’ll have to type less in the next steps …
$ = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)
… because we can now write, for example, $.Status_Bar
instead of sketch.Status_Bar
.
2. Making the “Library” screen scrollable
Currently, we only see the “Library” screen, because the other artboards are all to the right, off-screen (with the same distance between them as in the Sketch file). We’ll move them over later, when we need them.
When you scroll to the bottom of the Layer Panel, you’ll see that our Library
artboard (now a layer, of course) contains three child layers: Status_Bar
, Tabs
, and Library_content
. (The last two have children of their own.)
Well, the content of the screen itself is in Library_content
, and with the ScrollComponent’s wrap()
function we make it scrollable:
scroll_library = ScrollComponent.wrap $.Library_content
Our new ScrollComponent, scroll_library
, will by default be scrollable in all directions, so also sideways, but that’s easily fixed by disabling its scrollHorizontal
.
scroll_library.scrollHorizontal = no
The end of the page is partly hidden by the Tab Bar so we should add a bit of contentInset
:
scroll_library.contentInset =
bottom: $.Tabs.height + 80
I’ve used the height
of $.Tabs
, but added an extra 80
points to make space for the mini-player.
3. Making only the first tab active
Currently, all the tabs are red, while only the first one should be, and the inactive tabs should be gray.
I left them red on purpose because you can tweak a layer’s color in Framer. And removing a color (using grayscale
or saturate
) is especially easy.
We use a for…in
loop to iterate through all of $.Tabs
’s children
and dial down their color saturation to zero, which will make them gray.
They’ll still be a bit too dark, but by also lowering their opacity
to 60% they’ll have the correct shade of gray.
for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = 0.6
And then we can reset these properties for $.Tab_Library
, because that’s our first tab, the one that should be active.
$.Tab_Library.saturate = 100
$.Tab_Library.opacity = 1
4. Making the “For You” screen scrollable
The $.For_you
artboard is off-screen, to the right, so we bring it over by changing its x
position:
$.For_you.x = 0
To make it scrollable, we wrap it, as we did with the “Library” screen.
scroll_for_you = ScrollComponent.wrap $.For_you
Again, a few tweaks to the ScrollComponent:
scroll_for_you.props =
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40
(Instead of writing separate lines for each property you can set them all at once on props
.)
5. Placing the Status Bar and Tab Bar on top of everything
You’ll notice that we’ve lost the Tab Bar, and also the Status Bar. That’s normal because they’re both children of the “Library” artboard.
We can lift them out of $.Library
by making them parentless:
$.Status_Bar.parent = null
$.Tabs.parent = null
Setting their parent
to null
will make them… orphans, I suppose. Their parent is now the screen, and they will also jump to the top of the list.
Just what we wanted!
There’s one problem with this, though. The extra layers we’ll create in the next steps (a ScrollComponent here, a transparent gray overlay there) will also be placed on top of all existing layers.
So we would then have to bring Status and Tab to the front again (and again, and again):
$.Status_Bar.bringToFront()
$.Tabs.bringToFront()
The solution: We make the Status Bar and Tab Bar parentless after everything else, by placing the lines at the end of our project.
So I made a fold that contains this …
# Place the Status Bar and Tab Bar on top of everything
$.Status_Bar.parent = null
$.Tabs.parent = null
… and made sure that it stays at the end of the document.
6. Making the “Recently Played” albums scrollable
The whole “For You” screen is currently scrollable, but that doesn’t inhibit us from making parts of it also scrollable.
The “Recently Played’ section contains many more albums than we can currently see. Let’s make it scroll horizontally.
recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20
These 20
points of content inset will make the last album line up with the “See All” button.
Limiting scroll movement
There is one small thing we need to fix, though. You’ll notice that when scrolling left or right, you can inadvertently also scroll up or down. That’s not how it behaves in the original app.
When you start scrolling in a certain direction, it should block scrolling in the other direction. For this, we have to enable directionLock
on both ScrollComponents. They should look like this:
# ScrollComponent for the whole artboard
scroll_for_you = ScrollComponent.wrap $.For_you
scroll_for_you.props =
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40
directionLock: yes
# ScrollComponent for the Recently Played section
recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20
directionLock: yes
7. A Page Component for the “New Music Mix,” “Favourites Mix,” and “Chill Mix” cards
We want to be able to swipe between the “New Music Mix,” “Favourites Mix,” and “Chill Mix” with one card always snapping to the center of the screen (a carousel). So we’ll use a PageComponent
.
mixes = new PageComponent
frame: $.New_Music_mix.frame # Reusing the card’s frame
parent: $.For_you
scrollVertical: no
directionLock: yes
A layer’s frame
property contains both the layer’s dimensions (width and height) and its position (x and y), so this way the PageComponent will occupy the same space in its parent layer ($.For_you
) as the original card.
Now we can use the addPage()
function to add the cards, like this:
mixes.addPage $.New_Music_mix
mixes.addPage $.Favourites_mix
mixes.addPage $.Chill_mix
8. Showing parts of the other cards
There’s a small detail: A part of the second card should already be visible, as an affordance to signal that you can swipe. (Just like with the “Recently Played” albums, where the third album also peeps in.)
So our cards should be smaller. We need to cut a slice of the first one’s right edge, make the “Favourites Mix” card smaller on both sides, and cut a bit of the left side of “Chill Mix.” We can do this by placing each card inside another layer that will serve as a mask.
(By the way, you can delete the addPage()
lines we used.)
First, a wrapper for the first card:
wrapper1 = new Layer
width: $.New_Music_mix.width - 15
height: $.New_Music_mix.height
backgroundColor: null
clip: yes
We reuse the card’s height
but subtract 15
points from its width
. We get rid of the layer’s default backgroundColor
by setting it to null
, and by enabling clip
, the layer will act as a mask.
Then we place $.New_Music_mix
inside it:
$.New_Music_mix.parent = wrapper1
$.New_Music_mix.y = 0
We now do need to set its vertical position, though. That wasn’t needed earlier because addPage()
automatically corrects x
and y
positions.
And now we add our wrapper as a page to the mixes
PageComponent.
mixes.addPage wrapper1
For the second card, “Favourites Mix,” we do the same:
wrapper2 = new Layer
width: $.Favourites_mix.width - 30 # Cut from both sides
height: $.Favourites_mix.height
backgroundColor: null
clip: yes
$.Favourites_mix.parent = wrapper2
$.Favourites_mix.y = 0 # Reset y-position
$.Favourites_mix.x = -15 # Repositionmixes.addPage wrapper2
With one difference: We move it 15 points to the left.
This way its parent layer, wrapper2
, will cut 15 points off its left side and 15 points off its right side.
And the third card, “Chill Mix,” loses 15 points of its left side:
wrapper3 = new Layer
width: $.Chill_mix.width - 15 # Cut from the left side
height: $.Favourites_mix.height
backgroundColor: null
clip: yes$.Chill_mix.parent = wrapper3
$.Chill_mix.y = 0 # Reset y-position
$.Chill_mix.x = -15 # Repositionmixes.addPage wrapper3
9. Making today’s date dynamic
The top of the “For You” screen shows today’s date. It’s an image, so on the off chance that you’re reading this on June 17, 2023 (which promises to be pleasant Saturday) it’ll be incorrect. But that can easily be fixed with a Text Layer and some lines of custom code.
Adding a Text Layer
Let’s first make a textLayer
with the correct font size, weight, position, and color. When that’s in place, we’ll make its text dynamic with a function.
Our Text Layer:
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
You don’t need to set its fontFamily
because on your Mac it’ll be the same default font as it is on iOS: San Francisco. The fontSize
is apparently 13.5
points. (That’ll be 27 pixels.)
I used "red"
as a contrasting (and temporary) text color to find the correct position more easily.
The existing date is in a separate layer, $.Today_s_date
, and its parent is $.Header_For_You
. By giving our Text Layer the same parent
, we can reuse $.Today_s_date
’s position.
You’ll see that the Text Layer has to move up a bit. Making its y
position 7 pixels less should put it in the correct spot.
y: $.Today_s_date.y - 3.5
A function that outputs today’s date
Now, here’s the function that will give us the current day as a text string:
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() # = a number between 0 and 6
monthNumber = now.getMonth() # = a number between 0 and 11
theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()
return theDateAsText
I’ll explain it line by line.
The first line creates the function, todaysDate
.
todaysDate = ->
The arrow ->
says: “This is a function, and the following lines should run when it’s called.”
The first line in our brand-new function just creates an array, days
, which contains the names of the days of the week …
days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
… and the second line does the same for the names of the months.
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
Then we create now
, a JavaScript date object, using new Date()
.
now = new Date()
We don’t give the Date()
constructor any more information, so by default, it will contain the current date (and also time, actually, up to the millisecond).
A Date
object comes with a lot of built-in functions; we’ll use three of them:
getDay()
, to get the current day of the week. It returns a number between0
and6
. You might think the first day of the week would be Monday (or Saturday)… but in this case, the0
represents Sunday.getMonth()
, to get a number for the current month. No discussion here: the first one will always be January.getDate()
, to get the day in the month. This one doesn’t use zero-based numbering (as the others above do), so it simply starts with1
.
First, we get the numbers for the current day of the week and month and save them in dayOfTheWeekNumber
and monthNumber
.
dayOfTheWeekNumber = now.getDay() # = a number between 0 and 6
monthNumber = now.getMonth() # = a number between 0 and 11
Now we can pull all this together and construct a text string that contains everything.
theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()
The first part, days[dayOfTheWeekNumber]
, picks the correct day of the week out of the array we made earlier, and the second part, months[monthNumber]
, does the same for the name of the month.
We join them (with a comma and space, ", "
, between them), and stick the day of the month at the end with now.getDate()
.
And then the last line of our function return
s that text string.
return theDateAsText
When you call the function and print
it out, like this …
print todaysDate()
… you’ll see today’s date appear in the Console.
Using the function in the Text Layer
We can now use todaysDate()
to set our Text Layer’s 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"
I made two more changes: The correct text color
is actually "#929292"
, and with this textTransform
, all the text will change to "uppercase"
.
Everything looks okay, so we can hide the original Sketch layer by setting its visible
to no
:
$.Today_s_date.visible = no
I prefer to put my functions at the beginning of a project. So in the project above, I made a separate “Functions” fold, just under the Sketch import.
10. Switching between the Tabs
Now that the “For You” screen is also ready, we can make it possible to switch between the two screens.
So when we tap on the “Library” tab this screen should be visible, while the “For You” screen should be hidden.
$.Tab_Library.onTap ->
scroll_library.visible = yes
scroll_for_you.visible = no
And the reverse should happen when tapping the “For You” tab.
$.Tab_For_You.onTap ->
scroll_for_you.visible = yes
scroll_library.visible = no
By the way, here’s how you can quickly add an event to any imported layer:
But the correct tab should also be made active. We do that by adding these lines to both event handlers:
# Make all tabs gray
for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = .6
# Except this one
@saturate = 100
@opacity = 1
The for…in
loop makes all the tabs gray, just like we did earlier, and the two last lines make the current tab red again.
In those last two lines we’re actually writing this:
this.saturate = 100
this.opacity = 1
With ‘this’ being the tab that received the event, the one that was tapped. But instead of ‘this.
’ you can also write ‘@
’.
We add one more line underneath these onTap
handlers because when the prototype starts, the “For You” screen should be hidden:
# Initially hide the “For You” screen
scroll_for_you.visible = no
11. The “Now Playing” screen
The only artboard we haven’t used yet is the “Now Playing” screen. It’s another scrollable one because it also contains lyrics and a list of upcoming songs.
scroll_now_playing = new ScrollComponent
width: Screen.width
height: Screen.height - 33
y: 33
scrollHorizontal: no
directionLock: yes
The ScrollComponent is placed 33
points lower because there’s a gap at the top of the screen. The “Now Playing” screen actually begins 13 points below the 20 points of the Status Bar.
And because it’s placed lower we also subtract those same 33 points from Screen.height
when setting its height
.
By the way, this is how it should look in the end:
Direction lock is enabled because we don’t want the screen to scroll when we change the playback volume or scrub to a different point in the song.
Now the artboard. We bring it over by setting its x
to 0
, and we add it to the ScrollComponent’s content
layer. (Because that’s how you do it when not using wrap()
.)
$.Now_Playing.x = 0
$.Now_Playing.parent = scroll_now_playing.content
You’ll notice that there’s quite a lot of space at the end of the page.
That’s on purpose. Because by now setting a negative value for the contentInset
(at the bottom
) the user will be able to scroll beyond the end of the page (this is called overdrag) without seeing the screen underneath.
Add these lines to the ScrollComponent’s properties:
contentInset:
bottom: -100
Ah, the Tab Bar is still in the way. We’ll move it down.
Add this line, preferably higher up in the code, inside its The Tab Bar
fold:
$.Tabs.y = Screen.height
It places the Tab Bar just below the screen.
Later, when transitioning from the “Now Playing” screen to the mini-player, we’ll animate it back up.
12. Transparent gray overlay behind the “Now Playing” screen
The top of the current screen (“Library” or “For You”) should still be visible underneath the “Now Playing” screen, darkened by a gray overlay.
This overlay can be a simple layer the size of the screen that’s 50% transparent black, like this:
overlay = new Layer
frame: Screen.frame
backgroundColor: "rgba(0,0,0,0.5)"
With the placeBehind()
function we move it underneath the “Now Playing” screen:
overlay.placeBehind scroll_now_playing
A few details are missing, though.
The “Now Playing” screen should have rounded corners, which it does… but not when you scroll up.
And the screen in the back should also look like a card, but one that’s further down in the stack, like this:
How do we add rounded corners? That’s obvious: The ScrollComponent needs some borderRadius
, 10
points of it.
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
(You can set border radius separately on different corners. For the bottom corners, use bottomRight
and bottomLeft
.)
Now, the screen currently under the “Now Playing” screen, scroll_library
, should also look like a card.
We give it the same amount of borderRadius
and move it 20
points down so that it’s just below the Status Bar.
It needs to shrink a bit, but only horizontally: a scaleX
of 93
% seems about right.
scroll_library.props =
borderRadius: 10
y: 20
scaleX: 0.93
Because of this darker background, we should have a light Status Bar when the “Now Playing” screen is visible. Another filter to the rescue: with an invert
of 100
% we make it white.
$.Status_Bar.invert = 100
13. Playing music with the Framer Audio module
Framer’s Benjamin den Boer created a module that makes it very easy to make a music player in Framer.
Download the Framer Audio module as a ZIP:
Unzip it, find the audio.coffee
file (it’s in the ‘src’ folder), and drag it to your project’s window.
You’ll see this line added at the top of your project:
audio = require 'audio'
(And the file will automatically be copied to the “modules” folder inside your project folder.)
As instructed on the module’s GitHub page we’ll change the line to:
{Audio, Slider} = require "audio"
With this module, you create an audio player by wrapping the existing play and pause buttons.
By the way, you did import a “Play” button, but it was a hidden group in Sketch so its visible
was switched off.
Let’s show it.
$.Button_Play.visible = yes
And now we can wrap()
the buttons:
audio = Audio.wrap($.Button_Play, $.Button_Pause)
The resulting player, named audio
, will have the same position as the buttons and also their place in the hierarchy. So the audio player is now also a child of $.Now_Playing
.
We need some music. That can be online music, so we’ll use this 90-second Apple Music preview clip of the Onuka song.
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"
How do we get a preview like that? I searched Apple Music’s catalogue with their online tool.
Then I used Safari’s Web Inspector to see which .m4a
file was being downloaded when I played the music, and copied that URL.
I love this song but have no idea what it’s about, apart from the fact that “misto” (місто) is Ukrainian for “city”.
You can already play the music. Try it. Tap the “Play” button!
14. Animating the album cover
When the music is playing the album art should be full-size, just like it is now, and when paused the album cover shrinks (and also loses most of its shadow).
FYI: The shadow in the original app is actually more of a blurred version of the album cover, but since our cover is black we’ll keep it simple and use a shadow.
To animate between these two states we’ll use, of course, States.
But first, we have to set up a few things.
Setup
Later on, we’ll show this same album cover very small on the mini-player… and have the whole “Now Playing” screen disappear. That’s why we should lift the album cover layer out of its parent, and put it directly in the ScrollComponent.
That’s easily done though, with just one line:
$.Album_Cover.parent = scroll_now_playing.content
It’s now still in the ScrollComponent, but independently, as a sibling of the “Now Playing” screen. (And we didn’t even have to correct its position.)
Next, we need to get rid of the existing (static) shadow. I made it a separate group in the Sketch document, so you can just make this $.Album_Cover_shadow
layer invisible.
$.Album_Cover_shadow.visible = no
Creating the “playing” and “paused” states
We can now define the states.
When the music is playing, the album cover should look like this:
- It’s shown at its full 311 x 311 points — so its
scale
is1
- The shadow’s color is 40% black —
"rgba(0,0,0,0.4)"
- The shadow is projected downwards —
shadowY
is20
points - … but also outwards in all directions — a
shadowSpread
of10
points - (There’s no
shadowX
) - The shadow’s blur is also high —
50
points of Gaussian blur (shadowBlur
)
And when the music is paused, it should look like this:
- The album is 249 x 249 — which makes for a
scale
of0.8
- The shadow is very light: only 10% black —
"rgba(0,0,0,0.1)"
- A
shadowY
of19
- No
shadowSpread
- A
shadowBlur
of37
points
(The shadow will actually end up being 20% smaller because of the scale change.)
To keep it simple we’ll call our states "playing"
and "paused"
. We can define them both at the same time:
$.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
I’ve also included the original frame
of the layer in each state. This is because, later on, we’ll add a third state for the mini-player in which we’ll change its position.
And I’ve also included animationOptions
:
- Animating to the
"playing"
state takes0.8
seconds, but it seems faster because it ends with a soft (dampened) bounce. - There’s no bounce when animating back to
"paused"
(we use the defaultBezier.ease
curve), and that animation’s duration is0.5
seconds.
To test the animations, we can stateCycle()
between them with a tap on the album cover:
$.Album_Cover.onTap ->
this.stateCycle "paused", "playing"
(By including their names the "default"
state will be ignored.)
That looks okay.
You can delete the onTap()
event for now because we’ll trigger these state animations with the starting and stopping of the music.
With stateSwitch()
we can switch a layer to a certain state without animating it. We use this function to make "paused"
the initial state.
$.Album_Cover.stateSwitch "paused"
Animating between the states when the music starts and stops
Now, we could use onTap
events on the “Play” and “Pause” buttons to trigger these animations, like this…
$.Button_Play.onTap ->
$.Album_Cover.animate "playing"
…but later we’ll have two more buttons: the ones on the mini-player.
So we’ll do it differently. We’re going to listen to the audio player’s playing
and pause
events.
The audio player has a player
object, which is the HTML5 audio element that actually plays the music. And apparently, we can add functions to it that will be run when an event occurs. You do this by creating a function in player
with an on
in front of the event name.
So playing
and pause
become onplaying
and onpause
.
# When the music started playing
audio.player.onplaying = ->
$.Album_Cover.animate "playing"# When the music is paused
audio.player.onpause = ->
$.Album_Cover.animate "paused"
15. Recreating the progress bar and timers
Adding the progress bar
The Framer Audio module uses sliders for its progress bar and volume slider.
You customize a SliderComponent to look like you want (or create one in Design) and then pass it to the audio player.
Try this slider:
progressBar = new SliderComponent
width: 311
height: 3
backgroundColor: "#DBDBDB"
knobSize: 7
x: Align.center
y: 363
parent: $.Now_Playing
It’s as wide as the album cover, 311
points, and it’s thin, only 3
points high. The light gray of the slider is "#DBDBDB"
.
The size of the knob, knobSize
, is also quite small, only 7
points.
By making $.Now_Playing
its parent
it’ll be placed where it belongs: inside the “Now Playing” screen. We use Align.center
to center it horizontally and place it 363
points from the top.
The left part of a SliderComponent is its fill
, a child layer, so we have to set its color separately:
progressBar.fill.backgroundColor = "#8C8C91"
And the same is true for the knob
, which gets the same color as the fill:
progressBar.knob.props =
backgroundColor: progressBar.fill.backgroundColor
shadowColor: null
(We get rid of its default shadow by setting shadowColor
to null
.)
Now, to activate the slider, you pass it to the audio player’s showProgress()
function.
audio.showProgress progressBar
Adding the Time Played counter
Just under the progress bar, on the left, we want the Time Played counter. Same deal — you make a Text Layer, and then pass it to the audio player.
timePlayed = new TextLayer
fontSize: 14
color: progressBar.fill.backgroundColor
x: progressBar.x
y: progressBar.y + 5.5
parent: $.Now_Playing
It uses the default (iOS and Mac) San Francisco font with a size of 14
, and we want it to be placed 5.5
points below the progressBar
. The text’s color
is the same as the progress bar’s fill
.
Then, to have it update when the music plays, you pass it to the audio player’s showTime()
function.
audio.showTime timePlayed
Adding the Time Remaining counter
There’s also a Time Remaining counter, which counts backward.
It has the same text properties as the timePlayed
counter, so we can just copy that one…
timeRemaining = timePlayed.copy()
…and then change a few properties to move it to the right:
- Its right edge,
maxX
, should be aligned with theprogressBar
’s. - Its text should be right-aligned.
- And it needs a fixed width for right-aligning text to work.
timeRemaining.props =
textAlign: Align.right
width: 60
maxX: progressBar.maxX
parent: $.Now_Playing
You pass it to the audio player with a showTimeLeft()
.
audio.showTimeLeft timeRemaining
You’ll notice that everything is now placed precisely on top of the Sketch $.Progress_bar
, so we can hide it:
$.Progress_bar.visible = no
By the way, you can now see when the music has loaded. When the Time Remaining counter changes to the correct length, -1:29
, it means that the file is ready. When this takes too long, you can download the .m4a
file and save it inside your project folder (best in a new “sounds” folder). Of course, you’ll then have to change the URL to a local one.
16. Recreating the volume slider
As you would expect, the volume slider is also a SliderComponent.
volumeSlider = new SliderComponent
width: 266
height: 3
backgroundColor: progressBar.backgroundColor
knobSize: 28
x: 50
y: 559
parent: $.Now_Playing
value: 0.75
This slider’s backgroundColor
is the same as the progressBar
’s.
The volume in the design was set at 75%, so we follow that by setting the SliderComponent’s value
.
Its fill
also has the same color as the progressBar
’s.
volumeSlider.fill.backgroundColor = progressBar.fill.backgroundColor
The knob
keeps its default white color but has a different shadow, and it has a very thin border of only 0.5
points.
volumeSlider.knob.props =
borderColor: "#ccc"
borderWidth: 0.5
shadowY: 3
shadowColor: "rgba(0,0,0,0.2)"
shadowBlur: 4
The slider should now look the same as the one in the Sketch design.
Before adding it, we’ll set the actual volume of the audio player also at 75
%.
audio.player.volume = 0.75
And analogous to the earlier showProgress()
, showTime()
and showTimeLeft()
functions there’s also a showVolume()
:
audio.showVolume volumeSlider
We can now make the original Sketch layer disappear:
$.Volume_slider.visible = no
17. Drawing the mini-player in Framer Design
When you drag the “Now Playing” screen downwards, it should transition to a small mini-player just above the Tab Bar.
That mini-player is not in our Sketch file. It’s simple, though: a transparent background with a mini version of the album art, the song title, and a few buttons.
So… code break! Type ⌘1
to switch to Design.
Your Design screen will still be empty. Start by adding an ‘Apple iPhone 8’ frame.
As a template for the correct dimensions and position, I made a screenshot that you can find here. Just drag it to the frame.
It comes with an added “Play” button because we’ll need one of those as well.
It’ll be too big because of its retina resolution, but, just as in Sketch (or Framer Code), you can use calculations in the property fields. So by changing its width of 750
to 750/2
it will assume the correct size.
It’s best to lock the template, so that you can’t accidentally select it or drag it. Select it and type ⌘L
(or right-click it and select “Lock”).
Frames vs. Shapes
Earlier, all objects drawn in Design simply became layers, but since Version 107 we now have Frames and Shapes.
In short:
- Shapes are for precise drawing — Frames are for layout
- Only Frames can have layout constraints
- Frames are just
layers
in code land, but Shapes are something new:SVGLayers
- Or in HTML lingo: Frames are
<div>
elements, and Shapes<svg>
elements
For more info, see this Framer Help article.
Let’s zoom in and start with the “Play” button.
The “Play” button
We need a triangle. You can use the Polygon tool, make a three-sided polygon, and rotate it, but it’s probably easier to go straight for the Path tool. It’ll give you more control. (You can also make a polygon, double-click it to turn it into a path, and then make corrections.)
Give the triangle a black fill.
The triangle alone would make for a tiny and not easily tappable button, so we’ll make it bigger by drawing a square on top of it. (Now you see why I added those blue outlines to the template.)
Draw a Frame that follows the blue outline. It should be 40
points.
Because it’s bigger (and also because it’s not a Shape or Path) it will automatically become the parent of the triangle, which is exactly what we want.
Change its name to Mini Button Play
, and make it transparent by switching off its Fill.
The “Pause” button
The “Pause” button is easy: two rectangles with a bit of border radius.
You could make them with the Frame tool, but it’s better to use the Rectangle tool, because a Shape can be positioned on sub-point values. You’ll notice that the correct y-position for the rectangles will be 576.5
points.
The border radius seems to be ± 1
point.
Just like with the “Play” button we make the tappable area bigger by drawing a frame on top of it, which we’ll name Mini Button Pause
(and also make transparent by switching off its Fill).
The “Next” button
Two triangles. You can ⌘D
duplicate the “Play” triangle to have something to start from.
We will not actually use this button in our prototype, but we can clean things up a bit by selecting these two and typing ⌘↩
to put them in a parent frame …
… which we‘ll name Mini Button Next
.
The track name
The title of the song is in Apple’s SF Pro Text
(or SF UI Text
) with a Regular weight, 17
points big, and with a letter spacing of -0.4
.
The album art
When transitioning from the “Now Playing” screen to the mini-player the big album cover will shrink, so we actually don’t need album art on the mini-player. But by drawing it here in Design, we’ll have the correct position and shadow at our disposal in Code.
The album art image is 48
points square and has a border radius of 3
points. You can simply draw a frame and leave it in the default transparent blue. (We’ll hide it afterwards anyway.)
Its shadow should be 30%
black, with a y-offset of 3
points and a blur of 10
points.
Name the frame Mini Album Cover
.
The mini-player’s background
We need a separate frame for the player’s background. Later, you’ll see why.
The background should be 375
by 64
points, and its color is a very light off-white, #F6F6F6
, that is 50%
opaque. Name it Mini Player Background
.
Mini Player Background
probably just became the parent of all other objects, so it’s best to select its children in the layer list and drag them out again.
Line at the top
The mini-player has a thin line at the top. You could draw one with the Path tool, but it’s easier to just give Mini Player Background
a top border. It should be 0.5
points thick and have #AEAEAE
as its color.
The “Mini Player” parent layer
Now we can select all objects, do ⌘↩
“Add Frame,” and name the new encompassing frame Mini Player
.
Setting Targets
We need to set targets for the Frames that we want to use in Code. Give the following ones a target by clicking their little circle:
Mini Player
Mini Album Cover
Mini Button Pause
Mini Button Play
Mini Button Background
Your layer list should now look more or less like this:
All set. We don’t need the template anymore so we can make Mini player.png
invisible by right-clicking it and selecting “Hide” (⌘;
).
We also don’t need the (default) white background of the Apple iPhone 8
frame, so we set the opacity of its Fill to 0
%.
18. Tweaking the mini-player in Code
How we’ll switch between the “Now Playing” screen and the mini-player
Okay, here’s the trick. The mini-player will be inside our “Now Playing” screen, all the time. We just hide it when “Now Playing” is shown, like this:
The album cover is a separate layer, as you know, which we resize and reposition when transitioning between the big and the small players.
Positioning the mini-player
We place Mini_Player
inside the “Now Playing” screen’s ScrollComponent and change its y
to zero so that it’s placed at the top.
Mini_Player.props =
parent: scroll_now_playing.content
y: 0
Positioning the “Play” and “Pause” buttons
We should rearrange the buttons. The “Play” button should be visible at first, in the spot where the “Pause” button is now.
First, we give Mini_Button_Play
the same horizontal position as Mini_Button_Pause
…
Mini_Button_Play.x = Mini_Button_Pause.x
… and then we hide Mini_Button_Pause
.
Mini_Button_Pause.visible = no
Making the “Play” and “Pause” buttons react to taps
We’ll let these buttons also play()
and pause()
the music, with these (HTML5 audio) functions on the audio module’s player
object:
Mini_Button_Play.onTap ->
audio.player.play()
Mini_Button_Pause.onTap ->
audio.player.pause()
Now, we could use these same onTap
event handlers to make the buttons appear and disappear (to switch between the “Pause” and “Play” buttons).
But we were already listening to a few of the player
’s events to know when the music started or stopped. If you remember, we’re using them to grow and shrink the album cover.
Go back to the Animating the Album Cover
fold and add these lines:
# When the music started playing
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Show and hide the small buttons
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes# When the music is paused
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Show and hide the small buttons
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no
This way the small buttons will also change when we use the big buttons on the “Now Playing” screen.
To make the reverse also work (the big buttons should change when we tap the small ones), we add similar lines for $.Button_Play
and $.Button_Pause
.
# When the music started playing
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Show and hide the small buttons
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … and also the big buttons
$.Button_Play.visible = no
$.Button_Pause.visible = yes# When the music is paused
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Show and hide the small buttons
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no
# … and also the big buttons
$.Button_Play.visible = yes
$.Button_Pause.visible = no
Now all buttons will change at the same time, independent of which “Play” or “Pause” button (big or small) was tapped.
19. Small version of the album cover on the mini-player
We’ll now make an extra state for the album cover, in which it’ll be small and positioned on the mini-player, with the correct shadow.
But first, as you noticed when playing the music, the album cover is behind the mini-player. That’s easily fixed with a placeBefore()
:
$.Album_Cover.placeBefore Mini_Player
Now, for this new state, "mini"
, we can copy the properties of Mini_Album_Cover
, that tiny album cover we created in Design. We’ll use its frame
, shadowColor
, shadowY
, and 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
… but we also set shadowSpread
and scale
, because these properties were changed by the other states. (Mini_Album_Cover
just has the default values: a shadowSpread
of 0
and a scale
of 1
.)
We actually don’t want Mini_Album_Cover
to be visible; we just wanted to copy its properties. So we can hide it:
Mini_Album_Cover.visible = no
Now, as a test, you can animate to the new "mini"
state with a tap on the album cover:
$.Album_Cover.onTap ->
$.Album_Cover.animate "mini"
20. Transitioning from the “Now Playing” screen to the mini-player
Everything looks okay. So we can hide the mini-player, for now.
Mini_Player.opacity = 0
We use opacity
because we’ll want to animate it.
But we also don’t want it to be tapped by accident when the user swipes down the “Now Playing” screen, so we also switch off its visible
.
Mini_Player.visible = no
Later we’ll need to know when the mini-player is in use (you’ll see why). We create a variable for that, miniPlayerActive
, which at this point will still be ‘no
’.
miniPlayerActive = no
Listening to the scroll movement
To know when the user has dragged down the “Now Playing” screen we’ll listen to its onScrollEnd
event. This event gets triggered the moment the user stops scrolling.
scroll_now_playing.onScrollEnd ->
Now we need to check if the user has scrolled down far enough. If they haven’t, we’ll just let the ScrollComponent bounce back.
In the original app, the user has to drag the “Now Playing” screen 121 points or more from the top of the screen to have it transition to the mini-player.
The “Now Playing” screen is already placed 33 points from the top, so we’ll start our animation when the user has scrolled down 88
points.
But since we’re scrolling down, and not up like as we would normally (which is also why there’s some scroll resistance), we’re checking for a negative value of scrollY
, the scroll distance.
(You can put a print maxi_player.scrollY
in the event handler to test this.)
scroll_now_playing.onScrollEnd ->
if scroll_now_playing.scrollY < -88
Freezing the scroll position
Now, when the user has scrolled this far down we can start the transition, but we face a problem: the “Now Playing” screen will be in a “scrolled down” state.
We’ll solve this by quickly resetting the ScrollComponent to its initial state, like this:
Before starting the animations, we’ll move the ScrollComponent down while moving its content up. We do it instantly, without animation.
Okay, step by step:
# Make the ScrollComponent jump to its content’s position
scroll_now_playing.y = scroll_now_playing.content.y + 33
(Note that we’re not using scrollY
here, but the content
layer’s y
position, which increases when scrolling down.)
So no matter how far the user has scrolled down, our ScrollComponent will always end up in the correct place.
Now we move the content back to the top:
# … and reset the content to its initial position
scroll_now_playing.scrollToPoint
y: 0
no
The scrollToPoint()
function does what it says: it lets you scroll to a certain point. By setting its ‘animate’ argument to no
, this will happen instantly, without animation.
All together it should look like this:
scroll_now_playing.onScrollEnd ->
if scroll_now_playing.scrollY < -88 # 121 points minus 33
# Make the ScrollComponent jump to its content’s position
scroll_now_playing.y = scroll_now_playing.content.y + 33
# … and reset the content to its initial position
scroll_now_playing.scrollToPoint
y: 0
no
Please try it. You can scroll up and down all you want, but once you pull down far enough, it will stay at the spot where you released it.
Now get ready. We’ll have nine animations, with different timings, all run at the same time.
First set of animations
The first set of six animations starts immediately, and the duration for all of them will be a third of a second.
# -- First set of animations, over a third of a second -- #
firstSetDuration = 0.3
In 0.3
seconds we’ll:
- Show the mini-player (
opacity
) - Hide the transparent gray overlay behind it (
opacity
) - Reset the “Library” screen in the back (
scaleX
,y
,borderRadius
) … - … and do the same for the “For You” screen
- Make the Status Bar black again (
invert
) - And move the Tab Bar up (
y
)
Here we go.
Showing the mini-player: We make it visible
again and animate its opacity
back to 1
.
Mini_Player.visible = yesMini_Player.animate
opacity: 1
options:
time: firstSetDuration
Hide the transparent gray overlay by animating its opacity
to zero.
overlay.animate
opacity: 0
options:
time: firstSetDuration
Next, we move scroll_library
and scroll_for_you
back to the top of the screen, reset their horizontal scale, and remove their border radius.
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
(Initially, we only changed scroll_library
, but after use of the prototype either one of them might be in the background.)
Earlier we made the Status Bar white by changing its invert
; we now dial it back to the default value: 0
.
$.Status_Bar.animate
invert: 0
options:
time: firstSetDuration
By setting the Tab Bar’s bottom, maxY
, to the bottom of the screen, it will slide back up.
$.Tabs.animate
maxY: Screen.height
options:
time: firstSetDuration
Because we used a variable, firstSetDuration
, to set the duration for these animations, we can slow all of them down to better observe what’s happening.
Like having them animate with a duration of 3 seconds …
# -- First set of animations, over a third of a second -- #
firstSetDuration = 0.3 * 10
… as I did for the GIF below:
Second set of animations
The next two animations also start immediately but are slower, and they have a subtle bounce.
# -- Second set of animations: 0.7 seconds -- #
secondSetDuration = 0.7
In 0.7
seconds we’ll:
- Move the whole “Now Playing’ screen (which includes the mini-player) downwards (
y
,borderRadius
) - Make the album cover fit on the mini-player (a state animation)
We don’t want to animate everything off-screen because the mini-player should still be visible, so we move the top of the “Now Playing” screen to the height of the Tab Bar + the height of the mini-player.
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)
(Apparently, we need to add 1
extra point to not have a small gap appear.)
We also get rid of the border radius because otherwise, we would have a mini-player with rounded corners.
The added Spring
curve is only slightly bouncy, with a 0.77
damping
instead of the default 0.5
.
And we use this same spring curve when shrinking $.Album_Cover
to its "mini"
state:
$.Album_Cover.animate "mini",
time: secondSetDuration
curve: Spring(damping: 0.77)
We did not include animation options when creating "mini"
(as we did for the "playing"
and "paused"
states), but we can add the desired duration and curve here.
Here’s a GIF of all eight animations at a tenth of their speed:
Last animation: Hiding the “Now Playing” screen
This last animation starts 0.5
seconds later because we want to be sure that the mini-player is in place before fading out the screen underneath it.
$.Now_Playing.animate
opacity: 0
options:
delay: 0.5
time: 0.5
(Here we’re animating the opacity of the $.Now_Playing
Sketch layer that’s inside our ScrollComponent.)
Background Blur
Now that you can see the mini-player’s transparency you’ll notice something is missing: Background Blur. Whatever is underneath the mini-player should be blurred.
Go back to Design, select the mini-player, give it a Blur of 25
, and then change this blur from Layer to Background.
Here’s the result:
Ah, and now that we switched to the mini-player we can also “flip the switch”:
# The mini-player is now active
miniPlayerActive = yes
21. Transitioning from the mini-player back to “Now Playing”
Now we want to get back. When the user taps the mini-player, it should sprout into the “Now Playing” screen.
We’ll listen for an onTap
on the mini-player’s background layer.
Mini_Player_Background.onTap ->
Why the background? Because this way we can continue to use the “Play” and “Pause” buttons on the mini-player without also triggering this transition.
In the event handler, we start by making the “Now Playing” screen visible:
# Show the “Now Playing” screen, so that it doesn’t have to fade in
$.Now_Playing.opacity = 1
It’s underneath the mini-player anyway, and we don’t want to animate its transparency while sliding it up.
First set of animations
Now, our animations. There’s a fast set (one-third of a second) and a slower set (half a second). First the fast set:
# -- First set of animations, over a third of a second -- #
firstSetDuration = 0.3
We hide the mini-player …
# Fade out the mini-player
Mini_Player.animate
opacity: 0
options:
time: firstSetDuration
… and in the same 0.3
seconds we lower the Tab Bar:
# Lower the Tab Bar
$.Tabs.animate
y: Screen.height
options:
time: firstSetDuration
It’s a small movement anyway (compared to the whole “Now Playing” screen sliding up).
Here’s a GIF of what’s happening (also at one-tenth of the speed):
Second set of animations
The second set starts at the same time, but these animations run slower: 0.5
seconds. Just like the first set, they use the default Bezier.ease
curve.
# -- Second set of animations: half a second -- #
secondSetDuration = 0.5
The most obvious animation is the “Now Playing” screen moving back up:
# Animate the ScrollComponent upwards
scroll_now_playing.animate
y: 33
borderRadius:
topLeft: 10
topRight: 10
options:
time: secondSetDuration
(We also restore the border radius on the top corners.)
And at the same time, we want to bring the album cover back to its bigger size. But, we should check if the music is playing, so that we can animate it to the correct state.
As you know, the player
object on the audio player gives us access to its HTML5 “audio” element. One of this element’s properties is paused
, which will be ‘true’ when the music isn’t playing.
if audio.player.paused
$.Album_Cover.animate "paused",
time: secondSetDuration
else
$.Album_Cover.animate "playing",
time: secondSetDuration
curve: Bezier.ease
By giving these animations a time
we override the durations that were set when creating the states.
We also don’t want the spring curve contained in "playing"
, so we override it with a Bezier.ease
curve.
What remains are the things that happen in the background, underneath the “Now Playing” screen:
- The transparent gray overlay should fade in again
- The screen in the back should also become a card
- The Status Bar should be white again
The gray overlay:
# Show the transparent gray overlay
overlay.animate
opacity: 1
options:
time: secondSetDuration
Dealing with the “Library” and “For You” screens:
# Shrink & move the screens in the back
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
(Again, only one of them will be visible at this point.)
The Status Bar:
# Make the Status Bar white
$.Status_Bar.animate
invert: 100
options:
time: secondSetDuration
All set. We should now make the mini-player untappable so that it can’t be triggered inadvertently when the user swipes down…
Mini_Player.visible = no
… and we register that the mini-player is not active.
miniPlayerActive = no
Prevent album cover animations when the mini-player is active
By now you might be asking why we even need this miniPlayerActive
variable.
Well, tap the “Play” button in the mini-player.
These animations should not happen when we’re in the mini-player.
Go back to the # Animating the album cover
fold.
Until now the onplaying()
and onpause()
functions looked like this:
# When the music started playing
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Show and hide the small buttons
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … and also the big buttons
$.Button_Play.visible = no
$.Button_Pause.visible = yes
(The onpause()
function will contain similar code.)
With an extra if
line we’ll check for miniPlayerActive
, and only when it’s not active (no
) we change the state of $.Album_Cover
.
# When the music started playing
audio.player.onplaying = ->
if miniPlayerActive is no
$.Album_Cover.animate "playing"
# Show and hide the small buttons
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … and also the big buttons
$.Button_Play.visible = no
$.Button_Pause.visible = yes
(Add the same line to the onpause()
function.)
Done!
I hope you 👏 liked this tutorial.
If you did, check out my book. It has similar tutorials for two more apps, and a whole lot more about Framer Code. And there’s a free preview version!