package org.music.player;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
/**
* Displays a flingable/draggable View of cover art/song info images
* generated by CoverBitmap.
*/
public final class CoverView extends View implements Handler.Callback {
/**
* The system-provided snap velocity, used as a threshold for detecting
* flings.
*/
private static int sSnapVelocity = -1;
/**
* The screen density, from {@link DisplayMetrics#density}.
*/
private static double sDensity = -1;
/**
* The Handler with which to do background work. Will be null until
* setupHandler is called.
*/
private Handler mHandler;
/**
* A handler running on the UI thread, for UI operations.
*/
private final Handler mUiHandler = new Handler(this);
/**
* How to render cover art and metadata. One of
* CoverBitmap.STYLE_*
*/
private int mCoverStyle;
/**
* Interface to respond to CoverView motion actions.
*/
public interface Callback {
/**
* Called after the view has scrolled to the previous or next cover.
*
* @param delta -1 for the previous cover, 1 for the next.
*/
public void shiftCurrentSong(int delta);
/**
* Called when the user has swiped up on the view.
*/
public void upSwipe();
/**
* Called when the user has swiped down on the view.
*/
public void downSwipe();
}
/**
* The instance of the callback.
*/
private Callback mCallback;
/**
* The current set of songs: 0 = previous, 1 = current, and 2 = next.
*/
private Song[] mSongs = new Song[3];
/**
* The covers for the current songs: 0 = previous, 1 = current, and 2 = next.
*/
private Bitmap[] mBitmaps = new Bitmap[3];
/**
* The bitmaps to be drawn. Usually the same as mBitmaps, unless scrolling.
*/
private Bitmap[] mActiveBitmaps = mBitmaps;
/**
* Cover art to use when a song has no cover art in no info display styles.
*/
private Bitmap mDefaultCover;
/**
* Computes scroll animations.
*/
private final Scroller mScroller;
/**
* Computes scroll velocity to detect flings.
*/
private VelocityTracker mVelocityTracker;
/**
* The x coordinate of the last touch down or move event.
*/
private float mLastMotionX;
/**
* The y coordinate of the last touch down or move event.
*/
private float mLastMotionY;
/**
* The x coordinate of the last touch down event.
*/
private float mStartX;
/**
* The y coordinate of the last touch down event.
*/
private float mStartY;
/**
* Ignore the next pointer up event, for long presses.
*/
private boolean mIgnoreNextUp;
/**
* If true, querySongs was called before the view initialized and should
* be called when initialization finishes.
*/
private boolean mPendingQuery;
/**
* The current x scroll position of the view.
*
* Scrolling code from {@link View} is not used for this class since many of
* its features are not required.
*/
private int mScrollX;
/**
* True if a scroll is in progress (i.e. mScrollX != getWidth()), false
* otherwise.
*/
private boolean mScrolling;
/**
* Constructor intended to be called by inflating from XML.
*/
public CoverView(Context context, AttributeSet attributes)
{
super(context, attributes);
mScroller = new Scroller(context);
if (sSnapVelocity == -1) {
sSnapVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
sDensity = context.getResources().getDisplayMetrics().density;
}
}
/**
* Setup the Handler and callback. This must be called before
* the CoverView is used.
*
* @param looper A looper created on a worker thread.
* @param callback The callback for nextSong/previousSong
* @param style One of CoverBitmap.STYLE_*
*/
public void setup(Looper looper, Callback callback, int style)
{
mHandler = new Handler(looper, this);
mCallback = callback;
mCoverStyle = style;
}
/**
* Reset the scroll position to its default state.
*/
private void resetScroll()
{
if (!mScroller.isFinished())
mScroller.abortAnimation();
mScrollX = getWidth();
invalidate();
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
{
if (mPendingQuery && width != 0 && height != 0) {
mPendingQuery = false;
querySongs(PlaybackService.get(getContext()));
}
}
/**
* Paint the cover art views to the canvas.
*/
@Override
protected void onDraw(Canvas canvas)
{
int width = getWidth();
int height = getHeight();
int x = 0;
int scrollX = mScrollX;
canvas.drawColor(Color.BLACK);
for (Bitmap bitmap : mActiveBitmaps) {
if (bitmap != null && scrollX + width > x && scrollX < x + width) {
int xOffset = (width - bitmap.getWidth()) / 2;
int yOffset = (height - bitmap.getHeight()) / 2;
canvas.drawBitmap(bitmap, x + xOffset - scrollX, yOffset, null);
}
x += width;
}
}
/**
* Scrolls the view when dragged. Animates a fling to one of the three covers
* when finished. The cover flung to will be either the nearest cover, or if
* the fling is fast enough, the cover in the direction of the fling.
*
* Also performs a click on the view when it is tapped without dragging.
*/
@Override
public boolean onTouchEvent(MotionEvent ev)
{
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
float x = ev.getX();
float y = ev.getY();
int scrollX = mScrollX;
int width = getWidth();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
mActiveBitmaps = mBitmaps;
}
mStartX = x;
mStartY = y;
mLastMotionX = x;
mLastMotionY = y;
mScrolling = true;
mUiHandler.sendEmptyMessageDelayed(MSG_LONG_CLICK, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE: {
float deltaX = mLastMotionX - x;
float deltaY = mLastMotionY - y;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX < 0) {
int availableToScroll = scrollX - (mSongs[0] == null ? width : 0);
if (availableToScroll > 0) {
mScrollX += Math.max(-availableToScroll, (int)deltaX);
invalidate();
}
} else if (deltaX > 0) {
int availableToScroll = width * 2 - scrollX;
if (availableToScroll > 0) {
mScrollX += Math.min(availableToScroll, (int)deltaX);
invalidate();
}
}
}
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_UP: {
mUiHandler.removeMessages(MSG_LONG_CLICK);
VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(250);
int velocityX = (int) velocityTracker.getXVelocity();
int velocityY = (int) velocityTracker.getYVelocity();
int mvx = Math.abs(velocityX);
int mvy = Math.abs(velocityY);
// If -1 or 1, play the previous or next song, respectively and scroll
// to that song's cover. If 0, just scroll back to current song's cover.
int whichCover = 0;
int min = mSongs[0] == null ? 0 : -1;
int max = 1;
if (Math.abs(mStartX - x) + Math.abs(mStartY - y) < 10) {
// A long press was performed and thus the normal action should
// not be executed.
if (mIgnoreNextUp)
mIgnoreNextUp = false;
else
performClick();
} else if (mvx > sSnapVelocity || mvy > sSnapVelocity) {
if (mvy > mvx) {
if (velocityY > 0)
mCallback.downSwipe();
else
mCallback.upSwipe();
} else {
if (velocityX > 0)
whichCover = min;
else
whichCover = max;
}
} else {
int nearestCover = (scrollX + width / 2) / width - 1;
whichCover = Math.max(min, Math.min(nearestCover, max));
}
if (whichCover != 0) {
scrollX = scrollX - width * whichCover;
Bitmap[] bitmaps = mBitmaps;
// Save the two covers being scrolled between, so that if one
// of them changes from switching songs (which can happen when
// shuffling), the new cover doesn't pop in during the scroll.
// mActiveBitmaps is reset when the scroll is finished.
if (whichCover == 1) {
mActiveBitmaps = new Bitmap[] { bitmaps[1], bitmaps[2], null };
} else {
mActiveBitmaps = new Bitmap[] { null, bitmaps[0], bitmaps[1] };
}
mCallback.shiftCurrentSong(whichCover);
mScrollX = scrollX;
}
int delta = width - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, (int)(Math.abs(delta) * 2 / sDensity));
mUiHandler.sendEmptyMessage(MSG_SCROLL);
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
return true;
}
/**
* Generates a bitmap for the given song.
*
* @param i The position of the song in mSongs.
*/
private void generateBitmap(int i)
{
Song song = mSongs[i];
int style = mCoverStyle;
Context context = getContext();
Bitmap cover = song == null ? null : song.getCover(context);
if (cover == null && style == CoverBitmap.STYLE_NO_INFO) {
Bitmap def = mDefaultCover;
if (def == null) {
mDefaultCover = def = CoverBitmap.generateDefaultCover(getWidth(), getHeight());
}
mBitmaps[i] = def;
} else {
mBitmaps[i] = CoverBitmap.createBitmap(context, style, cover, song, getWidth(), getHeight());
}
postInvalidate();
}
/**
* Set the Song at position <code>i</code> to <code>song</code>, generating
* the bitmap for it in the background if needed.
*/
public void setSong(int i, Song song)
{
if (song == mSongs[i])
return;
mSongs[i] = song;
mBitmaps[i] = null;
if (song != null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0));
}
}
/**
* Query all songs. Must be called on the UI thread.
*
* @param service Service to query from.
*/
public void querySongs(PlaybackService service)
{
if (getWidth() == 0 || getHeight() == 0) {
mPendingQuery = true;
return;
}
mHandler.removeMessages(MSG_GENERATE_BITMAP);
Song[] songs = mSongs;
Bitmap[] bitmaps = mBitmaps;
Song[] newSongs = { service.getSong(-1), service.getSong(0), service.getSong(1) };
Bitmap[] newBitmaps = new Bitmap[3];
mSongs = newSongs;
mBitmaps = newBitmaps;
if (!mScrolling)
mActiveBitmaps = newBitmaps;
for (int i = 0; i != 3; ++i) {
if (newSongs[i] == null)
continue;
for (int j = 0; j != 3; ++j) {
if (newSongs[i] == songs[j]) {
newBitmaps[i] = bitmaps[j];
break;
}
}
if (newBitmaps[i] == null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0));
}
}
resetScroll();
}
/**
* Call {@link CoverView#generateBitmap(int)} for the song at the given index.
*
* arg1 should be the index of the song.
*/
private static final int MSG_GENERATE_BITMAP = 0;
/**
* Perform a long click.
*
* @see View#performLongClick()
*/
private static final int MSG_LONG_CLICK = 2;
/**
* Update position for fling scroll animation and, when it is finished,
* notify PlaybackService that the user has requested a track change and
* update the cover art views. Will resend message until scrolling is
* finished.
*/
private static final int MSG_SCROLL = 3;
@Override
public boolean handleMessage(Message message)
{
switch (message.what) {
case MSG_GENERATE_BITMAP:
generateBitmap(message.arg1);
break;
case MSG_LONG_CLICK:
if (Math.abs(mStartX - mLastMotionX) + Math.abs(mStartY - mLastMotionY) < 10) {
mIgnoreNextUp = true;
performLongClick();
}
break;
case MSG_SCROLL:
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
invalidate();
mUiHandler.sendEmptyMessage(MSG_SCROLL);
} else {
mScrolling = false;
mActiveBitmaps = mBitmaps;
}
break;
default:
return false;
}
return true;
}
@Override
protected void onMeasure(int widthSpec, int heightSpec)
{
// This implementation only tries to handle two cases: use in the
// FullPlaybackActivity, where we want to fill the whole screen,
// and use in the MiniPlaybackActivity, where we want to be square.
int width = View.MeasureSpec.getSize(widthSpec);
int height = View.MeasureSpec.getSize(heightSpec);
if (View.MeasureSpec.getMode(widthSpec) == View.MeasureSpec.EXACTLY
&& View.MeasureSpec.getMode(heightSpec) == View.MeasureSpec.EXACTLY) {
// FullPlaybackActivity: fill screen
setMeasuredDimension(width, height);
} else {
// MiniPlaybackActivity: be square
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
}
}