diff --git a/src/org/kreed/vanilla/CoverView.java b/src/org/kreed/vanilla/CoverView.java index fa9d347f..25b19a7e 100644 --- a/src/org/kreed/vanilla/CoverView.java +++ b/src/org/kreed/vanilla/CoverView.java @@ -41,28 +41,50 @@ import android.widget.Scroller; * 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 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 next (right) cover. + */ public void nextSong(); + /** + * Called after the view has scrolled to the previous (left) cover. + */ public void previousSong(); + /** + * Called when the user has swiped up on the view. + */ public void upSwipe(); + /** + * Called when the user had 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. */ @@ -79,24 +101,49 @@ public final class CoverView extends View implements Handler.Callback { * 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; + /** + * The index of the cover that is being scrolled to during a fling + * animation, or -1 if the cover is the active (middle) cover. + */ private int mTentativeCover = -1; /** * 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; + /** + * If true, calls to invalidate() will do nothing. We use this so we can + * only invalidate the dirty rect during scrolling. + */ + private boolean mSuppressInvalidate; /** * Constructor intended to be called by inflating from XML. @@ -151,21 +198,20 @@ public final class CoverView extends View implements Handler.Callback { @Override protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - int width = getWidth(); int height = getHeight(); + int x = 0; int scrollX = getScrollX(); canvas.drawColor(Color.BLACK); - for (int x = 0, i = 0; i != 3; ++i, x += width) { - Bitmap bitmap = mBitmaps[i]; + for (Bitmap bitmap : mBitmaps) { 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, yOffset, null); } + x += width; } } @@ -198,7 +244,7 @@ public final class CoverView extends View implements Handler.Callback { mLastMotionX = x; mLastMotionY = y; - mHandler.sendEmptyMessageDelayed(MSG_LONG_CLICK, ViewConfiguration.getLongPressTimeout()); + mUiHandler.sendEmptyMessageDelayed(MSG_LONG_CLICK, ViewConfiguration.getLongPressTimeout()); break; case MotionEvent.ACTION_MOVE: { float deltaX = mLastMotionX - x; @@ -221,7 +267,7 @@ public final class CoverView extends View implements Handler.Callback { break; } case MotionEvent.ACTION_UP: { - mHandler.removeMessages(MSG_LONG_CLICK); + mUiHandler.removeMessages(MSG_LONG_CLICK); VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(250); @@ -266,7 +312,7 @@ public final class CoverView extends View implements Handler.Callback { if (whichCover != 1) mTentativeCover = whichCover; - postInvalidate(); + mUiHandler.sendEmptyMessage(MSG_SCROLL); if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -279,28 +325,6 @@ public final class CoverView extends View implements Handler.Callback { return true; } - /** - * 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. - */ - @Override - public void computeScroll() - { - if (mScroller.computeScrollOffset()) { - scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - postInvalidate(); - } else if (mTentativeCover != -1) { - int delta = mTentativeCover - 1; - mTentativeCover = -1; - if (delta == 1) - mCallback.nextSong(); - else - mCallback.previousSong(); - resetScroll(); - } - } - /** * Generates a bitmap for the given song if the cache does not contain one * for it, or moves the bitmap to the top of the cache if it does. @@ -390,6 +414,13 @@ public final class CoverView extends View implements Handler.Callback { * @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; public boolean handleMessage(Message message) { @@ -403,6 +434,27 @@ public final class CoverView extends View implements Handler.Callback { performLongClick(); } break; + case MSG_SCROLL: + if (mScroller.computeScrollOffset()) { + // scrollTo calls invalidate(), however, we want to invalidate + // only the region where the covers are drawn, so we need to + // suppress this call. + mSuppressInvalidate = true; + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + mSuppressInvalidate = false; + + invalidateCovers(); + mUiHandler.sendEmptyMessage(MSG_SCROLL); + } else if (mTentativeCover != -1) { + int delta = mTentativeCover - 1; + mTentativeCover = -1; + if (delta == 1) + mCallback.nextSong(); + else + mCallback.previousSong(); + resetScroll(); + } + break; default: return false; } @@ -430,4 +482,40 @@ public final class CoverView extends View implements Handler.Callback { setMeasuredDimension(size, size); } } + + /** + * Overridden to allow redraws to be suppressed. + */ + @Override + public void invalidate() + { + if (!mSuppressInvalidate) + super.invalidate(); + } + + /** + * Call {@link View#invalidate(int,int,int,int)} with the area + * containing the visible cover(s). + */ + public void invalidateCovers() + { + int width = getWidth(); + int height = getHeight(); + int scrollX = getScrollX(); + int x = 0; + int maxHeight = 0; + + for (Bitmap bitmap : mBitmaps) { + if (bitmap != null && scrollX + width > x && scrollX < x + width) { + int bitmapHeight = bitmap.getHeight(); + if (bitmapHeight > maxHeight) { + maxHeight = bitmapHeight; + } + } + x += width; + } + + int offset = (height - maxHeight) / 2; + invalidate(scrollX, offset, scrollX + width, height - offset); + } }