/* * 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--; } } } } } }