In this tutorial, we will be building a custom music player interface in Ionic 2, with a focus on creating a visually pleasing design. The interface will include the following features:
- Track list with alternating row styles
- Track progress bar
- Information for currently playing track
- Styling and animation for currently playing track
- Next track and previous track
- Pause and play current track
- Select track to play
NOTE: We’re not building an actual music player in this tutorial, just the interface. The interface will have all the necessary features to play music and we will simulate music playing, but we won’t be implementing the audio portion. Given the way this interface will be designed, though, it should be quite easy to integrate any kind of music service into it.
Here’s what it will look like when it’s done:
Before We Get Started
Last updated for Ionic 3.9.2
Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.
If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.
1. Generate a New Ionic 2 Application
We’re going to start off by generating a new Ionic 2 application with the following command:
ionic start ionic2-music-interface
Once the application has finished generating, you should make it your current working directory by running the following command:
cd ionic2-music-interface
We will also be implementing a custom component in this application, so we will use the Ionic CLI to generate this now.
Run the following command to generate the Progress Bar component:
ionic g component ProgressBar
and we will need to set this new component up in the app.module.ts file so that we are able to use it in the application.
Modify src/app/app.module.ts to reflect the following:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ProgressBarComponent } from '../components/progress-bar/progress-bar';
@NgModule({
declarations: [MyApp, HomePage, ProgressBarComponent],
imports: [BrowserModule, IonicModule.forRoot(MyApp)],
bootstrap: [IonicApp],
entryComponents: [MyApp, HomePage],
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler },
],
})
export class AppModule {}
Now that we have everything set up, let’s get started.
2. Implement the Progress Bar Component
If you take a look at the gif of the application we are building above, you may notice that there is a progress bar for the track at the top of the screen. We will be using the component we just generated to implement this.
The code for this will be based on a tutorial I have written previously, with just a few minor adjustments. Since I have already explained this component in-depth I won’t explain it again here, so if you’re interested in the learning about how this works and about custom components in general you should read Build a Simple Progress Bar Component in Ionic 2.
Modify src/components/progress-bar/progress-bar.ts to reflect the following:
import { Component, Input } from '@angular/core';
@Component({
selector: 'progress-bar',
templateUrl: 'progress-bar.html',
})
export class ProgressBarComponent {
@Input('progress') progress;
constructor() {}
}
Modify src/components/progress-bar/progress-bar.html to reflect the following:
<div class="progress-outer">
<div class="progress-inner" [style.width]="progress + '%'"></div>
</div>
Modify src/components/progress-bar/progress-bar.scss to reflect the following:
progress-bar {
.progress-outer {
width: 100%;
padding: 1px;
text-align: center;
background-color: map-get($colors, light);
color: #fff;
}
.progress-inner {
min-width: 1%;
white-space: nowrap;
overflow: hidden;
padding: 2px;
background-color: map-get($colors, dark);
}
}
With the component defined, we will now be able to use it easily in any of our template. We are going to add it to the Home Page template such that it sits in the header area.
Modify src/pages/home/home.html to reflect the following:
<ion-header>
<progress-bar [progress]="currentTrack.progress"></progress-bar>
</ion-header>
<ion-content fullscreen> </ion-content>
We bind the progress property of the component to currentTrack.progress
which will indicate the progress of the currently playing track – we will add this in just a moment. We also add the fullscreen
attribute to the content area, which will cause the content to extend all the way up to the top of the screen (without this attribute, the header is taken into account and a margin is applied to the content area such that the content does not run underneath the header).
3. Set up the Track Data
We are already referencing some data that does not exist yet, and we will need to reference more data in our templates soon (so that we can display songs), so we are going to set up some data to represent some tracks now.
Modify src/pages/home/home.ts to reflect the following:
import { Component } from '@angular/core';
@Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage {
tracks: any;
playing: boolean = true;
currentTrack: any;
progressInterval: any;
constructor() {
this.tracks = [
{
title: 'Something About You',
artist: 'ODESZA',
playing: false,
progress: 0,
},
{
title: 'Run',
artist: 'Allison Wonderland',
playing: false,
progress: 0,
},
{ title: 'Breathe', artist: 'Seeb Neev', playing: false, progress: 0 },
{
title: 'HyperParadise',
artist: 'Hermitude',
playing: false,
progress: 0,
},
{ title: 'Lifespan', artist: 'Vaults', playing: false, progress: 0 },
{ title: 'Stay High', artist: 'Tove Lo', playing: false, progress: 0 },
{ title: 'Lean On', artist: 'Major Lazer', playing: false, progress: 0 },
{ title: 'They Say', artist: 'Kilter', playing: false, progress: 0 },
];
this.currentTrack = this.tracks[0];
}
}
We’ve set up an array that contains some objects that represent tracks, including the progress
property that we are tieing to the progress bar component. We set the first track in the array at the currently playing track.
We also set up a progressInterval
member variable that we will make use of later. As I mentioned before, we will simulate playing a song rather than using actual music, the interval will be used to gradually increase the progress
property of a particular track.
4. Track List with Alternating Row Style
We are going to implement the track list in the template now, but we are going to do something a little more interesting than just a normal list. We are going to set the list up such that every second item in the list has different styling.
Modify src/pages/home/home.html to reflect the following:
<ion-header>
<progress-bar [progress]="currentTrack.progress"></progress-bar>
</ion-header>
<ion-content fullscreen>
<ion-list no-lines>
<button
detail-none
ion-item
*ngFor="let track of tracks; let i = index;"
(click)="track.playing ? pauseTrack(track) : playTrack(track)"
[ngClass]="{ 'alternate': i % 2, 'playing': track.playing }"
>
<ion-avatar item-left>
<img [src]="'https://api.adorable.io/avatars/75/' + track.artist" />
<ion-spinner
item-right
*ngIf="track.playing"
name="bubbles"
item-left
></ion-spinner>
</ion-avatar>
<h2>{{track.artist}}</h2>
<p>{{track.title}}</p>
</button>
</ion-list>
</ion-content>
There’s a few things going on here, so let’s talk through them. We loop through all of our track data by using an *ngFor
but as well as creating a reference to each individual track
we also set up reference to index
. This index
indicates the index of the *ngFor
loop, so for the first track it will be 0, for the second track it will be 1, the third track will be 2, and so on.
We make use of this index to apply the alternating list item styles by referencing it in the [ngClass]
binding:
[ngClass]="{ 'alternate': i % 2, 'playing': track.playing }
If you are unfamiliar with how ngClass
works I would recommend watching: Conditional Attributes, Styles, and Classes in Ionic 2.
Essentially, we are saying we want to apply the alternate
and playing
classes conditionally here. If track.playing
is true then we set the playing
class (which will allow us to add some specific styling for the currently playing track), and if i % 2
is true then we apply the alternate
class. The check for the alternate class is a modulus operation, which will give us the remainder when i
is divided by 2
. This will return for even numbers, and `1` for odd numbers. Since
is equivalent to false
and 1
is equivalent to true
we can use that operation directly to indicate if the class should be applied or not. A more verbose way to write that condition would be i % 2 == 1
.
In order to make use of these conditional classes, we will need to actually create the corresponding classes in our .scss
file, but we will take care of this later.
We also have a (click)
handler set up that will call pauseTrack
if the track is currently playing, and playTrack
if it is not. We use the ternary operator to handle both conditions directly in the template.
Then we just have the item itself, which has an automatically generated avatar, a spinner animation, and the track details.
5. Add Current Track Info
Next, we are going to add a card to display the currently playing track information in an overlay.
Modify src/pages/home/home.html to reflect the following:
<ion-header>
<progress-bar [progress]="currentTrack.progress"></progress-bar>
</ion-header>
<ion-content fullscreen>
<ion-card ion-fixed>
<ion-card-header> {{currentTrack.artist}} </ion-card-header>
<ion-card-content> {{currentTrack.title}} </ion-card-content>
</ion-card>
<ion-list no-lines>
<button
detail-none
ion-item
*ngFor="let track of tracks; let i = index;"
(click)="track.playing ? pauseTrack(track) : playTrack(track)"
[ngClass]="{ 'alternate': i % 2, 'playing': track.playing }"
>
<ion-avatar item-left>
<img [src]="'https://api.adorable.io/avatars/75/' + track.artist" />
<ion-spinner
item-right
*ngIf="track.playing"
name="bubbles"
item-left
></ion-spinner>
</ion-avatar>
<h2>{{track.artist}}</h2>
<p>{{track.title}}</p>
</button>
</ion-list>
</ion-content>
We have added an <ion-card>
here, and used the ion-fixed
attribute. This will add this element to the fixed-content
area which will cause it to stick to its current place on the screen as the list is scrolled. By default, this will fix the content to the top of the screen, but we will add some styling later to move it to the bottom of the screen instead.
6. Add Music Controls
We are going to add some music controls now to pause the current track, skip the track, and go back to a previous track. We will add this to the <ion-footer>
section so that it is always on screen.
Modify src/pages/home/home.html to reflect the following:
<ion-header>
<progress-bar [progress]="currentTrack.progress"></progress-bar>
</ion-header>
<ion-content fullscreen>
<ion-card ion-fixed>
<ion-card-header> {{currentTrack.artist}} </ion-card-header>
<ion-card-content> {{currentTrack.title}} </ion-card-content>
</ion-card>
<ion-list no-lines>
<button
detail-none
ion-item
*ngFor="let track of tracks; let i = index;"
(click)="track.playing ? pauseTrack(track) : playTrack(track)"
[ngClass]="{ 'alternate': i % 2, 'playing': track.playing }"
>
<ion-avatar item-left>
<img [src]="'https://api.adorable.io/avatars/75/' + track.artist" />
<ion-spinner
item-right
*ngIf="track.playing"
name="bubbles"
item-left
></ion-spinner>
</ion-avatar>
<h2>{{track.artist}}</h2>
<p>{{track.title}}</p>
</button>
</ion-list>
</ion-content>
<ion-footer>
<ion-grid>
<ion-row>
<ion-col width="33">
<button (click)="prevTrack()" color="light" clear ion-button icon-only>
<ion-icon name="skip-backward-outline"></ion-icon>
</button>
</ion-col>
<ion-col width="33">
<button
*ngIf="!currentTrack.playing"
(click)="playTrack(currentTrack)"
color="light"
clear
ion-button
icon-only
>
<ion-icon name="play-outline"></ion-icon>
</button>
<button
*ngIf="currentTrack.playing"
(click)="pauseTrack(currentTrack)"
color="light"
clear
ion-button
icon-only
>
<ion-icon name="pause-outline"></ion-icon>
</button>
</ion-col>
<ion-col width="33">
<button (click)="nextTrack()" color="light" clear ion-button icon-only>
<ion-icon name="skip-forward-outline"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-grid>
</ion-footer>
We are using a grid with 3 evenly spaced columns to contain the buttons that will control the music. These buttons will trigger functions in the TypeScript file that we will implement shortly.
If you are not familiar with the grid component you should watch An Overview of the Grid Component in Ionic 2.
By default, the buttons inside of the columns will not display exactly where we want them – we will have add some custom styling to achieve what we want, but we will do this later.
Also notice that the middle column contains two buttons, but they are conditionally displayed with *ngIf
. This allows us to only display either a pause or a play button, depending on whether there is a track currently playing or not.
7. Set up Functions
Our template is just about done now, but we are referencing some functions in our template that do not exist yet. We are going to add those now.
Modify src/pages/home/home.ts to reflect the following:
import { Component } from '@angular/core';
@Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage {
tracks: any;
currentTrack: any;
progressInterval: any;
constructor() {
this.tracks = [
{
title: 'Something About You',
artist: 'ODESZA',
playing: false,
progress: 0,
},
{
title: 'Run',
artist: 'Allison Wonderland',
playing: false,
progress: 0,
},
{ title: 'Breathe', artist: 'Seeb Neev', playing: false, progress: 0 },
{
title: 'HyperParadise',
artist: 'Hermitude',
playing: false,
progress: 0,
},
{ title: 'Lifespan', artist: 'Vaults', playing: false, progress: 0 },
{ title: 'Stay High', artist: 'Tove Lo', playing: false, progress: 0 },
{ title: 'Lean On', artist: 'Major Lazer', playing: false, progress: 0 },
{ title: 'They Say', artist: 'Kilter', playing: false, progress: 0 },
];
this.currentTrack = this.tracks[0];
}
playTrack(track) {
// First stop any currently playing tracks
for (let checkTrack of this.tracks) {
if (checkTrack.playing) {
this.pauseTrack(checkTrack);
}
}
track.playing = true;
this.currentTrack = track;
// Simulate track playing
this.progressInterval = setInterval(() => {
track.progress < 100 ? track.progress++ : (track.progress = 0);
}, 1000);
}
pauseTrack(track) {
track.playing = false;
clearInterval(this.progressInterval);
}
nextTrack() {
let index = this.tracks.indexOf(this.currentTrack);
index >= this.tracks.length - 1 ? (index = 0) : index++;
this.playTrack(this.tracks[index]);
}
prevTrack() {
let index = this.tracks.indexOf(this.currentTrack);
index > 0 ? index-- : (index = this.tracks.length - 1);
this.playTrack(this.tracks[index]);
}
}
We have added functions for playTrack
, pauseTrack
, nextTrack
, and prevTrack
. The playTrack
function will first check if any track is currently playing, and if it is it will pause it before playing the new track. The “playing” of a track is simply an interval that runs once every second to increment the progress
property of an individual track.
The other functions are all quite simple. The pauseTrack
function toggles the playing
property and clears the interval the playTrack
function creates. The nextTrack
and prevTrack
functions just grab the current index and either increment it or decrement it by 1, and then call the playTrack
function for that track (making sure that the start or end of the track list hasn’t been reached, in which case it will start cycling through the tracks again).
8. Styling
We’re just about done now, we just need to add a little styling.
Modify src/pages/home/home.scss to reflect the following:
.ios,
.md {
page-home {
$song-card-height: 90px;
ion-list {
margin-bottom: $song-card-height + 20px;
.alternate {
background-color: map-get($colors, secondary);
}
.playing {
background-color: map-get($colors, dark);
img {
opacity: 0.4;
}
}
}
ion-avatar {
position: relative;
}
ion-spinner {
position: absolute;
top: 0;
left: 0;
margin: 4px !important;
}
[ion-item] {
background-color: map-get($colors, primary);
padding: 20px;
}
ion-card {
background-color: map-get($colors, light);
opacity: 0.9;
height: $song-card-height;
bottom: 0;
}
ion-footer {
height: 75px;
background-color: map-get($colors, dark);
ion-grid,
ion-row,
ion-col {
height: 100%;
}
ion-col {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
There’s actually a few interesting things going on here, so I’m going to talk through a couple of points:
- We set up a SASS variable for
$song-card-height
so that we can reference it in multiple places, and perform calculations to ensure that the card never overlaps the list when the user has scrolled all the way to the bottom (otherwise the tracks at the bottom would always be obscured) - We make use of SASS nesting to simplify our selectors
- We are using
map-get
to grab the colors defined in the variables.scss file (which we will change in a moment) - We use an attribute selector for
[ion-item]
since the list is actually made up of buttons with anion-item
attribute. - We add the
flex
property to our columns so that we can use Flebox properties to align their content (allowing us to easily center the buttons they contain both vertically and horizontally).
Finally, we will just add some variables to the variables.scss file to change some colours.
Modify the Named Color Variables in src/theme/variables.scss to reflect the following:
$colors: (
primary: #34495e,
secondary: #2c3e50,
danger: #f53d3d,
light: #f4f4f4,
dark: #353535
);
$background-color: map-get($colors, dark);
Here’s what it should look like now:
Summary
We’ve used quite a few different techniques in this tutorial, a lot of which I have covered extensively in other tutorials, and combined them all into one visually pleasing music player interface. Although this application does not actually play music now, all you would have to do is hook your music playing functions into the existing functions that are in the home.ts file right now and it should work quite well.