Re-implement CoverView.

This implementation should hopefully drop less frames.
This commit is contained in:
Adrian Ulrich 2017-08-03 20:35:56 +02:00
parent b308b97880
commit 6bbc3800c2
2 changed files with 502 additions and 361 deletions

View File

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2012 Christopher Eby <kreed@kreed.org> * Copyright (C) 2017 Adrian Ulrich <adrian@blinkenlights.ch>
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -26,93 +25,79 @@ package ch.blinkenlights.android.vanilla;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.view.View;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker; import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.animation.LinearInterpolator;
import android.widget.Scroller; import android.widget.Scroller;
import java.lang.IllegalStateException;
import java.lang.IllegalArgumentException;
import android.util.Log;
/** /**
* Displays a flingable/draggable View of cover art/song info images * Displays a flingable/draggable View of cover art/song info images
* generated by CoverBitmap. * generated by CoverBitmap.
*/ */
public final class CoverView extends View implements Handler.Callback { public final class CoverView extends View implements Handler.Callback {
/** /**
* The system-provided snap velocity, used as a threshold for detecting * If >= 0, perform the switch after this delay
* flings.
*/ */
private static int sSnapVelocity = -1; private final static int ASYNC_SWITCH = -1;
/** /**
* The screen density, from {@link DisplayMetrics#density}. * Maximum amount of pixels we are allowed to scroll to consider
* touch events to be normal touches.
*/
private final static double TOUCH_MAX_SCROLL_PX = 10;
/**
* The system provided display density
*/ */
private static double sDensity = -1; private static double sDensity = -1;
/** /**
* The Handler with which to do background work. Will be null until * The minimum velocity to move to the next song
* setupHandler is called.
*/ */
private Handler mHandler; private static double sSnapVelocity = -1;
/**
* 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.
*/
void shiftCurrentSong(int delta);
/**
* Called when the user has swiped up on the view.
*/
void upSwipe();
/**
* Called when the user has swiped down on the view.
*/
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. * Computes scroll velocity to detect flings.
*/ */
private VelocityTracker mVelocityTracker; private VelocityTracker mVelocityTracker;
/**
* The context.
*/
private final Context mContext;
/**
* The scroller instance we are using.
*/
private final CoverScroller mScroller;
/**
* Our bitmap cache helper
*/
private BitmapBucket mBitmapBucket;
/**
* Our callback to dispatch song events.
*/
private CoverView.Callback mCallback;
/**
* Indicates that we attempted to query and update our songs but couldn't as the
* view was not yet ready.
*/
private boolean mPendingQuery;
/**
* The x coordinate of the initial touch event.
*/
private float mInitialMotionX;
/**
* The y coordinate of the initial touch event.
*/
private float mInitialMotionY;
/** /**
* The x coordinate of the last touch down or move event. * The x coordinate of the last touch down or move event.
*/ */
@ -122,82 +107,190 @@ public final class CoverView extends View implements Handler.Callback {
*/ */
private float mLastMotionY; private float mLastMotionY;
/** /**
* The x coordinate of the last touch down event. * The message handler used by this class.
*/ */
private float mStartX; private Handler mHandler;
/** /**
* The y coordinate of the last touch down event. * Our current scroll position.
* Setting this to '0' means that we will display bitmap[0].
*/ */
private float mStartY; private int mScrollX = -1;
/** /**
* Ignore the next pointer up event, for long presses. * The style to use for the cover.
*/ */
private boolean mIgnoreNextUp; private int mCoverStyle = -1;
/** /**
* If true, querySongs was called before the view initialized and should * Our public callback interface
* be called when initialization finishes.
*/ */
private boolean mPendingQuery; public interface Callback {
/** void shiftCurrentSong(int delta);
* The current x scroll position of the view. void upSwipe();
* void downSwipe();
* 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 * Constructs a new CoverView class, note that setup() must be called
* the CoverView is used. * before the view becomes useable.
*
* @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) public CoverView(Context context, AttributeSet attributes) {
{ super(context, attributes);
if (sDensity == -1) {
sDensity = context.getResources().getDisplayMetrics().density;
sSnapVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * 5;
}
mContext = context;
mBitmapBucket = new BitmapBucket();
mScroller = new CoverScroller(context);
}
/**
* Configures and sets up this view
*/
public void setup(Looper looper, Callback callback, int style) {
mHandler = new Handler(looper, this); mHandler = new Handler(looper, this);
mCallback = callback; mCallback = callback;
mCoverStyle = style; mCoverStyle = style;
} }
/** /**
* Reset the scroll position to its default state. * Sent if the songs timeline changed and we should check if
* mCacheBitmap is stale.
* Just calls querySongsInternal() via handler to ensure
* that we do this in a background thread.
*/ */
private void resetScroll() public void querySongs() {
{ mHandler.removeMessages(MSG_QUERY_SONGS);
if (!mScroller.isFinished()) mHandler.sendEmptyMessage(MSG_QUERY_SONGS);
mScroller.abortAnimation();
mScrollX = getWidth();
invalidate();
} }
/**
* Called if a specific song got replaced.
* The current implementation does not take this hint into
* account as querySongsInternal() already tries to be efficient.
*/
public void replaceSong(int delta, Song song) {
querySongs();
}
/**
* Called by querySongs() - this runs in a background thread.
*/
private void querySongsInternal() {
DEBUG("querySongsInternal");
if (getWidth() < 1 || getHeight() < 1) {
mPendingQuery = true;
return;
}
if (mScrollX < 0) { // initialize mScrollX to show cover '1' by default.
mScrollX = getWidth();
}
mHandler.removeMessages(MSG_SET_BITMAP);
PlaybackService service = PlaybackService.get(mContext);
final Song[] songs = { service.getSong(-1), service.getSong(0), service.getSong(1) };
final int len = songs.length;
for (int i = 0; i < len; i++) {
Song song = songs[i];
if (mBitmapBucket.getSong(i) != song) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_BITMAP, i, 0, song));
}
}
}
/**
* Updates the cover in bitmap bucket for given index.
*
* @param i the index to modify
* @param song the source of the cover
*/
private void setSongBitmap(int i, Song song) {
Bitmap bitmap = mBitmapBucket.grepBitmap(song);
if (bitmap == null)
bitmap = generateBitmap(song);
mBitmapBucket.setSongBitmap(i, song, bitmap);
postInvalidateOnAnimation();
}
/**
* Returns a correctly sized cover bitmap for given song
*/
private Bitmap generateBitmap(Song song) {
int style = mCoverStyle;
Bitmap cover = song == null ? null : song.getCover(mContext);
if (cover == null && style != CoverBitmap.STYLE_OVERLAPPING_BOX) {
cover = CoverBitmap.generateDefaultCover(mContext, getWidth(), getHeight());
}
return CoverBitmap.createBitmap(mContext, style, cover, song, getWidth(), getHeight());
}
private final static int MSG_QUERY_SONGS = 1;
private final static int MSG_LONG_CLICK = 2;
private final static int MSG_SHIFT_SONG = 3;
private final static int MSG_SET_BITMAP = 4;
@Override @Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) public boolean handleMessage(Message message) {
{ switch (message.what) {
case MSG_QUERY_SONGS:
querySongsInternal();
break;
case MSG_LONG_CLICK:
if (scrollIsNotSignificant()) {
performLongClick();
}
break;
case MSG_SHIFT_SONG:
DEBUG("Shifting to song: "+message.arg1);
mCallback.shiftCurrentSong(message.arg1);
break;
case MSG_SET_BITMAP:
setSongBitmap(message.arg1, (Song)message.obj);
break;
default:
throw new IllegalArgumentException("Unknown message received: "+message.what);
}
return true;
}
/**
* Triggers if the view changes its size, may call querySongs() if it was called
* previously but had to be aborted as the view was not yet laid out.
*/
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
if (mPendingQuery && width != 0 && height != 0) { if (mPendingQuery && width != 0 && height != 0) {
mPendingQuery = false; mPendingQuery = false;
querySongs(PlaybackService.get(getContext())); querySongs();
}
}
/**
* Lays out this view - only handles our specific use cases
*/
@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);
} }
} }
@ -205,309 +298,357 @@ public final class CoverView extends View implements Handler.Callback {
* Paint the cover art views to the canvas. * Paint the cover art views to the canvas.
*/ */
@Override @Override
protected void onDraw(Canvas canvas) protected void onDraw(Canvas canvas) {
{
int width = getWidth(); int width = getWidth();
int height = getHeight(); int height = getHeight();
int x = 0; int x = 0;
int scrollX = mScrollX; int scrollX = mScrollX;
double padding = 14 * sDensity; double padding = 14 * sDensity;
boolean snapshot = !mScroller.isFinished();
Bitmap bitmap;
for (Bitmap bitmap : mActiveBitmaps) { for (int i=0; i <= 2 ; i++) {
bitmap = snapshot ? mBitmapBucket.getSnapshot(i) : mBitmapBucket.getBitmap(i);
if (bitmap != null && scrollX + width > x && scrollX < x + width) { if (bitmap != null && scrollX + width > x && scrollX < x + width) {
int xOffset = (width - bitmap.getWidth()) / 2; final int xOffset = (width - bitmap.getWidth()) / 2;
int yOffset = (int)(padding + (height - bitmap.getHeight()) / 2); final int yOffset = (int)(padding + (height - bitmap.getHeight()) / 2);
canvas.drawBitmap(bitmap, x + xOffset - scrollX, yOffset, null); canvas.drawBitmap(bitmap, x + xOffset - scrollX, yOffset, null);
} }
x += width; x += width;
} }
advanceScroll();
} }
/** /**
* Scrolls the view when dragged. Animates a fling to one of the three covers * Advances to the next frame of the animation.
* 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. private void advanceScroll() {
* boolean running = mScroller.computeScrollOffset();
* Also performs a click on the view when it is tapped without dragging. if (running) {
mScrollX = mScroller.getCurrX();
if (mScroller.isFinished()) {
// just hit the end!
mBitmapBucket.finalizeScroll();
mScrollX = getWidth();
int coverIntent = mScroller.getCoverIntent();
if (coverIntent != 0 && ASYNC_SWITCH < 0)
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHIFT_SONG, coverIntent, 0));
DEBUG("Scroll finished, invalidating all snapshot bitmaps!");
}
invalidate();
}
}
/**
* Handles all touch events received by this view.
*/ */
@Override @Override
public boolean onTouchEvent(MotionEvent ev) public boolean onTouchEvent(MotionEvent ev) {
{
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
float x = ev.getX(); float x = ev.getX();
float y = ev.getY(); float y = ev.getY();
int scrollX = mScrollX; int scrollX = mScrollX;
int width = getWidth(); int width = getWidth();
boolean invalidate = false;
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
switch (ev.getAction()) { switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
mActiveBitmaps = mBitmaps;
}
mStartX = x; if (mScroller.isFinished()) {
mStartY = y; mHandler.sendEmptyMessageDelayed(MSG_LONG_CLICK, ViewConfiguration.getLongPressTimeout());
mLastMotionX = x; } else {
mLastMotionY = y; // Animation was still running while we got a new down event
mScrolling = true; // Abort the current animation!
mUiHandler.sendEmptyMessageDelayed(MSG_LONG_CLICK, ViewConfiguration.getLongPressTimeout()); final int coverIntent = mScroller.getCoverIntent();
break; mBitmapBucket.abortScroll();
case MotionEvent.ACTION_MOVE: { mScroller.abortAnimation();
float deltaX = mLastMotionX - x;
float deltaY = mLastMotionY - y;
if (Math.abs(deltaX) > Math.abs(deltaY)) { if (coverIntent != 0) {
if (deltaX < 0) { // The running animation was actually supposed to switch to a new song.
int availableToScroll = scrollX - (mSongs[0] == null ? width : 0); // Do this right now as the animation is canceled:
if (availableToScroll > 0) { // First, set our non-cached covers to a sane version
mScrollX += Math.max(-availableToScroll, (int)deltaX); mBitmapBucket.prepareScroll(coverIntent);
invalidate(); // ..and fix up the scrolling position.
mScrollX -= coverIntent * getWidth();
// all done: we can now trigger the song jump
if (ASYNC_SWITCH < 0 || mHandler.hasMessages(MSG_SHIFT_SONG)) {
mHandler.removeMessages(MSG_SHIFT_SONG);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHIFT_SONG, coverIntent, 0));
}
} }
} else if (deltaX > 0) {
int availableToScroll = width * 2 - scrollX; // There is no running animation (anymore?), so we can drop the cache.
if (availableToScroll > 0) { mBitmapBucket.finalizeScroll();
mScrollX += Math.min(availableToScroll, (int)deltaX); }
invalidate();
mLastMotionX = mInitialMotionX = x;
mLastMotionY = mInitialMotionY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
final float deltaX = mLastMotionX - x;
final float deltaY = mLastMotionY - y;
if (Math.abs(deltaX) > Math.abs(deltaY)) { // only change X if the fling is horizontal
if (deltaX < 0) {
int availableToScroll = scrollX - (mBitmapBucket.getSong(0) == null ? width : 0);
if (availableToScroll > 0) {
mScrollX += Math.max(-availableToScroll, (int)deltaX);
invalidate = true;
}
} else if (deltaX > 0) {
int availableToScroll = width * 2 - scrollX;
if (availableToScroll > 0) {
mScrollX += Math.min(availableToScroll, (int)deltaX);
invalidate = true;
}
} }
} }
mLastMotionX = x;
mLastMotionY = y;
break;
} }
case MotionEvent.ACTION_UP: {
mLastMotionX = x; VelocityTracker velocityTracker = mVelocityTracker;
mLastMotionY = y; velocityTracker.computeCurrentVelocity(1000); // report velocity in pixels-per-second, as assumed by snap-velocity and co.
break; final int velocityX = (int) velocityTracker.getXVelocity();
} final int velocityY = (int) velocityTracker.getYVelocity();
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.recycle();
mVelocityTracker = null; mVelocityTracker = null;
}
break; final int mvx = Math.abs(velocityX);
} final int mvy = Math.abs(velocityY);
final float distanceX = mLastMotionX - mInitialMotionX;
int whichCover = 0;
if (scrollIsNotSignificant()) {
if (mHandler.hasMessages(MSG_LONG_CLICK)) {
// long click didn't fire yet -> consider this to be a normal click
performClick();
}
} else if (Math.abs(distanceX) > width/2) {
whichCover = distanceX < 0 ? 1 : -1;
} else if (mvx > sSnapVelocity || mvy > sSnapVelocity) {
if (mvy > mvx) {
if (velocityY > 0)
mCallback.downSwipe();
else
mCallback.upSwipe();
} else {
whichCover = velocityX < 0 ? 1 : -1;
}
}
// Ensure that the target song actually exists.
// Eg: We may not have song 0 in random mode.
if (mBitmapBucket.getSong(1+whichCover) == null)
whichCover = 0;
final int scrollTargetX = width + whichCover*width;
mBitmapBucket.prepareScroll(whichCover);
mScroller.handleFling(velocityX, mScrollX, scrollTargetX, whichCover);
if (ASYNC_SWITCH >= 0)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHIFT_SONG, whichCover, 0), ASYNC_SWITCH);
mHandler.removeMessages(MSG_LONG_CLICK);
invalidate = true;
break;
}
} }
if (invalidate)
postInvalidateOnAnimation();
return true; return true;
} }
/** /**
* Generates a bitmap for the given song. * Returns true if the scroll traveled a significant distance
* * and is therefore not considered to be random noise during a click.
* @param i The position of the song in mSongs.
*/ */
private void generateBitmap(int i) private boolean scrollIsNotSignificant() {
{ final float distanceX = mLastMotionX - mInitialMotionX;
if(getWidth() == 0 || getHeight() == 0) { final float distanceY = mLastMotionY - mInitialMotionY;
// View isn't laid out - can't generate the bitmap until we know the size return Math.abs(distanceX) + Math.abs(distanceY) < TOUCH_MAX_SCROLL_PX;
mPendingQuery = true;
return;
}
Song song = mSongs[i];
int style = mCoverStyle;
Context context = getContext();
Bitmap cover = song == null ? null : song.getCover(context);
if (cover == null && style != CoverBitmap.STYLE_OVERLAPPING_BOX) {
if (mDefaultCover == null) {
mDefaultCover = CoverBitmap.generateDefaultCover(context, getWidth(), getHeight());
}
cover = mDefaultCover;
}
mBitmaps[i] = CoverBitmap.createBitmap(context, style, cover, song, getWidth(), getHeight());
postInvalidate();
} }
/** private void DEBUG(String s) {
* Set the Song at position <code>i</code> to <code>song</code>, generating // Log.v("VanillaMusicCover", s);
* 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. * Class to handle access to our bitmap and song mapping
*
* @param service Service to query from.
*/ */
public void querySongs(PlaybackService service) private class BitmapBucket {
{ /**
if (getWidth() == 0 || getHeight() == 0) { * The pre-generated bitmaps for all 3 songs
mPendingQuery = true; */
return; private final Bitmap[] mCacheBitmaps = new Bitmap[3];
/**
* Cached songs, used to check if mCacheBitmaps is still valid
*/
private final Song[] mCacheSongs = new Song[3];
/**
* A WIP copy: We use this (iff available) to draw.
* This allows us to update mCacheBitmaps while scrolling
*/
private final Bitmap[] mSnapshotBitmaps = new Bitmap[3];
/**
* A WIP copy of songs, used if we have to restore
* This snapshot is only used internally and copied
* to mCacheBitmaps if we restore our bitmap snapshot.
*/
private final Song[] mSnapshotSongs = new Song[3];
/**
* Constructor for BitmapBucket
*/
public BitmapBucket() {
} }
mHandler.removeMessages(MSG_GENERATE_BITMAP); public Song getSong(int i) {
return mCacheSongs[i];
}
Song[] songs = mSongs; public Bitmap getBitmap(int i) {
Bitmap[] bitmaps = mBitmaps; return mCacheBitmaps[i];
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) { public Bitmap getSnapshot(int i) {
if (newSongs[i] == null) return mSnapshotBitmaps[i];
continue; }
for (int j = 0; j != 3; ++j) { public void setSongBitmap(int i, Song song, Bitmap bitmap) {
if (newSongs[i] == songs[j]) { mCacheSongs[i] = song;
newBitmaps[i] = bitmaps[j]; mCacheBitmaps[i] = bitmap;
break; }
public Bitmap grepBitmap(Song song) {
final int len = mCacheSongs.length;
for (int i = 0; i < len ; i++) {
if (mCacheSongs[i] == song) {
return mCacheBitmaps[i];
} }
} }
return null;
}
if (newBitmaps[i] == null) { /**
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0)); * Hint that we are going to scroll.
* This causes us to populate our bitmap snapshot
* and will cause us to modify the current cache with a guess.
*
* @param futureCover the cover we are going to scroll to
*/
public void prepareScroll(int futureCover) {
// Grab a snapshot of the bitmaps which will be used
// while the animation is running, so that we can concurrently
// modify mCacheBitmaps.
System.arraycopy(mCacheBitmaps, 0, mSnapshotBitmaps, 0, 3);
System.arraycopy(mCacheSongs, 0, mSnapshotSongs, 0, 3);
// we are going to scroll, so most likely we can save 2 bitmaps by guessing the
// new situation. This doesn't have to be 100% correct as the next querySongs()
// call would fix up wrong guesses.
// FIXME: This may be under-locked: cache checks via getSong() and prepareScroll() may race.
if (futureCover > 0) {
mCacheBitmaps[0] = mCacheBitmaps[1];
mCacheSongs[0] = mCacheSongs[1];
mCacheBitmaps[1] = mCacheBitmaps[2];
mCacheSongs[1] = mCacheSongs[2];
mCacheBitmaps[2] = null;
mCacheSongs[2] = new Song(-1);
} else if (futureCover < 0) {
mCacheBitmaps[2] = mCacheBitmaps[1];
mCacheSongs[2] = mCacheSongs[1];
mCacheBitmaps[1] = mCacheBitmaps[0];
mCacheSongs[1] = mCacheSongs[0];
mCacheBitmaps[0] = null;
mCacheSongs[0] = new Song(-1);
} }
} }
resetScroll(); /**
} * Abort a scroll initiated by prepareScroll.
*/
/** public void abortScroll() {
* Call {@link CoverView#generateBitmap(int)} for the song at the given index. // undo our guess we did in prepareScroll
* System.arraycopy(mSnapshotBitmaps, 0, mCacheBitmaps, 0, 3);
* arg1 should be the index of the song. System.arraycopy(mSnapshotSongs, 0, mCacheSongs, 0, 3);
*/ finalizeScroll();
private static final int MSG_GENERATE_BITMAP = 0; }
/**
* Perform a long click. public void finalizeScroll() {
* for (int i=0; i <= 2; i++) {
* @see View#performLongClick() mSnapshotBitmaps[i] = null;
*/ mSnapshotSongs[i] = null;
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) * The scroller class helps to keep track
{ * of the current scroll progress.
// This implementation only tries to handle two cases: use in the */
// FullPlaybackActivity, where we want to fill the whole screen, private class CoverScroller extends Scroller {
// and use in the MiniPlaybackActivity, where we want to be square. /**
* The cover we are scrolling to
*/
private int mCoverIntent;
/**
* Returns a new scroller instance
*/
public CoverScroller(Context context) {
super(context, new LinearInterpolator(), false);
}
int width = View.MeasureSpec.getSize(widthSpec); /**
int height = View.MeasureSpec.getSize(heightSpec); * Returns the cover set by the last handleFling
* call.
*
* @return int the cover set by handleFling.
*/
public int getCoverIntent() {
return mCoverIntent;
}
if (View.MeasureSpec.getMode(widthSpec) == View.MeasureSpec.EXACTLY @Override
&& View.MeasureSpec.getMode(heightSpec) == View.MeasureSpec.EXACTLY) { public void abortAnimation() {
// FullPlaybackActivity: fill screen mCoverIntent = 0;
setMeasuredDimension(width, height); super.abortAnimation();
} else { }
// MiniPlaybackActivity: be square /**
int size = Math.min(width, height); * Starts a fling operation
setMeasuredDimension(size, size); *
* @param velocity the current velocity
* @param from the current x coordinate
* @param to the target x coordinate
* @param coverIntent the cover we are scrolling to, returned by getCoverIntent()
*/
public void handleFling(int velocity, int from, int to, int coverIntent) {
if (!isFinished()) {
abortAnimation();
}
mCoverIntent = coverIntent;
final int distance = to - from;
int duration = (int)(Math.abs(distance) / sDensity);
if (duration > 200)
duration = 200;
startScroll(from, 0, distance, 0, duration);
} }
} }
} }

View File

@ -323,7 +323,7 @@ public abstract class PlaybackActivity extends Activity
protected void onSongChange(Song song) protected void onSongChange(Song song)
{ {
if (mCoverView != null) if (mCoverView != null)
mCoverView.querySongs(PlaybackService.get(this)); mCoverView.querySongs();
} }
protected void setSong(final Song song) protected void setSong(final Song song)
@ -375,7 +375,7 @@ public abstract class PlaybackActivity extends Activity
public void replaceSong(int delta, Song song) public void replaceSong(int delta, Song song)
{ {
if (mCoverView != null) if (mCoverView != null)
mCoverView.setSong(delta + 1, song); mCoverView.replaceSong(delta, song);
} }
/** /**