Optimize CoverView scrolling by only redrawing the dirty region

This commit is contained in:
Christopher Eby 2011-11-08 17:23:26 -06:00
parent aadbf2c9e2
commit c880907a03

View File

@ -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);
}
}