Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all articles
Browse latest Browse all 1836

Create a Music Player on Android: User Controls

$
0
0

By the end of this tutorial, our basic music player app will be complete. We learned how to display a list of songs from the user's device in the first tutorial. The second tutorial explained how we can play individual songs from the list. The only thing missing now is the code that allows users to control the playback of different songs.

In this tutorial, we will learn how to let users control the playback of different songs. This includes the ability to play, pause, or seek a particular song. They will also be able to play the next or previous tracks as well as turn shuffling on to play the songs in random order.

We will also display a notification during the playback so that the user can jump back to the music player after using other apps.

At the end of this tutorial, you will have a music player that looks like the image below:

Android Music Player Final Result PreviewAndroid Music Player Final Result PreviewAndroid Music Player Final Result Preview

Creating a Controller

Since we want to add playback controls for our media player, we need to implement the MediaPlayerControl interface first. Update your MainActivity class declaration so that it looks like the line below:

1
classMainActivity:AppCompatActivity(),MediaPlayerControl

You will now see an error message about unimplemented methods if you hover over the class name. You can get rid of the error by telling Android Studio to add the unimplemented methods.

Add a new Kotlin class to your project and name it MusicController. This will create a new file called MusicController.kt. The MusicController class extends the MediaController class and overrides one of its methods called hide().

Your MusicController.kt file should now have the following code:

1
packagecom.tutsplus.musicplayer
2
3
importandroid.content.Context
4
importandroid.widget.MediaController
5
6
7
classMusicController(c:Context?):MediaController(c){
8
overridefunhide(){}
9
}

TheMediaControllerclass presents a standard widget with play/pause, rewind, fast-forward, and skip (previous/next) buttons in it. The widget also contains a seek bar, which updates as the song plays and contains text indicating the duration of the song and the player's current position.

With this extended class, we can customize the behavior of the controller according to our requirements. In this case, we simply want to prevent the controls from hiding automatically after three seconds. We do this by overriding the hide() method.

Now, update the MainActivity class to add the following variables to it near the top:

1
privatelateinitvarcontroller:MusicController
2
3
privatevarpaused:Boolean=false
4
privatevarplaybackPaused:Boolean=false

Create a helper method called setController() and add the following code to it:

1
privatefunsetController(){
2
controller=MusicController(this)
3
4
controller.setPrevNextListeners({playNext()}
5
){playPrev()}
6
7
controller.setMediaPlayer(this)
8
controller.setAnchorView(findViewById(R.id.song_list))
9
controller.isEnabled=true
10
}

We begin by instantiating the MusicController class and then assign the playNext() and playPrev() methods to its next and previous button click listeners respectively. The setAnchorView() method tells the app to anchor the controller UI to our specified view.

Let's define the playNext() and playPrev() methods now:

1
privatefunplayNext(){
2
musicService!!.playNext()
3
if(playbackPaused){
4
setController()
5
playbackPaused=false
6
}
7
controller.show(0)
8
}
9
10
privatefunplayPrev(){
11
musicService!!.playPrev()
12
if(playbackPaused){
13
setController()
14
playbackPaused=false
15
}
16
controller.show(0)
17
}

The call to the show() method of controller object with a value of 0 means that the media controller will be displayed on the screen until it is manually hidden by the user.

Now, update the displaySongs() method in MainAcitvity to add a call to setController() at the end.

1
setController()

Implement Playback Control

Remember that the media playback is happening in theMusicServiceclass, but that the user interface comes from the MainActivityclass. In the previous tutorial, we bound theMainActivityinstance to theMusicServiceinstance, so that we could control playback from the user interface.

The methods in ourMainActivityclass that we added to implement theMediaPlayerControlinterface will be called when the user attempts to control playback. We will need theMusicServiceclass to act on any events related to those controls. Open yourMusicServiceclass now to add a few more methods to it:

1
fungetPosition():Int{
2
returnmediaPlayer.currentPosition
3
}
4
5
fungetDuration():Int{
6
returnmediaPlayer.duration
7
}
8
9
funisPlaying():Boolean{
10
returnmediaPlayer.isPlaying
11
}
12
13
funpausePlayer(){
14
mediaPlayer.pause()
15
}
16
17
funseek(position:Int){
18
mediaPlayer.seekTo(position)
19
}
20
21
fungo(){
22
mediaPlayer.start()
23
}

In the previous section, we added the playNext() and playPrev() methods to our MainActivity class. Inside those methods, we call the playNext() and playPrev() methods of the MusicService class. We will now define those methods inside the MusicService class.

1
funplayPrev(){
2
songPosition--
3
if(songPosition<0)songPosition=songs.size-1
4
playSong()
5
}
6
7
funplayNext(){
8
songPosition++
9
if(songPosition>=songs.size)songPosition=0
10
playSong()
11
}

In both methods, we check if the songPosition falls outside the list of songs and then we reset it accordingly. After that, we make a call to playSong() in order to play song at current position.

We will now update the implementation of some of the methods of the MediaPlayerControl interface. We are simply returning true for these methods to all users to pause, seek forward or seek backward while playing a song. Add the following code to your MainActivity class.

1
overridefuncanPause():Boolean{
2
returntrue
3
}
4
5
overridefuncanSeekBackward():Boolean{
6
returntrue
7
}
8
9
overridefuncanSeekForward():Boolean{
10
returntrue
11
}
12
13
overridefungetAudioSessionId():Int{
14
return1
15
}
16
17
overridefungetBufferPercentage():Int{
18
valduration=musicService!!.getDuration()
19
if(duration>0){
20
return(musicService!!.getPosition()*100)/(duration)
21
}
22
return0
23
}
24
25
overridefungetCurrentPosition():Int{
26
if(musicService!=null&&musicBound&&musicService!!.isPlaying())
27
returnmusicService!!.getPosition()
28
elsereturn0
29
}

In the above snippet, you might have noticed that we called the getPosition() method from the musicService class to calculate the buffer percentage and to get the current position within MainActivity.

Let's update the implementation of some more methods so that they make a call to the respective methods inside the MusicService class.

1
overridefunstart(){
2
musicService!!.go()
3
}
4
5
overridefunpause(){
6
playbackPaused=true
7
musicService!!.pausePlayer()
8
}
9
10
overridefunseekTo(p0:Int){
11
musicService!!.seek(p0)
12
}
13
14
overridefunisPlaying():Boolean{
15
if(musicService!=null&&musicBound)
16
returnmusicService!!.isPlaying()
17
returnfalse
18
}
19
20
overridefungetDuration():Int{
21
if(musicService!=null&&musicBound&&musicService!!.isPlaying())
22
returnmusicService!!.getDuration()
23
elsereturn0
24
}

Handling Navigation Back Into the App

When a user starts playing a song in any music app, they expect the song to keep playing even if they navigate away from the app to do something else. We can implement this feature by showing users a notification that displays the title of the current song being played. Clicking on the notification will take users to the app.

Begin by adding the following variables to the MusicService class.

1
privatevarsongTitle:String?=""
2
privatevalnotifyId=1

In our second tutorial where we implement playback, the onPrepared() method of the MusicService class was simply starting the media player. We will now update the method to create and display a notification. Here is the complete code of the method.

1
overridefunonPrepared(mediaPlayer:MediaPlayer?){
2
mediaPlayer?.start()
3
4
valchannelId="my_channel_id"
5
6
funcreateNotificationChannel(channelId:String,channelName:String):String{
7
lateinitvarnotificationChannel:NotificationChannel
8
9
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
10
notificationChannel=NotificationChannel(
11
channelId,
12
channelName,NotificationManager.IMPORTANCE_DEFAULT
13
)
14
15
notificationChannel.lockscreenVisibility=Notification.VISIBILITY_PUBLIC
16
valmanager=getSystemService(NOTIFICATION_SERVICE)asNotificationManager
17
manager.createNotificationChannel(notificationChannel)
18
}
19
returnchannelId
20
}
21
22
valpendingIntent:PendingIntent=
23
Intent(this,MainActivity::class.java).let{intent->
24
PendingIntent.getActivity(this,0,intent,
25
PendingIntent.FLAG_IMMUTABLE)
26
}
27
28
29
valnotification:Notification=NotificationCompat.Builder(this,channelId)
30
.setSmallIcon(R.drawable.ic_launcher_foreground)
31
.setOngoing(true)
32
.setContentTitle("Playing")
33
.setContentText(songTitle)
34
.setContentIntent(pendingIntent)
35
.setTicker(songTitle)
36
.build()
37
38
createNotificationChannel(channelId,"My Music Player")
39
40
startForeground(notifyId,notification)
41
}

After starting the media player, we create a string variable to store the channel ID. In the next line, we define a function called createNotificationChannel() to create a NotificationChannel with our specified ID and name.

Notification channels provide a way to group notifications. They were introduced in Android 8.0 or API level 26. We have to create a notification channel to display notifications in any android version above 8.0.

Next, we create a PendingIntent that will launch the main activity of our app once clicked. Finally, we create a new notification using the notification builder. I have passed true to the setOngoing() method to prevent users from accidently swiping away the notification.

Make sure that you update the playSong() method to include the following line to set the value of songTitle to the currently playing song.

1
songTitle=playSong.name

Finally, update the onDestory() method of the MusicService class to remove the notification once the app is destroyed.

1
overridefunonDestroy(){
2
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
3
stopForeground(STOP_FOREGROUND_REMOVE)
4
}else{
5
stopForeground(true)
6
}
7
}

Shuffle Playback

So far, the Shuffle button in our app isn't doing anything. We will now write some code for this button so that the song to be played next is selected at random.

Begin by adding the following variables to the MusicService class.

1
privatevarshuffle=false
2
privatevarrandom:Random?=null

Now add the setShuffle() method to the MusicService class. This method simply toggles the value of the shuffle variable.

1
funsetShuffle(){
2
shuffle=!shuffle
3
}

Update the playNext() method so that it checks for the shuffle value and selects a song at random if it is set to true.

1
funplayNext(){
2
if(shuffle){
3
varnewSong=songPosition
4
while(newSong==songPosition){
5
newSong=random!!.nextInt(songs.size)
6
}
7
songPosition=newSong
8
}else{
9
songPosition++
10
if(songPosition>=songs.size)songPosition=0
11
}
12
playSong()
13
}

Finally, add the shuffleSongs() and stopSong() method to the MainActivity class.

1
funshuffleSongs(view:View){
2
musicService?.setShuffle()
3
}
4
5
funstopSong(view:View){
6
stopService(playIntent)
7
musicService=null
8
exitProcess(0)
9
}

Updating Lifecycle Methods

We will now make some changes to the onPause(), onResume(), and onStop() methods to do some initializations and cleanups. Add the following code to the MainAcitvity class.

1
overridefunonPause(){
2
super.onPause()
3
paused=true
4
}
5
6
overridefunonResume(){
7
super.onResume()
8
if(paused){
9
setController()
10
paused=false
11
}
12
}
13
14
overridefunonStop(){
15
controller.hide()
16
super.onStop()
17
}

Also update the songPicked() method inside MainActivity to handle paused playback.

1
funsongPicked(view:View){
2
musicService!!.setSong(view.tag.toString().toInt())
3
musicService!!.playSong()
4
5
if(playbackPaused){
6
setController()
7
playbackPaused=false
8
}
9
controller.show(0)
10
}

With this update, we resume the playback after a new song is picked if it was paused earlier.

The last thing you need to do is update the onError() and onCompletion() methods of the MusicService class to reset the media player and play the next song.

1
overridefunonError(mp:MediaPlayer,what:Int,extra:Int):Boolean{
2
mp.reset()
3
returnfalse
4
}
5
6
overridefunonCompletion(mp:MediaPlayer){
7
if(mediaPlayer.currentPosition>0){
8
mp.reset()
9
playNext()
10
}
11
}

Final Thoughts

At this point, you should have a fully functioning music player app that you can install and use on your own device. Please keep in mind that the aim of this tutorial was to get you started with the basics.

There are a lot of improvements that you can make to this app to enhance its UI or implement additional features. You will also need to update some of these methods depending on how many user devices and API levels you want to support.

2023-07-30 15:40:00 UTC2023-07-30 15:40:00 UTCNitish Kumar

Viewing all articles
Browse latest Browse all 1836

Trending Articles