Ok, so I am a newbie Android and java coder. Doing it as a hobby only and learning it during the free time that I can find.
I came across SeekBar and decided to create a simple media player to experiment with it. After getting the basics going, I opened Spotify and tried to mimic its SeekBar behavior.
So here's the concept: I have one Runnable running every 200ms when the MediaPlayer is in the playing status to update the left time label and the SeekBar position.
Now, when you hold the Spotify SeekBar and drag it around, what happens is that the leftmost time label gets updated as the SeekBar moves, displaying the timestamp of where the SeekBar is, without the music being affected. It will only effectively change the music "position" when we release the SeekBar.
So, my way of thinking about this approach was: Create a second Runnable that gets called via a Handlerwhen the user holds the SeekBar, and release the handler when the user releases the SeekBar. Also this runnable would updatw every 50ms because the act of dragging the bar is kinda fast and to visually change the timer label as the user moves the bar around.
Now, here's the code to my MainActivity doing all this:
package com.heymilkshake.simpleaudioplayer;
import androidx.appcompat.app.AppCompatActivity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button playBtn;
private Button stopBtn;
private Button skipForward10Btn;
private Button skipBackward10Btn;
private TextView displayStatus;
private TextView songCurrentTime;
private TextView songTimeEnd;
private SeekBar seekBar;
private MediaPlayer mediaPlayer;
private Handler seekBarHandler;
private Runnable updateSeekBar;
private Runnable updateTimer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
tagButtons();
createPlayer();
initialize();
}
private void initialize() {
songCurrentTime.setText(formatTime(mediaPlayer.getCurrentPosition()));
songTimeEnd.setText(formatTime(mediaPlayer.getDuration()));
seekBar.setMax(mediaPlayer.getDuration());
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (mediaPlayer.isPlaying()) {
seekBarHandler.removeCallbacks(updateSeekBar);
}
seekBarHandler.postDelayed(updateTimer, 0);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
seekBarHandler.removeCallbacks(updateTimer);
mediaPlayer.seekTo(seekBar.getProgress());
if (mediaPlayer.isPlaying()) {
seekBarHandler.postDelayed(updateSeekBar, 0);
}
}
});
seekBarHandler = new Handler();
updateSeekBar = new Runnable() {
@Override
public void run() {
updateSeekAndTime();
seekBarHandler.postDelayed(this, 200);
}
};
updateTimer = new Runnable() {
@Override
public void run() {
songCurrentTime.setText(formatTime(seekBar.getProgress()));
seekBarHandler.postDelayed(this, 50);
}
};
}
private String formatTime(int ms) {
return String.format("%d:%02d",
TimeUnit.MILLISECONDS.toMinutes((long) ms),
TimeUnit.MILLISECONDS.toSeconds((long) ms) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) ms)));
}
private void createPlayer() {
mediaPlayer = MediaPlayer.create(this, R.raw.come_alive);
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
handleStopButton();
}
});
}
private void findViews() {
playBtn = findViewById(R.id.button_play);
stopBtn = findViewById(R.id.button_stop);
skipForward10Btn = findViewById(R.id.button_skip_forward_10);
skipBackward10Btn = findViewById(R.id.button_skip_backward_10);
displayStatus = findViewById(R.id.display_status);
songCurrentTime = findViewById(R.id.song_time_start);
songTimeEnd = findViewById(R.id.song_time_end);
seekBar = findViewById(R.id.seek_bar);
}
private void tagButtons() {
int i = 0;
for (Button button : Arrays.asList(
playBtn,
stopBtn,
skipForward10Btn,
skipBackward10Btn)) {
button.setTag(i++);
button.setOnClickListener(this);
}
}
@Override
public void onClick(View v) {
int clickedTag = (int) v.getTag();
switch (clickedTag) {
case 0:
handlePlayButton();
break;
case 1:
handleStopButton();
break;
case 2:
handleForward10Btn();
break;
case 3:
handleBackwards10Btn();
break;
default:
}
}
private void handleBackwards10Btn() {
if (mediaPlayer.getCurrentPosition() - 10000 < 0) {
mediaPlayer.seekTo(0);
} else {
mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() - 10000);
}
updateSeekAndTime();
}
private void handleForward10Btn() {
if (mediaPlayer.getCurrentPosition() + 10000 > mediaPlayer.getDuration()) {
mediaPlayer.seekTo(mediaPlayer.getDuration());
} else {
mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() + 10000);
}
updateSeekAndTime();
}
private void updateSeekAndTime() {
seekBar.setProgress(mediaPlayer.getCurrentPosition());
songCurrentTime.setText(formatTime(seekBar.getProgress()));
}
private void handleStopButton() {
mediaPlayer.pause();
mediaPlayer.seekTo(0);
seekBarHandler.removeCallbacks(updateSeekBar);
updateSeekAndTime();
displayStatus.setText("Stopped");
playBtn.setText("Play ►");
}
private void handlePlayButton() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
seekBarHandler.removeCallbacks(updateSeekBar);
displayStatus.setText("Paused");
playBtn.setText("Play ►");
} else {
mediaPlayer.start();
seekBarHandler.postDelayed(updateSeekBar, 0);
displayStatus.setText("Playing");
playBtn.setText("Pause ||");
}
}
}
When I tested my code, I got this behavior: The bar jumping back and forth to and from the location of where the music is playing while the user is dragging it (gif for visualization of the undesired behavior)
Well, I am perfectly aware why this happens:updateSeekBar is running every 200ms, calling method updateSeekAndTime() which changes the label and bar to the current position of where the music is playing. So, when the onStartTrackingTouch detects that the user is holding the SeekBar, we run the updateTimer runnable every 50ms. This Runnable changes the text of the time label concurrently with the updateSeekBar Runnable, causing the above undesired behavior.
To solve this, I modified the code of the SeekBar.OnSeekBarChangeListener() interface anonymous implementation by adding some condition checks: If the song is playing and the user grabs the SeekBar, remove the callback to the updateSeekBar so that it stops running and when the user releases the bar, run it again. So here are the code changes:
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (mediaPlayer.isPlaying()) {
seekBarHandler.removeCallbacks(updateSeekBar);
}
seekBarHandler.postDelayed(updateTimer, 0);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
seekBarHandler.removeCallbacks(updateTimer);
mediaPlayer.seekTo(seekBar.getProgress());
if (mediaPlayer.isPlaying()) {
seekBarHandler.postDelayed(updateSeekBar, 0);
}
}
});
Ok, This worked just great! Look at another gif:
But this left me wondering and it's here that I ask you guys:
- Is there a better approach to do this?
- Is there any way for a Thread that is looping with the help of a Handler to instruct another Thread to "wait" while it is being executed?
- How would YOU solve this problem??
IMPORTANT NOTE: Try to simplify or talk about stuff in a simple manner as I am just a Java and Android beginner.
Thank you all!!


