Rework the media controls, adding a seek bar

The media controls now appear after clicking on the cover view and disppear
after a period of inactivity. The previous and next menu entries have been
removed in favor of the previous and next buttons and the media control view.
This commit is contained in:
Christopher Eby 2010-02-15 22:35:17 -06:00
parent 608a0316cc
commit b60404a950
18 changed files with 357 additions and 40 deletions

10
res/drawable/next.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/next_focused" />
<item
android:state_focused="true"
android:drawable="@drawable/next_focused" />
<item android:drawable="@drawable/next_normal" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

10
res/drawable/pause.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/pause_focused" />
<item
android:state_focused="true"
android:drawable="@drawable/pause_focused" />
<item android:drawable="@drawable/pause_normal" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

10
res/drawable/play.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/play_focused" />
<item
android:state_focused="true"
android:drawable="@drawable/play_focused" />
<item android:drawable="@drawable/play_normal" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

10
res/drawable/previous.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/previous_focused" />
<item
android:state_focused="true"
android:drawable="@drawable/previous_focused" />
<item android:drawable="@drawable/previous_normal" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

59
res/layout/nowplaying.xml Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<org.kreed.tumult.CoverView
android:id="@+id/cover_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent" />
<LinearLayout
android:id="@+id/controls"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_gravity="bottom|center_horizontal"
android:background="#000"
android:orientation="vertical">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/previous"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:background="@null"
android:src="@drawable/previous" />
<ImageButton
android:id="@+id/play_pause"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:background="@null"
android:src="@drawable/play" />
<ImageButton
android:id="@+id/next"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:background="@null"
android:src="@drawable/next" />
</LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seek_bar"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_weight="1" />
<TextView
android:id="@+id/seek_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:text="0:00 / 0:00" />
</LinearLayout>
</LinearLayout>
</merge>

View File

@ -8,6 +8,7 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
@ -32,17 +33,17 @@ public class CoverView extends View {
public interface CoverViewWatcher {
public void next();
public void previous();
public void togglePlayback();
public void clicked();
}
public CoverView(Context context)
public CoverView(Context context, AttributeSet attributes)
{
super(context);
super(context, attributes);
mScroller = new Scroller(context);
}
public void setCoverSwapListener(CoverViewWatcher listener)
public void setWatcher(CoverViewWatcher listener)
{
mListener = listener;
}
@ -254,7 +255,7 @@ public class CoverView extends View {
break;
case MotionEvent.ACTION_UP:
if (Math.abs(mStartX - x) + Math.abs(mStartY - ev.getY()) < 10) {
mListener.togglePlayback();
mListener.clicked();
} else {
VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);

View File

@ -6,4 +6,5 @@ oneway interface IMusicPlayerWatcher {
void previousSong(in Song playingSong, in Song nextForwardSong);
void nextSong(in Song playingSong, in Song nextBackwardSong);
void stateChanged(in int oldState, in int newState);
void mediaLengthChanged(in long startTime, in int duration);
}

View File

@ -7,8 +7,12 @@ interface IPlaybackService {
void registerWatcher(IMusicPlayerWatcher watcher);
Song[] getCurrentSongs();
int getState();
long getStartTime();
int getDuration();
void previousSong();
void togglePlayback();
void nextSong();
void seekToProgress(int progress);
}

View File

@ -29,12 +29,32 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
public static final int STATE_NORMAL = 0;
public static final int STATE_NO_MEDIA = 1;
public static final int STATE_PLAYING = 2;
public IPlaybackService.Stub mBinder = new IPlaybackService.Stub() {
public Song[] getCurrentSongs()
{
return new Song[] { getSong(-1), getSong(0), getSong(1) };
}
public int getState()
{
return mState;
}
public long getStartTime()
{
if (mMediaPlayer == null)
return 0;
return MusicPlayer.this.getStartTime();
}
public int getDuration()
{
if (mMediaPlayer == null)
return 0;
return mMediaPlayer.getDuration();
}
public void nextSong()
{
@ -62,6 +82,16 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
if (watcher != null)
mWatchers.register(watcher);
}
public void seekToProgress(int progress)
{
if (mMediaPlayer == null || !mMediaPlayer.isPlaying())
return;
long position = (long)mMediaPlayer.getDuration() * progress / 1000;
mMediaPlayer.seekTo((int)position);
mediaLengthChanged();
}
};
public void queueSong(int songId)
@ -201,6 +231,9 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
public void setState(int state)
{
if (mState == state)
return;
int oldState = mState;
mState = state;
@ -244,12 +277,15 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
notification.contentIntent = PendingIntent.getActivity(mService, 0, intent, 0);
mService.startForegroundCompat(NOTIFICATION_ID, notification);
setState(STATE_PLAYING);
}
private void pause()
{
mMediaPlayer.pause();
mService.stopForegroundCompat(NOTIFICATION_ID);
setState(STATE_NORMAL);
}
private void setPlaying(boolean play)
@ -291,6 +327,8 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
Log.e("Tumult", "IOException", e);
}
mediaLengthChanged();
getSong(+2);
while (mCurrentSong > 15) {
@ -298,6 +336,28 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
--mCurrentSong;
}
}
private long getStartTime()
{
int position = mMediaPlayer.getCurrentPosition();
return System.currentTimeMillis() - position;
}
private void mediaLengthChanged()
{
long start = getStartTime();
int duration = mMediaPlayer.getDuration();
int i = mWatchers.beginBroadcast();
while (--i != -1) {
try {
mWatchers.getBroadcastItem(i).mediaLengthChanged(start, duration);
} catch (RemoteException e) {
// Null elements will be removed automatically
}
}
mWatchers.finishBroadcast();
}
public void onCompletion(MediaPlayer player)
{
@ -306,7 +366,7 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
private Song randomSong()
{
return new Song(mSongs[mRandom.nextInt(mSongs.length)]);
return new Song(mSongs[mRandom.nextInt(mSongs.length)]);
}
private synchronized Song getSong(int delta)

View File

@ -8,22 +8,41 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection {
public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection, View.OnClickListener, SeekBar.OnSeekBarChangeListener {
private IPlaybackService mService;
private ViewGroup mLayout;
private CoverView mCoverView;
private LinearLayout mMessageBox;
private View mControls;
private ImageButton mPreviousButton;
private ImageButton mPlayPauseButton;
private ImageButton mNextButton;
private SeekBar mSeekBar;
private TextView mSeekText;
private int mState;
private long mStartTime;
private int mDuration;
private boolean mSeekBarTracking;
private static final int MENU_PREVIOUS = 0;
private static final int MENU_NEXT = 1;
private static final int MENU_PREFS = 2;
private static final int MENU_QUEUE = 3;
@ -32,18 +51,40 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
{
super.onCreate(icicle);
mCoverView = new CoverView(this);
mCoverView.setCoverSwapListener(this);;
setContentView(mCoverView);
// Bundle extras = getIntent().getExtras();
setContentView(R.layout.nowplaying);
mCoverView = (CoverView)findViewById(R.id.cover_view);
mCoverView.setWatcher(this);
mLayout = (ViewGroup)mCoverView.getParent();
mControls = findViewById(R.id.controls);
mPreviousButton = (ImageButton)findViewById(R.id.previous);
mPreviousButton.setOnClickListener(this);
mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause);
mPlayPauseButton.setOnClickListener(this);
mNextButton = (ImageButton)findViewById(R.id.next);
mNextButton.setOnClickListener(this);
mSeekText = (TextView)findViewById(R.id.seek_text);
mSeekBar = (SeekBar)findViewById(R.id.seek_bar);
mSeekBar.setMax(1000);
mSeekBar.setOnSeekBarChangeListener(this);
}
public void setState(int state)
{
mState = state;
switch (state) {
case MusicPlayer.STATE_NORMAL:
setContentView(mCoverView);
mMessageBox = null;
case MusicPlayer.STATE_PLAYING:
if (mMessageBox != null) {
mLayout.removeView(mMessageBox);
mMessageBox = null;
}
mPlayPauseButton.setImageResource(state == MusicPlayer.STATE_PLAYING ? R.drawable.pause : R.drawable.play);
break;
case MusicPlayer.STATE_NO_MEDIA:
mMessageBox = new LinearLayout(this);
@ -51,11 +92,11 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
TextView text = new TextView(this);
text.setText("No songs found on your device.");
text.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT);
layoutParams.gravity = Gravity.CENTER;
text.setLayoutParams(layoutParams);
mMessageBox.addView(text);
setContentView(mMessageBox);
mLayout.addView(mMessageBox);
break;
}
}
@ -98,6 +139,7 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
try {
mService.registerWatcher(mWatcher);
refreshSongs();
setState(mService.getState());
} catch (RemoteException e) {
Log.i("Tumult", "Failed to initialize connection to playback service", e);
}
@ -147,6 +189,12 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
}
});
}
public void mediaLengthChanged(long startTime, int duration)
{
mStartTime = startTime;
mDuration = duration;
}
};
public void next()
@ -167,7 +215,7 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
}
}
public void togglePlayback()
private void togglePlayback()
{
try {
mService.togglePlayback();
@ -178,8 +226,6 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
menu.add(0, MENU_PREVIOUS, 0, "Previous");
menu.add(0, MENU_NEXT, 0, "Next");
menu.add(0, MENU_PREFS, 0, "Preferences");
menu.add(0, MENU_QUEUE, 0, "Add to Queue");
return true;
@ -188,33 +234,139 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
new Thread(new Runnable() {
public void run()
{
switch (item.getItemId()) {
case MENU_PREVIOUS:
previous();
break;
case MENU_NEXT:
next();
break;
case MENU_PREFS:
startActivity(new Intent(NowPlayingActivity.this, PreferencesActivity.class));
break;
case MENU_QUEUE:
onSearchRequested();
break;
}
}
}).start();
switch (item.getItemId()) {
case MENU_PREFS:
startActivity(new Intent(this, PreferencesActivity.class));
break;
case MENU_QUEUE:
onSearchRequested();
break;
}
return true;
}
@Override
public boolean onSearchRequested()
{
startActivity(new Intent(this, SongSelector.class));
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
clicked();
return true;
}
return false;
}
public void clicked()
{
mControls.setVisibility(View.VISIBLE);
if (mStartTime == 0) {
try {
mStartTime = mService.getStartTime();
mDuration = mService.getDuration();
} catch (RemoteException e) {
return;
}
}
updateProgress();
sendHideMessage();
}
private String stringForTime(int ms)
{
int seconds = ms / 1000;
int hours = seconds / 3600;
seconds -= hours * 3600;
int minutes = seconds / 60;
seconds -= minutes * 60;
if (hours > 0)
return String.format("%d:%02d:%02d", hours, minutes, seconds);
else
return String.format("%02d:%02d", minutes, seconds);
}
private void updateProgress()
{
if (mState != MusicPlayer.STATE_PLAYING || mControls.getVisibility() != View.VISIBLE)
return;
long position = System.currentTimeMillis() - mStartTime;
if (!mSeekBarTracking)
mSeekBar.setProgress((int)(1000 * position / mDuration));
mSeekText.setText(stringForTime((int)position) + " / " + stringForTime(mDuration));
long next = 1000 - position % 1000;
mHandler.sendMessageDelayed(mHandler.obtainMessage(UPDATE_PROGRESS), next);
}
private void sendHideMessage()
{
Message message = mHandler.obtainMessage(HIDE);
mHandler.removeMessages(HIDE);
mHandler.sendMessageDelayed(message, 3000);
}
public void onClick(View view)
{
sendHideMessage();
if (view == mNextButton) {
next();
} else if (view == mPreviousButton) {
previous();
} else if (view == mPlayPauseButton) {
togglePlayback();
}
}
private static final int HIDE = 0;
private static final int UPDATE_PROGRESS = 1;
private Handler mHandler = new Handler() {
public void handleMessage(Message message) {
switch (message.what) {
case HIDE:
mControls.setVisibility(View.GONE);
break;
case UPDATE_PROGRESS:
updateProgress();
break;
}
}
};
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
if (fromUser) {
try {
mService.seekToProgress(progress);
} catch (RemoteException e) {
}
}
}
public void onStartTrackingTouch(SeekBar seekBar)
{
mHandler.removeMessages(HIDE);
mSeekBarTracking = true;
}
public void onStopTrackingTouch(SeekBar seekBar)
{
sendHideMessage();
mSeekBarTracking = false;
}
}