Recreating the Apple Music App in Framer

Go from Framer zero to hero with this step-by-step tutorial (complete with videos, downloadable files and design tips).

Tes Mat
Framer
38 min readAug 1, 2017

--

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

👀 view in browser — 🖥 open in Framer

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.

The five artboards in our Sketch file

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.

The “Symbols” page in the Sketch file, with the Recently Added albums for “Library” and Recently Played albums for “For You”

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.

Importing the Sketch file at 2x retina resolution, changing the device frame to ‘iPhone 8’, and shortening ‘sketch’ to ‘$’

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.

👀 view in browser — 🖥 open in Framer

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

In the Layer Panel: “Library” and its child layers

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.

👀 view in browser — 🖥 open in Framer

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
👀 view in browser — 🖥 open in Framer

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

👀 view in browser — 🖥 open in Framer

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.

The Tab Bar and Status Bar in the Layer Panel

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.

👀 view in browser — 🖥 open in Framer

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.

The “Recently Played” list is now also scrollable

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
👀 view in browser — 🖥 open in Framer

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.

A PageComponent for the “mixes” cards; it’s transparent gray because it’s still empty

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
👀 view in browser — 🖥 open in Framer

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
The “New Music Mix” card is now masked by a parent layer

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 # Reposition
mixes.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 # Reposition
mixes.addPage wrapper3
👀 view in browser — 🖥 open in Framer

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 between 0 and 6. You might think the first day of the week would be Monday (or Saturday)… but in this case, the 0 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 with 1.

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 returns 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
👀 view in browser — 🖥 open in Framer

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:

Pro tip: You can add an event, animation, or state to any layer by right-clicking its name in the Layer Panel

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
👀 view in browser — 🖥 open in Framer

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:

The other screen in the background and the drag handle signal that you can drag down to return

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.

The ScrollComponent for the “Now Playing” screen

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.

Extra space at the end of “Now Playing”

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
Extra “overdrag” space

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.

👀 view in browser — 🖥 open in Framer

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.

The “Now Playing” screen doesn’t have rounded corners

And the screen in the back should also look like a card, but one that’s further down in the stack, like this:

The “Now Playing” screen and the one underneath resemble stacked cards

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
👀 view in browser — 🖥 open in Framer

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!

👀 view in browser — 🖥 open in Framer

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:

How the album cover should look with music playing
  • It’s shown at its full 311 x 311 points — so its scale is 1
  • The shadow’s color is 40% black — "rgba(0,0,0,0.4)"
  • The shadow is projected downwards — shadowY is 20 points
  • … but also outwards in all directions — a shadowSpread of 10 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:

How the album cover should look when the music is paused
  • The album is 249 x 249 — which makes for a scale of 0.8
  • The shadow is very light: only 10% black — "rgba(0,0,0,0.1)"
  • A shadowY of 19
  • No shadowSpread
  • A shadowBlur of 37 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 takes 0.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 default Bezier.ease curve), and that animation’s duration is 0.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.)

Testing the states animation of the album cover

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"
👀 view in browser — 🖥 open in Framer

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 the progressBar’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.

👀 view in browser — 🖥 open in Framer

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
👀 view in browser — 🖥 open in Framer

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.

A bigger parent layer to make for a bigger button

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.

Shapes can be positioned and sized more precisely than Frames

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 …

Selecting “Add Frame” from the right-click menu

… 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 mini-player is hidden in the “Now Playing” screen, and the “Now Playing” screen never entirely scrolls off-screen

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
The mini-player is now inside the “Now Playing” screen’s ScrollComponent

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
The “Play” button now has its correct position, and the “Pause” button is hidden

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.

👀 view in browser — 🖥 open in Framer

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"
👀 view in browser — 🖥 open in Framer

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.

Scrolling stops once scrolled down more than 88 points

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:

👀 view in browser — 🖥 open in Framer

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:

👀 view in browser — 🖥 open in Framer

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:

👀 view in browser — 🖥 open in Framer

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

👀 view in browser — 🖥 open in Framer

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.

👀 view in browser — 🖥 open in Framer

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
👀 view in browser — 🖥 open in Framer

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.

The album art grows and shrinks as instructed

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

👀 view in browser — 🖥 open in Framer

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!

The Framer book

--

--