blob: 3c3caf18f33f80aa4228916e336dbcb50026975e [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.camera;
import com.android.gallery.R;
import static com.android.camera.Util.Assert;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.widget.Scroller;
import com.android.camera.gallery.IImage;
import com.android.camera.gallery.IImageList;
import java.util.HashMap;
class GridViewSpecial extends View {
@SuppressWarnings("unused")
private static final String TAG = "GridViewSpecial";
private static final float MAX_FLING_VELOCITY = 2500;
public static interface Listener {
public void onImageClicked(int index);
public void onImageTapped(int index);
public void onLayoutComplete(boolean changed);
/**
* Invoked when the <code>GridViewSpecial</code> scrolls.
*
* @param scrollPosition the position of the scroller in the range
* [0, 1], when 0 means on the top and 1 means on the buttom
*/
public void onScroll(float scrollPosition);
}
public static interface DrawAdapter {
public void drawImage(Canvas canvas, IImage image,
Bitmap b, int xPos, int yPos, int w, int h);
public void drawDecoration(Canvas canvas, IImage image,
int xPos, int yPos, int w, int h);
public boolean needsDecoration();
}
public static final int INDEX_NONE = -1;
// There are two cell size we will use. It can be set by setSizeChoice().
// The mLeftEdgePadding fields is filled in onLayout(). See the comments
// in onLayout() for details.
static class LayoutSpec {
LayoutSpec(int w, int h, int intercellSpacing, int leftEdgePadding,
DisplayMetrics metrics) {
mCellWidth = dpToPx(w, metrics);
mCellHeight = dpToPx(h, metrics);
mCellSpacing = dpToPx(intercellSpacing, metrics);
mLeftEdgePadding = dpToPx(leftEdgePadding, metrics);
}
int mCellWidth, mCellHeight;
int mCellSpacing;
int mLeftEdgePadding;
}
private LayoutSpec [] mCellSizeChoices;
private void initCellSize() {
Activity a = (Activity) getContext();
DisplayMetrics metrics = new DisplayMetrics();
a.getWindowManager().getDefaultDisplay().getMetrics(metrics);
mCellSizeChoices = new LayoutSpec[] {
new LayoutSpec(67, 67, 8, 0, metrics),
new LayoutSpec(92, 92, 8, 0, metrics),
};
}
// Converts dp to pixel.
private static int dpToPx(int dp, DisplayMetrics metrics) {
return (int) (metrics.density * dp);
}
// These are set in init().
private final Handler mHandler = new Handler();
private GestureDetector mGestureDetector;
private ImageBlockManager mImageBlockManager;
// These are set in set*() functions.
private ImageLoader mLoader;
private Listener mListener = null;
private DrawAdapter mDrawAdapter = null;
private IImageList mAllImages = ImageManager.makeEmptyImageList();
private int mSizeChoice = 1; // default is big cell size
// These are set in onLayout().
private LayoutSpec mSpec;
private int mColumns;
private int mMaxScrollY;
// We can handle events only if onLayout() is completed.
private boolean mLayoutComplete = false;
// Selection state
private int mCurrentSelection = INDEX_NONE;
private int mCurrentPressState = 0;
private static final int TAPPING_FLAG = 1;
private static final int CLICKING_FLAG = 2;
// These are cached derived information.
private int mCount; // Cache mImageList.getCount();
private int mRows; // Cache (mCount + mColumns - 1) / mColumns
private int mBlockHeight; // Cache mSpec.mCellSpacing + mSpec.mCellHeight
private boolean mRunning = false;
private Scroller mScroller = null;
public GridViewSpecial(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
setVerticalScrollBarEnabled(true);
initializeScrollbars(context.obtainStyledAttributes(
android.R.styleable.View));
mGestureDetector = new GestureDetector(context,
new MyGestureDetector());
setFocusableInTouchMode(true);
initCellSize();
}
private final Runnable mRedrawCallback = new Runnable() {
public void run() {
invalidate();
}
};
public void setLoader(ImageLoader loader) {
Assert(mRunning == false);
mLoader = loader;
}
public void setListener(Listener listener) {
Assert(mRunning == false);
mListener = listener;
}
public void setDrawAdapter(DrawAdapter adapter) {
Assert(mRunning == false);
mDrawAdapter = adapter;
}
public void setImageList(IImageList list) {
Assert(mRunning == false);
mAllImages = list;
mCount = mAllImages.getCount();
}
public void setSizeChoice(int choice) {
Assert(mRunning == false);
if (mSizeChoice == choice) return;
mSizeChoice = choice;
}
@Override
public void onLayout(boolean changed, int left, int top,
int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!mRunning) {
return;
}
mSpec = mCellSizeChoices[mSizeChoice];
int width = right - left;
// The width is divided into following parts:
//
// LeftEdgePadding CellWidth (CellSpacing CellWidth)* RightEdgePadding
//
// We determine number of cells (columns) first, then the left and right
// padding are derived. We make left and right paddings the same size.
//
// The height is divided into following parts:
//
// CellSpacing (CellHeight CellSpacing)+
mColumns = 1 + (width - mSpec.mCellWidth)
/ (mSpec.mCellWidth + mSpec.mCellSpacing);
mSpec.mLeftEdgePadding = (width
- ((mColumns - 1) * mSpec.mCellSpacing)
- (mColumns * mSpec.mCellWidth)) / 2;
mRows = (mCount + mColumns - 1) / mColumns;
mBlockHeight = mSpec.mCellSpacing + mSpec.mCellHeight;
mMaxScrollY = mSpec.mCellSpacing + (mRows * mBlockHeight)
- (bottom - top);
// Put mScrollY in the valid range. This matters if mMaxScrollY is
// changed. For example, orientation changed from portrait to landscape.
mScrollY = Math.max(0, Math.min(mMaxScrollY, mScrollY));
generateOutlineBitmap();
if (mImageBlockManager != null) {
mImageBlockManager.recycle();
}
mImageBlockManager = new ImageBlockManager(mHandler, mRedrawCallback,
mAllImages, mLoader, mDrawAdapter, mSpec, mColumns, width,
mOutline[OUTLINE_EMPTY]);
mListener.onLayoutComplete(changed);
moveDataWindow();
mLayoutComplete = true;
}
@Override
protected int computeVerticalScrollRange() {
return mMaxScrollY + getHeight();
}
// We cache the three outlines from NinePatch to Bitmap to speed up
// drawing. The cache must be updated if the cell size is changed.
public static final int OUTLINE_EMPTY = 0;
public static final int OUTLINE_PRESSED = 1;
public static final int OUTLINE_SELECTED = 2;
public Bitmap mOutline[] = new Bitmap[3];
private void generateOutlineBitmap() {
int w = mSpec.mCellWidth;
int h = mSpec.mCellHeight;
for (int i = 0; i < mOutline.length; i++) {
mOutline[i] = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
}
Drawable cellOutline;
cellOutline = GridViewSpecial.this.getResources()
.getDrawable(android.R.drawable.gallery_thumb);
cellOutline.setBounds(0, 0, w, h);
Canvas canvas = new Canvas();
canvas.setBitmap(mOutline[OUTLINE_EMPTY]);
cellOutline.setState(EMPTY_STATE_SET);
cellOutline.draw(canvas);
canvas.setBitmap(mOutline[OUTLINE_PRESSED]);
cellOutline.setState(
PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
cellOutline.draw(canvas);
canvas.setBitmap(mOutline[OUTLINE_SELECTED]);
cellOutline.setState(ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
cellOutline.draw(canvas);
}
private void moveDataWindow() {
// Calculate visible region according to scroll position.
int startRow = (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
int endRow = (mScrollY + getHeight() - mSpec.mCellSpacing - 1)
/ mBlockHeight + 1;
// Limit startRow and endRow to the valid range.
// Make sure we handle the mRows == 0 case right.
startRow = Math.max(Math.min(startRow, mRows - 1), 0);
endRow = Math.max(Math.min(endRow, mRows), 0);
mImageBlockManager.setVisibleRows(startRow, endRow);
}
// In MyGestureDetector we have to check canHandleEvent() because
// GestureDetector could queue events and fire them later. At that time
// stop() may have already been called and we can't handle the events.
private class MyGestureDetector extends SimpleOnGestureListener {
private AudioManager mAudioManager;
@Override
public boolean onDown(MotionEvent e) {
if (!canHandleEvent()) return false;
if (mScroller != null && !mScroller.isFinished()) {
mScroller.forceFinished(true);
return false;
}
int index = computeSelectedIndex(e.getX(), e.getY());
if (index >= 0 && index < mCount) {
setSelectedIndex(index);
} else {
setSelectedIndex(INDEX_NONE);
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (!canHandleEvent()) return false;
if (velocityY > MAX_FLING_VELOCITY) {
velocityY = MAX_FLING_VELOCITY;
} else if (velocityY < -MAX_FLING_VELOCITY) {
velocityY = -MAX_FLING_VELOCITY;
}
setSelectedIndex(INDEX_NONE);
mScroller = new Scroller(getContext());
mScroller.fling(0, mScrollY, 0, -(int) velocityY, 0, 0, 0,
mMaxScrollY);
computeScroll();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
if (!canHandleEvent()) return;
performLongClick();
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if (!canHandleEvent()) return false;
setSelectedIndex(INDEX_NONE);
scrollBy(0, (int) distanceY);
invalidate();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (!canHandleEvent()) return false;
int index = computeSelectedIndex(e.getX(), e.getY());
if (index >= 0 && index < mCount) {
// Play click sound.
if (mAudioManager == null) {
mAudioManager = (AudioManager) getContext()
.getSystemService(Context.AUDIO_SERVICE);
}
mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
mListener.onImageTapped(index);
return true;
}
return false;
}
}
public int getCurrentSelection() {
return mCurrentSelection;
}
public void invalidateImage(int index) {
if (index != INDEX_NONE) {
mImageBlockManager.invalidateImage(index);
}
}
/**
*
* @param index <code>INDEX_NONE</code> (-1) means remove selection.
*/
public void setSelectedIndex(int index) {
// A selection box will be shown for the image that being selected,
// (by finger or by the dpad center key). The selection box can be drawn
// in two colors. One color (yellow) is used when the the image is
// still being tapped or clicked (the finger is still on the touch
// screen or the dpad center key is not released). Another color
// (orange) is used after the finger leaves touch screen or the dpad
// center key is released.
if (mCurrentSelection == index) {
return;
}
// This happens when the last picture is deleted.
mCurrentSelection = Math.min(index, mCount - 1);
if (mCurrentSelection != INDEX_NONE) {
ensureVisible(mCurrentSelection);
}
invalidate();
}
public void scrollToImage(int index) {
Rect r = getRectForPosition(index);
scrollTo(0, r.top);
}
public void scrollToVisible(int index) {
Rect r = getRectForPosition(index);
int top = getScrollY();
int bottom = getScrollY() + getHeight();
if (r.bottom > bottom) {
scrollTo(0, r.bottom - getHeight());
} else if (r.top < top) {
scrollTo(0, r.top);
}
}
private void ensureVisible(int pos) {
Rect r = getRectForPosition(pos);
int top = getScrollY();
int bot = top + getHeight();
if (r.bottom > bot) {
mScroller = new Scroller(getContext());
mScroller.startScroll(mScrollX, mScrollY, 0,
r.bottom - getHeight() - mScrollY, 200);
computeScroll();
} else if (r.top < top) {
mScroller = new Scroller(getContext());
mScroller.startScroll(mScrollX, mScrollY, 0, r.top - mScrollY, 200);
computeScroll();
}
}
public void start() {
// These must be set before start().
Assert(mLoader != null);
Assert(mListener != null);
Assert(mDrawAdapter != null);
mRunning = true;
requestLayout();
}
// If the the underlying data is changed, for example,
// an image is deleted, or the size choice is changed,
// The following sequence is needed:
//
// mGvs.stop();
// mGvs.set...(...);
// mGvs.set...(...);
// mGvs.start();
public void stop() {
// Remove the long press callback from the queue if we are going to
// stop.
mHandler.removeCallbacks(mLongPressCallback);
mScroller = null;
if (mImageBlockManager != null) {
mImageBlockManager.recycle();
mImageBlockManager = null;
}
mRunning = false;
mCurrentSelection = INDEX_NONE;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!canHandleEvent()) return;
mImageBlockManager.doDraw(canvas, getWidth(), getHeight(), mScrollY);
paintDecoration(canvas);
paintSelection(canvas);
moveDataWindow();
}
@Override
public void computeScroll() {
if (mScroller != null) {
boolean more = mScroller.computeScrollOffset();
scrollTo(0, mScroller.getCurrY());
if (more) {
invalidate(); // So we draw again
} else {
mScroller = null;
}
} else {
super.computeScroll();
}
}
// Return the rectange for the thumbnail in the given position.
Rect getRectForPosition(int pos) {
int row = pos / mColumns;
int col = pos - (row * mColumns);
int left = mSpec.mLeftEdgePadding
+ (col * (mSpec.mCellWidth + mSpec.mCellSpacing));
int top = row * mBlockHeight;
return new Rect(left, top,
left + mSpec.mCellWidth + mSpec.mCellSpacing,
top + mSpec.mCellHeight + mSpec.mCellSpacing);
}
// Inverse of getRectForPosition: from screen coordinate to image position.
int computeSelectedIndex(float xFloat, float yFloat) {
int x = (int) xFloat;
int y = (int) yFloat;
int spacing = mSpec.mCellSpacing;
int leftSpacing = mSpec.mLeftEdgePadding;
int row = (mScrollY + y - spacing) / (mSpec.mCellHeight + spacing);
int col = Math.min(mColumns - 1,
(x - leftSpacing) / (mSpec.mCellWidth + spacing));
return (row * mColumns) + col;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!canHandleEvent()) {
return false;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurrentPressState |= TAPPING_FLAG;
invalidate();
break;
case MotionEvent.ACTION_UP:
mCurrentPressState &= ~TAPPING_FLAG;
invalidate();
break;
}
mGestureDetector.onTouchEvent(ev);
// Consume all events
return true;
}
@Override
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
public void scrollTo(float scrollPosition) {
scrollTo(0, Math.round(scrollPosition * mMaxScrollY));
}
@Override
public void scrollTo(int x, int y) {
y = Math.max(0, Math.min(mMaxScrollY, y));
if (mSpec != null) {
mListener.onScroll((float) mScrollY / mMaxScrollY);
}
super.scrollTo(x, y);
}
private boolean canHandleEvent() {
return mRunning && mLayoutComplete;
}
private final Runnable mLongPressCallback = new Runnable() {
public void run() {
mCurrentPressState &= ~CLICKING_FLAG;
showContextMenu();
}
};
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (!canHandleEvent()) return false;
int sel = mCurrentSelection;
if (sel != INDEX_NONE) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (sel != mCount - 1 && (sel % mColumns < mColumns - 1)) {
sel += 1;
}
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (sel > 0 && (sel % mColumns != 0)) {
sel -= 1;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (sel >= mColumns) {
sel -= mColumns;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
sel = Math.min(mCount - 1, sel + mColumns);
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
if (event.getRepeatCount() == 0) {
mCurrentPressState |= CLICKING_FLAG;
mHandler.postDelayed(mLongPressCallback,
ViewConfiguration.getLongPressTimeout());
}
break;
default:
return super.onKeyDown(keyCode, event);
}
} else {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
int startRow =
(mScrollY - mSpec.mCellSpacing) / mBlockHeight;
int topPos = startRow * mColumns;
Rect r = getRectForPosition(topPos);
if (r.top < getScrollY()) {
topPos += mColumns;
}
topPos = Math.min(mCount - 1, topPos);
sel = topPos;
break;
default:
return super.onKeyDown(keyCode, event);
}
}
setSelectedIndex(sel);
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!canHandleEvent()) return false;
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
mCurrentPressState &= ~CLICKING_FLAG;
invalidate();
// The keyUp doesn't get called when the longpress menu comes up. We
// only get here when the user lets go of the center key before the
// longpress menu comes up.
mHandler.removeCallbacks(mLongPressCallback);
// open the photo
mListener.onImageClicked(mCurrentSelection);
return true;
}
return super.onKeyUp(keyCode, event);
}
private void paintDecoration(Canvas canvas) {
if (!mDrawAdapter.needsDecoration()) return;
// Calculate visible region according to scroll position.
int startRow = (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
int endRow = (mScrollY + getHeight() - mSpec.mCellSpacing - 1)
/ mBlockHeight + 1;
// Limit startRow and endRow to the valid range.
// Make sure we handle the mRows == 0 case right.
startRow = Math.max(Math.min(startRow, mRows - 1), 0);
endRow = Math.max(Math.min(endRow, mRows), 0);
int startIndex = startRow * mColumns;
int endIndex = Math.min(endRow * mColumns, mCount);
int xPos = mSpec.mLeftEdgePadding;
int yPos = mSpec.mCellSpacing + startRow * mBlockHeight;
int off = 0;
for (int i = startIndex; i < endIndex; i++) {
IImage image = mAllImages.getImageAt(i);
mDrawAdapter.drawDecoration(canvas, image, xPos, yPos,
mSpec.mCellWidth, mSpec.mCellHeight);
// Calculate next position
off += 1;
if (off == mColumns) {
xPos = mSpec.mLeftEdgePadding;
yPos += mBlockHeight;
off = 0;
} else {
xPos += mSpec.mCellWidth + mSpec.mCellSpacing;
}
}
}
private void paintSelection(Canvas canvas) {
if (mCurrentSelection == INDEX_NONE) return;
int row = mCurrentSelection / mColumns;
int col = mCurrentSelection - (row * mColumns);
int spacing = mSpec.mCellSpacing;
int leftSpacing = mSpec.mLeftEdgePadding;
int xPos = leftSpacing + (col * (mSpec.mCellWidth + spacing));
int yTop = spacing + (row * mBlockHeight);
int type = OUTLINE_SELECTED;
if (mCurrentPressState != 0) {
type = OUTLINE_PRESSED;
}
canvas.drawBitmap(mOutline[type], xPos, yTop, null);
}
}
class ImageBlockManager {
@SuppressWarnings("unused")
private static final String TAG = "ImageBlockManager";
// Number of rows we want to cache.
// Assume there are 6 rows per page, this caches 5 pages.
private static final int CACHE_ROWS = 30;
// mCache maps from row number to the ImageBlock.
private final HashMap<Integer, ImageBlock> mCache;
// These are parameters set in the constructor.
private final Handler mHandler;
private final Runnable mRedrawCallback; // Called after a row is loaded,
// so GridViewSpecial can draw
// again using the new images.
private final IImageList mImageList;
private final ImageLoader mLoader;
private final GridViewSpecial.DrawAdapter mDrawAdapter;
private final GridViewSpecial.LayoutSpec mSpec;
private final int mColumns; // Columns per row.
private final int mBlockWidth; // The width of an ImageBlock.
private final Bitmap mOutline; // The outline bitmap put on top of each
// image.
private final int mCount; // Cache mImageList.getCount().
private final int mRows; // Cache (mCount + mColumns - 1) / mColumns
private final int mBlockHeight; // The height of an ImageBlock.
// Visible row range: [mStartRow, mEndRow). Set by setVisibleRows().
private int mStartRow = 0;
private int mEndRow = 0;
ImageBlockManager(Handler handler, Runnable redrawCallback,
IImageList imageList, ImageLoader loader,
GridViewSpecial.DrawAdapter adapter,
GridViewSpecial.LayoutSpec spec,
int columns, int blockWidth, Bitmap outline) {
mHandler = handler;
mRedrawCallback = redrawCallback;
mImageList = imageList;
mLoader = loader;
mDrawAdapter = adapter;
mSpec = spec;
mColumns = columns;
mBlockWidth = blockWidth;
mOutline = outline;
mBlockHeight = mSpec.mCellSpacing + mSpec.mCellHeight;
mCount = imageList.getCount();
mRows = (mCount + mColumns - 1) / mColumns;
mCache = new HashMap<Integer, ImageBlock>();
mPendingRequest = 0;
initGraphics();
}
// Set the window of visible rows. Once set we will start to load them as
// soon as possible (if they are not already in cache).
public void setVisibleRows(int startRow, int endRow) {
if (startRow != mStartRow || endRow != mEndRow) {
mStartRow = startRow;
mEndRow = endRow;
startLoading();
}
}
int mPendingRequest; // Number of pending requests (sent to ImageLoader).
// We want to keep enough requests in ImageLoader's queue, but not too
// many.
static final int REQUESTS_LOW = 3;
static final int REQUESTS_HIGH = 6;
// After clear requests currently in queue, start loading the thumbnails.
// We need to clear the queue first because the proper order of loading
// may have changed (because the visible region changed, or some images
// have been invalidated).
private void startLoading() {
clearLoaderQueue();
continueLoading();
}
private void clearLoaderQueue() {
int[] tags = mLoader.clearQueue();
for (int pos : tags) {
int row = pos / mColumns;
int col = pos - row * mColumns;
ImageBlock blk = mCache.get(row);
Assert(blk != null); // We won't reuse the block if it has pending
// requests. See getEmptyBlock().
blk.cancelRequest(col);
}
}
// Scan the cache and send requests to ImageLoader if needed.
private void continueLoading() {
// Check if we still have enough requests in the queue.
if (mPendingRequest >= REQUESTS_LOW) return;
// Scan the visible rows.
for (int i = mStartRow; i < mEndRow; i++) {
if (scanOne(i)) return;
}
int range = (CACHE_ROWS - (mEndRow - mStartRow)) / 2;
// Scan other rows.
// d is the distance between the row and visible region.
for (int d = 1; d <= range; d++) {
int after = mEndRow - 1 + d;
int before = mStartRow - d;
if (after >= mRows && before < 0) {
break; // Nothing more the scan.
}
if (after < mRows && scanOne(after)) return;
if (before >= 0 && scanOne(before)) return;
}
}
// Returns true if we can stop scanning.
private boolean scanOne(int i) {
mPendingRequest += tryToLoad(i);
return mPendingRequest >= REQUESTS_HIGH;
}
// Returns number of requests we issued for this row.
private int tryToLoad(int row) {
Assert(row >= 0 && row < mRows);
ImageBlock blk = mCache.get(row);
if (blk == null) {
// Find an empty block
blk = getEmptyBlock();
blk.setRow(row);
blk.invalidate();
mCache.put(row, blk);
}
return blk.loadImages();
}
// Get an empty block for the cache.
private ImageBlock getEmptyBlock() {
// See if we can allocate a new block.
if (mCache.size() < CACHE_ROWS) {
return new ImageBlock();
}
// Reclaim the old block with largest distance from the visible region.
int bestDistance = -1;
int bestIndex = -1;
for (int index : mCache.keySet()) {
// Make sure we don't reclaim a block which still has pending
// request.
if (mCache.get(index).hasPendingRequests()) {
continue;
}
int dist = 0;
if (index >= mEndRow) {
dist = index - mEndRow + 1;
} else if (index < mStartRow) {
dist = mStartRow - index;
} else {
// Inside the visible region.
continue;
}
if (dist > bestDistance) {
bestDistance = dist;
bestIndex = index;
}
}
return mCache.remove(bestIndex);
}
public void invalidateImage(int index) {
int row = index / mColumns;
int col = index - (row * mColumns);
ImageBlock blk = mCache.get(row);
if (blk == null) return;
if ((blk.mCompletedMask & (1 << col)) != 0) {
blk.mCompletedMask &= ~(1 << col);
}
startLoading();
}
// After calling recycle(), the instance should not be used anymore.
public void recycle() {
for (ImageBlock blk : mCache.values()) {
blk.recycle();
}
mCache.clear();
mEmptyBitmap.recycle();
}
// Draw the images to the given canvas.
public void doDraw(Canvas canvas, int thisWidth, int thisHeight,
int scrollPos) {
final int height = mBlockHeight;
// Note that currentBlock could be negative.
int currentBlock = (scrollPos < 0)
? ((scrollPos - height + 1) / height)
: (scrollPos / height);
while (true) {
final int yPos = currentBlock * height;
if (yPos >= scrollPos + thisHeight) {
break;
}
ImageBlock blk = mCache.get(currentBlock);
if (blk != null) {
blk.doDraw(canvas, 0, yPos);
} else {
drawEmptyBlock(canvas, 0, yPos, currentBlock);
}
currentBlock += 1;
}
}
// Return number of columns in the given row. (This could be less than
// mColumns for the last row).
private int numColumns(int row) {
return Math.min(mColumns, mCount - row * mColumns);
}
// Draw a block which has not been loaded.
private void drawEmptyBlock(Canvas canvas, int xPos, int yPos, int row) {
// Draw the background.
canvas.drawRect(xPos, yPos, xPos + mBlockWidth, yPos + mBlockHeight,
mBackgroundPaint);
// Draw the empty images.
int x = xPos + mSpec.mLeftEdgePadding;
int y = yPos + mSpec.mCellSpacing;
int cols = numColumns(row);
for (int i = 0; i < cols; i++) {
canvas.drawBitmap(mEmptyBitmap, x, y, null);
x += (mSpec.mCellWidth + mSpec.mCellSpacing);
}
}
// mEmptyBitmap is what we draw if we the wanted block hasn't been loaded.
// (If the user scrolls too fast). It is a gray image with normal outline.
// mBackgroundPaint is used to draw the (black) background outside
// mEmptyBitmap.
Paint mBackgroundPaint;
private Bitmap mEmptyBitmap;
private void initGraphics() {
mBackgroundPaint = new Paint();
mBackgroundPaint.setStyle(Paint.Style.FILL);
mBackgroundPaint.setColor(0xFF000000); // black
mEmptyBitmap = Bitmap.createBitmap(mSpec.mCellWidth, mSpec.mCellHeight,
Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(mEmptyBitmap);
canvas.drawRGB(0xDD, 0xDD, 0xDD);
canvas.drawBitmap(mOutline, 0, 0, null);
}
// ImageBlock stores bitmap for one row. The loaded thumbnail images are
// drawn to mBitmap. mBitmap is later used in onDraw() of GridViewSpecial.
private class ImageBlock {
private Bitmap mBitmap;
private final Canvas mCanvas;
// Columns which have been requested to the loader
private int mRequestedMask;
// Columns which have been completed from the loader
private int mCompletedMask;
// The row number this block represents.
private int mRow;
public ImageBlock() {
mBitmap = Bitmap.createBitmap(mBlockWidth, mBlockHeight,
Bitmap.Config.RGB_565);
mCanvas = new Canvas(mBitmap);
mRow = -1;
}
public void setRow(int row) {
mRow = row;
}
public void invalidate() {
// We do not change mRequestedMask or do cancelAllRequests()
// because the data coming from pending requests are valid. (We only
// invalidate data which has been drawn to the bitmap).
mCompletedMask = 0;
}
// After recycle, the ImageBlock instance should not be accessed.
public void recycle() {
cancelAllRequests();
mBitmap.recycle();
mBitmap = null;
}
private boolean isVisible() {
return mRow >= mStartRow && mRow < mEndRow;
}
// Returns number of requests submitted to ImageLoader.
public int loadImages() {
Assert(mRow != -1);
int columns = numColumns(mRow);
// Calculate what we need.
int needMask = ((1 << columns) - 1)
& ~(mCompletedMask | mRequestedMask);
if (needMask == 0) {
return 0;
}
int retVal = 0;
int base = mRow * mColumns;
for (int col = 0; col < columns; col++) {
if ((needMask & (1 << col)) == 0) {
continue;
}
int pos = base + col;
final IImage image = mImageList.getImageAt(pos);
if (image != null) {
// This callback is passed to ImageLoader. It will invoke
// loadImageDone() in the main thread. We limit the callback
// thread to be in this very short function. All other
// processing is done in the main thread.
final int colFinal = col;
ImageLoader.LoadedCallback cb =
new ImageLoader.LoadedCallback() {
public void run(final Bitmap b) {
mHandler.post(new Runnable() {
public void run() {
loadImageDone(image, b,
colFinal);
}
});
}
};
// Load Image
mLoader.getBitmap(image, cb, pos);
mRequestedMask |= (1 << col);
retVal += 1;
}
}
return retVal;
}
// Whether this block has pending requests.
public boolean hasPendingRequests() {
return mRequestedMask != 0;
}
// Called when an image is loaded.
private void loadImageDone(IImage image, Bitmap b,
int col) {
if (mBitmap == null) return; // This block has been recycled.
int spacing = mSpec.mCellSpacing;
int leftSpacing = mSpec.mLeftEdgePadding;
final int yPos = spacing;
final int xPos = leftSpacing
+ (col * (mSpec.mCellWidth + spacing));
drawBitmap(image, b, xPos, yPos);
if (b != null) {
b.recycle();
}
int mask = (1 << col);
Assert((mCompletedMask & mask) == 0);
Assert((mRequestedMask & mask) != 0);
mRequestedMask &= ~mask;
mCompletedMask |= mask;
mPendingRequest--;
if (isVisible()) {
mRedrawCallback.run();
}
// Kick start next block loading.
continueLoading();
}
// Draw the loaded bitmap to the block bitmap.
private void drawBitmap(
IImage image, Bitmap b, int xPos, int yPos) {
mDrawAdapter.drawImage(mCanvas, image, b, xPos, yPos,
mSpec.mCellWidth, mSpec.mCellHeight);
mCanvas.drawBitmap(mOutline, xPos, yPos, null);
}
// Draw the block bitmap to the specified canvas.
public void doDraw(Canvas canvas, int xPos, int yPos) {
int cols = numColumns(mRow);
if (cols == mColumns) {
canvas.drawBitmap(mBitmap, xPos, yPos, null);
} else {
// This must be the last row -- we draw only part of the block.
// Draw the background.
canvas.drawRect(xPos, yPos, xPos + mBlockWidth,
yPos + mBlockHeight, mBackgroundPaint);
// Draw part of the block.
int w = mSpec.mLeftEdgePadding
+ cols * (mSpec.mCellWidth + mSpec.mCellSpacing);
Rect srcRect = new Rect(0, 0, w, mBlockHeight);
Rect dstRect = new Rect(srcRect);
dstRect.offset(xPos, yPos);
canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
}
// Draw the part which has not been loaded.
int isEmpty = ((1 << cols) - 1) & ~mCompletedMask;
if (isEmpty != 0) {
int x = xPos + mSpec.mLeftEdgePadding;
int y = yPos + mSpec.mCellSpacing;
for (int i = 0; i < cols; i++) {
if ((isEmpty & (1 << i)) != 0) {
canvas.drawBitmap(mEmptyBitmap, x, y, null);
}
x += (mSpec.mCellWidth + mSpec.mCellSpacing);
}
}
}
// Mark a request as cancelled. The request has already been removed
// from the queue of ImageLoader, so we only need to mark the fact.
public void cancelRequest(int col) {
int mask = (1 << col);
Assert((mRequestedMask & mask) != 0);
mRequestedMask &= ~mask;
mPendingRequest--;
}
// Try to cancel all pending requests for this block. After this
// completes there could still be requests not cancelled (because it is
// already in progress). We deal with that situation by setting mBitmap
// to null in recycle() and check this in loadImageDone().
private void cancelAllRequests() {
for (int i = 0; i < mColumns; i++) {
int mask = (1 << i);
if ((mRequestedMask & mask) != 0) {
int pos = (mRow * mColumns) + i;
if (mLoader.cancel(mImageList.getImageAt(pos))) {
mRequestedMask &= ~mask;
mPendingRequest--;
}
}
}
}
}
}