Home | History | Annotate | Download | only in camera
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera;
     18 
     19 import com.android.gallery.R;
     20 
     21 import static com.android.camera.Util.Assert;
     22 
     23 import android.app.Activity;
     24 import android.content.Context;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Canvas;
     27 import android.graphics.Paint;
     28 import android.graphics.Rect;
     29 import android.graphics.drawable.Drawable;
     30 import android.media.AudioManager;
     31 import android.os.Handler;
     32 import android.util.AttributeSet;
     33 import android.util.DisplayMetrics;
     34 import android.view.GestureDetector;
     35 import android.view.KeyEvent;
     36 import android.view.MotionEvent;
     37 import android.view.View;
     38 import android.view.ViewConfiguration;
     39 import android.view.GestureDetector.SimpleOnGestureListener;
     40 import android.widget.Scroller;
     41 
     42 import com.android.camera.gallery.IImage;
     43 import com.android.camera.gallery.IImageList;
     44 
     45 import java.util.HashMap;
     46 
     47 class GridViewSpecial extends View {
     48     @SuppressWarnings("unused")
     49     private static final String TAG = "GridViewSpecial";
     50     private static final float MAX_FLING_VELOCITY = 2500;
     51 
     52     public static interface Listener {
     53         public void onImageClicked(int index);
     54         public void onImageTapped(int index);
     55         public void onLayoutComplete(boolean changed);
     56 
     57         /**
     58          * Invoked when the <code>GridViewSpecial</code> scrolls.
     59          *
     60          * @param scrollPosition the position of the scroller in the range
     61          *         [0, 1], when 0 means on the top and 1 means on the buttom
     62          */
     63         public void onScroll(float scrollPosition);
     64     }
     65 
     66     public static interface DrawAdapter {
     67         public void drawImage(Canvas canvas, IImage image,
     68                 Bitmap b, int xPos, int yPos, int w, int h);
     69         public void drawDecoration(Canvas canvas, IImage image,
     70                 int xPos, int yPos, int w, int h);
     71         public boolean needsDecoration();
     72     }
     73 
     74     public static final int INDEX_NONE = -1;
     75 
     76     // There are two cell size we will use. It can be set by setSizeChoice().
     77     // The mLeftEdgePadding fields is filled in onLayout(). See the comments
     78     // in onLayout() for details.
     79     static class LayoutSpec {
     80         LayoutSpec(int w, int h, int intercellSpacing, int leftEdgePadding,
     81                 DisplayMetrics metrics) {
     82             mCellWidth = dpToPx(w, metrics);
     83             mCellHeight = dpToPx(h, metrics);
     84             mCellSpacing = dpToPx(intercellSpacing, metrics);
     85             mLeftEdgePadding = dpToPx(leftEdgePadding, metrics);
     86         }
     87         int mCellWidth, mCellHeight;
     88         int mCellSpacing;
     89         int mLeftEdgePadding;
     90     }
     91 
     92     private LayoutSpec [] mCellSizeChoices;
     93 
     94     private void initCellSize() {
     95         Activity a = (Activity) getContext();
     96         DisplayMetrics metrics = new DisplayMetrics();
     97         a.getWindowManager().getDefaultDisplay().getMetrics(metrics);
     98         mCellSizeChoices = new LayoutSpec[] {
     99             new LayoutSpec(67, 67, 8, 0, metrics),
    100             new LayoutSpec(92, 92, 8, 0, metrics),
    101         };
    102     }
    103 
    104     // Converts dp to pixel.
    105     private static int dpToPx(int dp, DisplayMetrics metrics) {
    106         return (int) (metrics.density * dp);
    107     }
    108 
    109     // These are set in init().
    110     private final Handler mHandler = new Handler();
    111     private GestureDetector mGestureDetector;
    112     private ImageBlockManager mImageBlockManager;
    113 
    114     // These are set in set*() functions.
    115     private ImageLoader mLoader;
    116     private Listener mListener = null;
    117     private DrawAdapter mDrawAdapter = null;
    118     private IImageList mAllImages = ImageManager.makeEmptyImageList();
    119     private int mSizeChoice = 1;  // default is big cell size
    120 
    121     // These are set in onLayout().
    122     private LayoutSpec mSpec;
    123     private int mColumns;
    124     private int mMaxScrollY;
    125 
    126     // We can handle events only if onLayout() is completed.
    127     private boolean mLayoutComplete = false;
    128 
    129     // Selection state
    130     private int mCurrentSelection = INDEX_NONE;
    131     private int mCurrentPressState = 0;
    132     private static final int TAPPING_FLAG = 1;
    133     private static final int CLICKING_FLAG = 2;
    134 
    135     // These are cached derived information.
    136     private int mCount;  // Cache mImageList.getCount();
    137     private int mRows;  // Cache (mCount + mColumns - 1) / mColumns
    138     private int mBlockHeight; // Cache mSpec.mCellSpacing + mSpec.mCellHeight
    139 
    140     private boolean mRunning = false;
    141     private Scroller mScroller = null;
    142 
    143     public GridViewSpecial(Context context, AttributeSet attrs) {
    144         super(context, attrs);
    145         init(context);
    146     }
    147 
    148     private void init(Context context) {
    149         setVerticalScrollBarEnabled(true);
    150         initializeScrollbars(context.obtainStyledAttributes(
    151                 android.R.styleable.View));
    152         mGestureDetector = new GestureDetector(context,
    153                 new MyGestureDetector());
    154         setFocusableInTouchMode(true);
    155         initCellSize();
    156     }
    157 
    158     private final Runnable mRedrawCallback = new Runnable() {
    159                 public void run() {
    160                     invalidate();
    161                 }
    162             };
    163 
    164     public void setLoader(ImageLoader loader) {
    165         Assert(mRunning == false);
    166         mLoader = loader;
    167     }
    168 
    169     public void setListener(Listener listener) {
    170         Assert(mRunning == false);
    171         mListener = listener;
    172     }
    173 
    174     public void setDrawAdapter(DrawAdapter adapter) {
    175         Assert(mRunning == false);
    176         mDrawAdapter = adapter;
    177     }
    178 
    179     public void setImageList(IImageList list) {
    180         Assert(mRunning == false);
    181         mAllImages = list;
    182         mCount = mAllImages.getCount();
    183     }
    184 
    185     public void setSizeChoice(int choice) {
    186         Assert(mRunning == false);
    187         if (mSizeChoice == choice) return;
    188         mSizeChoice = choice;
    189     }
    190 
    191     @Override
    192     public void onLayout(boolean changed, int left, int top,
    193                          int right, int bottom) {
    194         super.onLayout(changed, left, top, right, bottom);
    195 
    196         if (!mRunning) {
    197             return;
    198         }
    199 
    200         mSpec = mCellSizeChoices[mSizeChoice];
    201 
    202         int width = right - left;
    203 
    204         // The width is divided into following parts:
    205         //
    206         // LeftEdgePadding CellWidth (CellSpacing CellWidth)* RightEdgePadding
    207         //
    208         // We determine number of cells (columns) first, then the left and right
    209         // padding are derived. We make left and right paddings the same size.
    210         //
    211         // The height is divided into following parts:
    212         //
    213         // CellSpacing (CellHeight CellSpacing)+
    214 
    215         mColumns = 1 + (width - mSpec.mCellWidth)
    216                 / (mSpec.mCellWidth + mSpec.mCellSpacing);
    217 
    218         mSpec.mLeftEdgePadding = (width
    219                 - ((mColumns - 1) * mSpec.mCellSpacing)
    220                 - (mColumns * mSpec.mCellWidth)) / 2;
    221 
    222         mRows = (mCount + mColumns - 1) / mColumns;
    223         mBlockHeight = mSpec.mCellSpacing + mSpec.mCellHeight;
    224         mMaxScrollY = mSpec.mCellSpacing + (mRows * mBlockHeight)
    225                 - (bottom - top);
    226 
    227         // Put mScrollY in the valid range. This matters if mMaxScrollY is
    228         // changed. For example, orientation changed from portrait to landscape.
    229         mScrollY = Math.max(0, Math.min(mMaxScrollY, mScrollY));
    230 
    231         generateOutlineBitmap();
    232 
    233         if (mImageBlockManager != null) {
    234             mImageBlockManager.recycle();
    235         }
    236 
    237         mImageBlockManager = new ImageBlockManager(mHandler, mRedrawCallback,
    238                 mAllImages, mLoader, mDrawAdapter, mSpec, mColumns, width,
    239                 mOutline[OUTLINE_EMPTY]);
    240 
    241         mListener.onLayoutComplete(changed);
    242 
    243         moveDataWindow();
    244 
    245         mLayoutComplete = true;
    246     }
    247 
    248     @Override
    249     protected int computeVerticalScrollRange() {
    250         return mMaxScrollY + getHeight();
    251     }
    252 
    253     // We cache the three outlines from NinePatch to Bitmap to speed up
    254     // drawing. The cache must be updated if the cell size is changed.
    255     public static final int OUTLINE_EMPTY = 0;
    256     public static final int OUTLINE_PRESSED = 1;
    257     public static final int OUTLINE_SELECTED = 2;
    258 
    259     public Bitmap mOutline[] = new Bitmap[3];
    260 
    261     private void generateOutlineBitmap() {
    262         int w = mSpec.mCellWidth;
    263         int h = mSpec.mCellHeight;
    264 
    265         for (int i = 0; i < mOutline.length; i++) {
    266             mOutline[i] = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    267         }
    268 
    269         Drawable cellOutline;
    270         cellOutline = GridViewSpecial.this.getResources()
    271                 .getDrawable(android.R.drawable.gallery_thumb);
    272         cellOutline.setBounds(0, 0, w, h);
    273         Canvas canvas = new Canvas();
    274 
    275         canvas.setBitmap(mOutline[OUTLINE_EMPTY]);
    276         cellOutline.setState(EMPTY_STATE_SET);
    277         cellOutline.draw(canvas);
    278 
    279         canvas.setBitmap(mOutline[OUTLINE_PRESSED]);
    280         cellOutline.setState(
    281                 PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
    282         cellOutline.draw(canvas);
    283 
    284         canvas.setBitmap(mOutline[OUTLINE_SELECTED]);
    285         cellOutline.setState(ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
    286         cellOutline.draw(canvas);
    287     }
    288 
    289     private void moveDataWindow() {
    290         // Calculate visible region according to scroll position.
    291         int startRow = (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
    292         int endRow = (mScrollY + getHeight() - mSpec.mCellSpacing - 1)
    293                 / mBlockHeight + 1;
    294 
    295         // Limit startRow and endRow to the valid range.
    296         // Make sure we handle the mRows == 0 case right.
    297         startRow = Math.max(Math.min(startRow, mRows - 1), 0);
    298         endRow = Math.max(Math.min(endRow, mRows), 0);
    299         mImageBlockManager.setVisibleRows(startRow, endRow);
    300     }
    301 
    302     // In MyGestureDetector we have to check canHandleEvent() because
    303     // GestureDetector could queue events and fire them later. At that time
    304     // stop() may have already been called and we can't handle the events.
    305     private class MyGestureDetector extends SimpleOnGestureListener {
    306         private AudioManager mAudioManager;
    307 
    308         @Override
    309         public boolean onDown(MotionEvent e) {
    310             if (!canHandleEvent()) return false;
    311             if (mScroller != null && !mScroller.isFinished()) {
    312                 mScroller.forceFinished(true);
    313                 return false;
    314             }
    315             int index = computeSelectedIndex(e.getX(), e.getY());
    316             if (index >= 0 && index < mCount) {
    317                 setSelectedIndex(index);
    318             } else {
    319                 setSelectedIndex(INDEX_NONE);
    320             }
    321             return true;
    322         }
    323 
    324         @Override
    325         public boolean onFling(MotionEvent e1, MotionEvent e2,
    326                 float velocityX, float velocityY) {
    327             if (!canHandleEvent()) return false;
    328             if (velocityY > MAX_FLING_VELOCITY) {
    329                 velocityY = MAX_FLING_VELOCITY;
    330             } else if (velocityY < -MAX_FLING_VELOCITY) {
    331                 velocityY = -MAX_FLING_VELOCITY;
    332             }
    333 
    334             setSelectedIndex(INDEX_NONE);
    335             mScroller = new Scroller(getContext());
    336             mScroller.fling(0, mScrollY, 0, -(int) velocityY, 0, 0, 0,
    337                     mMaxScrollY);
    338             computeScroll();
    339 
    340             return true;
    341         }
    342 
    343         @Override
    344         public void onLongPress(MotionEvent e) {
    345             if (!canHandleEvent()) return;
    346             performLongClick();
    347         }
    348 
    349         @Override
    350         public boolean onScroll(MotionEvent e1, MotionEvent e2,
    351                                 float distanceX, float distanceY) {
    352             if (!canHandleEvent()) return false;
    353             setSelectedIndex(INDEX_NONE);
    354             scrollBy(0, (int) distanceY);
    355             invalidate();
    356             return true;
    357         }
    358 
    359         @Override
    360         public boolean onSingleTapConfirmed(MotionEvent e) {
    361             if (!canHandleEvent()) return false;
    362             int index = computeSelectedIndex(e.getX(), e.getY());
    363             if (index >= 0 && index < mCount) {
    364                 // Play click sound.
    365                 if (mAudioManager == null) {
    366                     mAudioManager = (AudioManager) getContext()
    367                             .getSystemService(Context.AUDIO_SERVICE);
    368                 }
    369                 mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
    370 
    371                 mListener.onImageTapped(index);
    372                 return true;
    373             }
    374             return false;
    375         }
    376     }
    377 
    378     public int getCurrentSelection() {
    379         return mCurrentSelection;
    380     }
    381 
    382     public void invalidateImage(int index) {
    383         if (index != INDEX_NONE) {
    384             mImageBlockManager.invalidateImage(index);
    385         }
    386     }
    387 
    388     /**
    389      *
    390      * @param index <code>INDEX_NONE</code> (-1) means remove selection.
    391      */
    392     public void setSelectedIndex(int index) {
    393         // A selection box will be shown for the image that being selected,
    394         // (by finger or by the dpad center key). The selection box can be drawn
    395         // in two colors. One color (yellow) is used when the the image is
    396         // still being tapped or clicked (the finger is still on the touch
    397         // screen or the dpad center key is not released). Another color
    398         // (orange) is used after the finger leaves touch screen or the dpad
    399         // center key is released.
    400 
    401         if (mCurrentSelection == index) {
    402             return;
    403         }
    404         // This happens when the last picture is deleted.
    405         mCurrentSelection = Math.min(index, mCount - 1);
    406 
    407         if (mCurrentSelection != INDEX_NONE) {
    408             ensureVisible(mCurrentSelection);
    409         }
    410         invalidate();
    411     }
    412 
    413     public void scrollToImage(int index) {
    414         Rect r = getRectForPosition(index);
    415         scrollTo(0, r.top);
    416     }
    417 
    418     public void scrollToVisible(int index) {
    419         Rect r = getRectForPosition(index);
    420         int top = getScrollY();
    421         int bottom = getScrollY() + getHeight();
    422         if (r.bottom > bottom) {
    423             scrollTo(0, r.bottom - getHeight());
    424         } else if (r.top < top) {
    425             scrollTo(0, r.top);
    426         }
    427     }
    428 
    429     private void ensureVisible(int pos) {
    430         Rect r = getRectForPosition(pos);
    431         int top = getScrollY();
    432         int bot = top + getHeight();
    433 
    434         if (r.bottom > bot) {
    435             mScroller = new Scroller(getContext());
    436             mScroller.startScroll(mScrollX, mScrollY, 0,
    437                     r.bottom - getHeight() - mScrollY, 200);
    438             computeScroll();
    439         } else if (r.top < top) {
    440             mScroller = new Scroller(getContext());
    441             mScroller.startScroll(mScrollX, mScrollY, 0, r.top - mScrollY, 200);
    442             computeScroll();
    443         }
    444     }
    445 
    446     public void start() {
    447         // These must be set before start().
    448         Assert(mLoader != null);
    449         Assert(mListener != null);
    450         Assert(mDrawAdapter != null);
    451         mRunning = true;
    452         requestLayout();
    453     }
    454 
    455     // If the the underlying data is changed, for example,
    456     // an image is deleted, or the size choice is changed,
    457     // The following sequence is needed:
    458     //
    459     // mGvs.stop();
    460     // mGvs.set...(...);
    461     // mGvs.set...(...);
    462     // mGvs.start();
    463     public void stop() {
    464         // Remove the long press callback from the queue if we are going to
    465         // stop.
    466         mHandler.removeCallbacks(mLongPressCallback);
    467         mScroller = null;
    468         if (mImageBlockManager != null) {
    469             mImageBlockManager.recycle();
    470             mImageBlockManager = null;
    471         }
    472         mRunning = false;
    473         mCurrentSelection = INDEX_NONE;
    474     }
    475 
    476     @Override
    477     public void onDraw(Canvas canvas) {
    478         super.onDraw(canvas);
    479         if (!canHandleEvent()) return;
    480         mImageBlockManager.doDraw(canvas, getWidth(), getHeight(), mScrollY);
    481         paintDecoration(canvas);
    482         paintSelection(canvas);
    483         moveDataWindow();
    484     }
    485 
    486     @Override
    487     public void computeScroll() {
    488         if (mScroller != null) {
    489             boolean more = mScroller.computeScrollOffset();
    490             scrollTo(0, mScroller.getCurrY());
    491             if (more) {
    492                 invalidate();  // So we draw again
    493             } else {
    494                 mScroller = null;
    495             }
    496         } else {
    497             super.computeScroll();
    498         }
    499     }
    500 
    501     // Return the rectange for the thumbnail in the given position.
    502     Rect getRectForPosition(int pos) {
    503         int row = pos / mColumns;
    504         int col = pos - (row * mColumns);
    505 
    506         int left = mSpec.mLeftEdgePadding
    507                 + (col * (mSpec.mCellWidth + mSpec.mCellSpacing));
    508         int top = row * mBlockHeight;
    509 
    510         return new Rect(left, top,
    511                 left + mSpec.mCellWidth + mSpec.mCellSpacing,
    512                 top + mSpec.mCellHeight + mSpec.mCellSpacing);
    513     }
    514 
    515     // Inverse of getRectForPosition: from screen coordinate to image position.
    516     int computeSelectedIndex(float xFloat, float yFloat) {
    517         int x = (int) xFloat;
    518         int y = (int) yFloat;
    519 
    520         int spacing = mSpec.mCellSpacing;
    521         int leftSpacing = mSpec.mLeftEdgePadding;
    522 
    523         int row = (mScrollY + y - spacing) / (mSpec.mCellHeight + spacing);
    524         int col = Math.min(mColumns - 1,
    525                 (x - leftSpacing) / (mSpec.mCellWidth + spacing));
    526         return (row * mColumns) + col;
    527     }
    528 
    529     @Override
    530     public boolean onTouchEvent(MotionEvent ev) {
    531         if (!canHandleEvent()) {
    532             return false;
    533         }
    534         switch (ev.getAction()) {
    535             case MotionEvent.ACTION_DOWN:
    536                 mCurrentPressState |= TAPPING_FLAG;
    537                 invalidate();
    538                 break;
    539             case MotionEvent.ACTION_UP:
    540                 mCurrentPressState &= ~TAPPING_FLAG;
    541                 invalidate();
    542                 break;
    543         }
    544         mGestureDetector.onTouchEvent(ev);
    545         // Consume all events
    546         return true;
    547     }
    548 
    549     @Override
    550     public void scrollBy(int x, int y) {
    551         scrollTo(mScrollX + x, mScrollY + y);
    552     }
    553 
    554     public void scrollTo(float scrollPosition) {
    555         scrollTo(0, Math.round(scrollPosition * mMaxScrollY));
    556     }
    557 
    558     @Override
    559     public void scrollTo(int x, int y) {
    560         y = Math.max(0, Math.min(mMaxScrollY, y));
    561         if (mSpec != null) {
    562             mListener.onScroll((float) mScrollY / mMaxScrollY);
    563         }
    564         super.scrollTo(x, y);
    565     }
    566 
    567     private boolean canHandleEvent() {
    568         return mRunning && mLayoutComplete;
    569     }
    570 
    571     private final Runnable mLongPressCallback = new Runnable() {
    572         public void run() {
    573             mCurrentPressState &= ~CLICKING_FLAG;
    574             showContextMenu();
    575         }
    576     };
    577 
    578     @Override
    579     public boolean onKeyDown(int keyCode, KeyEvent event) {
    580         if (!canHandleEvent()) return false;
    581         int sel = mCurrentSelection;
    582         if (sel != INDEX_NONE) {
    583             switch (keyCode) {
    584                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    585                     if (sel != mCount - 1 && (sel % mColumns < mColumns - 1)) {
    586                         sel += 1;
    587                     }
    588                     break;
    589                 case KeyEvent.KEYCODE_DPAD_LEFT:
    590                     if (sel > 0 && (sel % mColumns != 0)) {
    591                         sel -= 1;
    592                     }
    593                     break;
    594                 case KeyEvent.KEYCODE_DPAD_UP:
    595                     if (sel >= mColumns) {
    596                         sel -= mColumns;
    597                     }
    598                     break;
    599                 case KeyEvent.KEYCODE_DPAD_DOWN:
    600                     sel = Math.min(mCount - 1, sel + mColumns);
    601                     break;
    602                 case KeyEvent.KEYCODE_DPAD_CENTER:
    603                     if (event.getRepeatCount() == 0) {
    604                         mCurrentPressState |= CLICKING_FLAG;
    605                         mHandler.postDelayed(mLongPressCallback,
    606                                 ViewConfiguration.getLongPressTimeout());
    607                     }
    608                     break;
    609                 default:
    610                     return super.onKeyDown(keyCode, event);
    611             }
    612         } else {
    613             switch (keyCode) {
    614                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    615                 case KeyEvent.KEYCODE_DPAD_LEFT:
    616                 case KeyEvent.KEYCODE_DPAD_UP:
    617                 case KeyEvent.KEYCODE_DPAD_DOWN:
    618                         int startRow =
    619                                 (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
    620                         int topPos = startRow * mColumns;
    621                         Rect r = getRectForPosition(topPos);
    622                         if (r.top < getScrollY()) {
    623                             topPos += mColumns;
    624                         }
    625                         topPos = Math.min(mCount - 1, topPos);
    626                         sel = topPos;
    627                     break;
    628                 default:
    629                     return super.onKeyDown(keyCode, event);
    630             }
    631         }
    632         setSelectedIndex(sel);
    633         return true;
    634     }
    635 
    636     @Override
    637     public boolean onKeyUp(int keyCode, KeyEvent event) {
    638         if (!canHandleEvent()) return false;
    639 
    640         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
    641             mCurrentPressState &= ~CLICKING_FLAG;
    642             invalidate();
    643 
    644             // The keyUp doesn't get called when the longpress menu comes up. We
    645             // only get here when the user lets go of the center key before the
    646             // longpress menu comes up.
    647             mHandler.removeCallbacks(mLongPressCallback);
    648 
    649             // open the photo
    650             mListener.onImageClicked(mCurrentSelection);
    651             return true;
    652         }
    653         return super.onKeyUp(keyCode, event);
    654     }
    655 
    656     private void paintDecoration(Canvas canvas) {
    657         if (!mDrawAdapter.needsDecoration()) return;
    658 
    659         // Calculate visible region according to scroll position.
    660         int startRow = (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
    661         int endRow = (mScrollY + getHeight() - mSpec.mCellSpacing - 1)
    662                 / mBlockHeight + 1;
    663 
    664         // Limit startRow and endRow to the valid range.
    665         // Make sure we handle the mRows == 0 case right.
    666         startRow = Math.max(Math.min(startRow, mRows - 1), 0);
    667         endRow = Math.max(Math.min(endRow, mRows), 0);
    668 
    669         int startIndex = startRow * mColumns;
    670         int endIndex = Math.min(endRow * mColumns, mCount);
    671 
    672         int xPos = mSpec.mLeftEdgePadding;
    673         int yPos = mSpec.mCellSpacing + startRow * mBlockHeight;
    674         int off = 0;
    675         for (int i = startIndex; i < endIndex; i++) {
    676             IImage image = mAllImages.getImageAt(i);
    677 
    678             mDrawAdapter.drawDecoration(canvas, image, xPos, yPos,
    679                     mSpec.mCellWidth, mSpec.mCellHeight);
    680 
    681             // Calculate next position
    682             off += 1;
    683             if (off == mColumns) {
    684                 xPos = mSpec.mLeftEdgePadding;
    685                 yPos += mBlockHeight;
    686                 off = 0;
    687             } else {
    688                 xPos += mSpec.mCellWidth + mSpec.mCellSpacing;
    689             }
    690         }
    691     }
    692 
    693     private void paintSelection(Canvas canvas) {
    694         if (mCurrentSelection == INDEX_NONE) return;
    695 
    696         int row = mCurrentSelection / mColumns;
    697         int col = mCurrentSelection - (row * mColumns);
    698 
    699         int spacing = mSpec.mCellSpacing;
    700         int leftSpacing = mSpec.mLeftEdgePadding;
    701         int xPos = leftSpacing + (col * (mSpec.mCellWidth + spacing));
    702         int yTop = spacing + (row * mBlockHeight);
    703 
    704         int type = OUTLINE_SELECTED;
    705         if (mCurrentPressState != 0) {
    706             type = OUTLINE_PRESSED;
    707         }
    708         canvas.drawBitmap(mOutline[type], xPos, yTop, null);
    709     }
    710 }
    711 
    712 class ImageBlockManager {
    713     @SuppressWarnings("unused")
    714     private static final String TAG = "ImageBlockManager";
    715 
    716     // Number of rows we want to cache.
    717     // Assume there are 6 rows per page, this caches 5 pages.
    718     private static final int CACHE_ROWS = 30;
    719 
    720     // mCache maps from row number to the ImageBlock.
    721     private final HashMap<Integer, ImageBlock> mCache;
    722 
    723     // These are parameters set in the constructor.
    724     private final Handler mHandler;
    725     private final Runnable mRedrawCallback;  // Called after a row is loaded,
    726                                              // so GridViewSpecial can draw
    727                                              // again using the new images.
    728     private final IImageList mImageList;
    729     private final ImageLoader mLoader;
    730     private final GridViewSpecial.DrawAdapter mDrawAdapter;
    731     private final GridViewSpecial.LayoutSpec mSpec;
    732     private final int mColumns;  // Columns per row.
    733     private final int mBlockWidth;  // The width of an ImageBlock.
    734     private final Bitmap mOutline;  // The outline bitmap put on top of each
    735                                     // image.
    736     private final int mCount;  // Cache mImageList.getCount().
    737     private final int mRows;  // Cache (mCount + mColumns - 1) / mColumns
    738     private final int mBlockHeight;  // The height of an ImageBlock.
    739 
    740     // Visible row range: [mStartRow, mEndRow). Set by setVisibleRows().
    741     private int mStartRow = 0;
    742     private int mEndRow = 0;
    743 
    744     ImageBlockManager(Handler handler, Runnable redrawCallback,
    745             IImageList imageList, ImageLoader loader,
    746             GridViewSpecial.DrawAdapter adapter,
    747             GridViewSpecial.LayoutSpec spec,
    748             int columns, int blockWidth, Bitmap outline) {
    749         mHandler = handler;
    750         mRedrawCallback = redrawCallback;
    751         mImageList = imageList;
    752         mLoader = loader;
    753         mDrawAdapter = adapter;
    754         mSpec = spec;
    755         mColumns = columns;
    756         mBlockWidth = blockWidth;
    757         mOutline = outline;
    758         mBlockHeight = mSpec.mCellSpacing + mSpec.mCellHeight;
    759         mCount = imageList.getCount();
    760         mRows = (mCount + mColumns - 1) / mColumns;
    761         mCache = new HashMap<Integer, ImageBlock>();
    762         mPendingRequest = 0;
    763         initGraphics();
    764     }
    765 
    766     // Set the window of visible rows. Once set we will start to load them as
    767     // soon as possible (if they are not already in cache).
    768     public void setVisibleRows(int startRow, int endRow) {
    769         if (startRow != mStartRow || endRow != mEndRow) {
    770             mStartRow = startRow;
    771             mEndRow = endRow;
    772             startLoading();
    773         }
    774     }
    775 
    776     int mPendingRequest;  // Number of pending requests (sent to ImageLoader).
    777     // We want to keep enough requests in ImageLoader's queue, but not too
    778     // many.
    779     static final int REQUESTS_LOW = 3;
    780     static final int REQUESTS_HIGH = 6;
    781 
    782     // After clear requests currently in queue, start loading the thumbnails.
    783     // We need to clear the queue first because the proper order of loading
    784     // may have changed (because the visible region changed, or some images
    785     // have been invalidated).
    786     private void startLoading() {
    787         clearLoaderQueue();
    788         continueLoading();
    789     }
    790 
    791     private void clearLoaderQueue() {
    792         int[] tags = mLoader.clearQueue();
    793         for (int pos : tags) {
    794             int row = pos / mColumns;
    795             int col = pos - row * mColumns;
    796             ImageBlock blk = mCache.get(row);
    797             Assert(blk != null);  // We won't reuse the block if it has pending
    798                                   // requests. See getEmptyBlock().
    799             blk.cancelRequest(col);
    800         }
    801     }
    802 
    803     // Scan the cache and send requests to ImageLoader if needed.
    804     private void continueLoading() {
    805         // Check if we still have enough requests in the queue.
    806         if (mPendingRequest >= REQUESTS_LOW) return;
    807 
    808         // Scan the visible rows.
    809         for (int i = mStartRow; i < mEndRow; i++) {
    810             if (scanOne(i)) return;
    811         }
    812 
    813         int range = (CACHE_ROWS - (mEndRow - mStartRow)) / 2;
    814         // Scan other rows.
    815         // d is the distance between the row and visible region.
    816         for (int d = 1; d <= range; d++) {
    817             int after = mEndRow - 1 + d;
    818             int before = mStartRow - d;
    819             if (after >= mRows && before < 0) {
    820                 break;  // Nothing more the scan.
    821             }
    822             if (after < mRows && scanOne(after)) return;
    823             if (before >= 0 && scanOne(before)) return;
    824         }
    825     }
    826 
    827     // Returns true if we can stop scanning.
    828     private boolean scanOne(int i) {
    829         mPendingRequest += tryToLoad(i);
    830         return mPendingRequest >= REQUESTS_HIGH;
    831     }
    832 
    833     // Returns number of requests we issued for this row.
    834     private int tryToLoad(int row) {
    835         Assert(row >= 0 && row < mRows);
    836         ImageBlock blk = mCache.get(row);
    837         if (blk == null) {
    838             // Find an empty block
    839             blk = getEmptyBlock();
    840             blk.setRow(row);
    841             blk.invalidate();
    842             mCache.put(row, blk);
    843         }
    844         return blk.loadImages();
    845     }
    846 
    847     // Get an empty block for the cache.
    848     private ImageBlock getEmptyBlock() {
    849         // See if we can allocate a new block.
    850         if (mCache.size() < CACHE_ROWS) {
    851             return new ImageBlock();
    852         }
    853         // Reclaim the old block with largest distance from the visible region.
    854         int bestDistance = -1;
    855         int bestIndex = -1;
    856         for (int index : mCache.keySet()) {
    857             // Make sure we don't reclaim a block which still has pending
    858             // request.
    859             if (mCache.get(index).hasPendingRequests()) {
    860                 continue;
    861             }
    862             int dist = 0;
    863             if (index >= mEndRow) {
    864                 dist = index - mEndRow + 1;
    865             } else if (index < mStartRow) {
    866                 dist = mStartRow - index;
    867             } else {
    868                 // Inside the visible region.
    869                 continue;
    870             }
    871             if (dist > bestDistance) {
    872                 bestDistance = dist;
    873                 bestIndex = index;
    874             }
    875         }
    876         return mCache.remove(bestIndex);
    877     }
    878 
    879     public void invalidateImage(int index) {
    880         int row = index / mColumns;
    881         int col = index - (row * mColumns);
    882         ImageBlock blk = mCache.get(row);
    883         if (blk == null) return;
    884         if ((blk.mCompletedMask & (1 << col)) != 0) {
    885             blk.mCompletedMask &= ~(1 << col);
    886         }
    887         startLoading();
    888     }
    889 
    890     // After calling recycle(), the instance should not be used anymore.
    891     public void recycle() {
    892         for (ImageBlock blk : mCache.values()) {
    893             blk.recycle();
    894         }
    895         mCache.clear();
    896         mEmptyBitmap.recycle();
    897     }
    898 
    899     // Draw the images to the given canvas.
    900     public void doDraw(Canvas canvas, int thisWidth, int thisHeight,
    901             int scrollPos) {
    902         final int height = mBlockHeight;
    903 
    904         // Note that currentBlock could be negative.
    905         int currentBlock = (scrollPos < 0)
    906                 ? ((scrollPos - height + 1) / height)
    907                 : (scrollPos / height);
    908 
    909         while (true) {
    910             final int yPos = currentBlock * height;
    911             if (yPos >= scrollPos + thisHeight) {
    912                 break;
    913             }
    914 
    915             ImageBlock blk = mCache.get(currentBlock);
    916             if (blk != null) {
    917                 blk.doDraw(canvas, 0, yPos);
    918             } else {
    919                 drawEmptyBlock(canvas, 0, yPos, currentBlock);
    920             }
    921 
    922             currentBlock += 1;
    923         }
    924     }
    925 
    926     // Return number of columns in the given row. (This could be less than
    927     // mColumns for the last row).
    928     private int numColumns(int row) {
    929         return Math.min(mColumns, mCount - row * mColumns);
    930     }
    931 
    932     // Draw a block which has not been loaded.
    933     private void drawEmptyBlock(Canvas canvas, int xPos, int yPos, int row) {
    934         // Draw the background.
    935         canvas.drawRect(xPos, yPos, xPos + mBlockWidth, yPos + mBlockHeight,
    936                 mBackgroundPaint);
    937 
    938         // Draw the empty images.
    939         int x = xPos + mSpec.mLeftEdgePadding;
    940         int y = yPos + mSpec.mCellSpacing;
    941         int cols = numColumns(row);
    942 
    943         for (int i = 0; i < cols; i++) {
    944             canvas.drawBitmap(mEmptyBitmap, x, y, null);
    945             x += (mSpec.mCellWidth + mSpec.mCellSpacing);
    946         }
    947     }
    948 
    949     // mEmptyBitmap is what we draw if we the wanted block hasn't been loaded.
    950     // (If the user scrolls too fast). It is a gray image with normal outline.
    951     // mBackgroundPaint is used to draw the (black) background outside
    952     // mEmptyBitmap.
    953     Paint mBackgroundPaint;
    954     private Bitmap mEmptyBitmap;
    955 
    956     private void initGraphics() {
    957         mBackgroundPaint = new Paint();
    958         mBackgroundPaint.setStyle(Paint.Style.FILL);
    959         mBackgroundPaint.setColor(0xFF000000);  // black
    960         mEmptyBitmap = Bitmap.createBitmap(mSpec.mCellWidth, mSpec.mCellHeight,
    961                 Bitmap.Config.RGB_565);
    962         Canvas canvas = new Canvas(mEmptyBitmap);
    963         canvas.drawRGB(0xDD, 0xDD, 0xDD);
    964         canvas.drawBitmap(mOutline, 0, 0, null);
    965     }
    966 
    967     // ImageBlock stores bitmap for one row. The loaded thumbnail images are
    968     // drawn to mBitmap. mBitmap is later used in onDraw() of GridViewSpecial.
    969     private class ImageBlock {
    970         private Bitmap mBitmap;
    971         private final Canvas mCanvas;
    972 
    973         // Columns which have been requested to the loader
    974         private int mRequestedMask;
    975 
    976         // Columns which have been completed from the loader
    977         private int mCompletedMask;
    978 
    979         // The row number this block represents.
    980         private int mRow;
    981 
    982         public ImageBlock() {
    983             mBitmap = Bitmap.createBitmap(mBlockWidth, mBlockHeight,
    984                     Bitmap.Config.RGB_565);
    985             mCanvas = new Canvas(mBitmap);
    986             mRow = -1;
    987         }
    988 
    989         public void setRow(int row) {
    990             mRow = row;
    991         }
    992 
    993         public void invalidate() {
    994             // We do not change mRequestedMask or do cancelAllRequests()
    995             // because the data coming from pending requests are valid. (We only
    996             // invalidate data which has been drawn to the bitmap).
    997             mCompletedMask = 0;
    998         }
    999 
   1000         // After recycle, the ImageBlock instance should not be accessed.
   1001         public void recycle() {
   1002             cancelAllRequests();
   1003             mBitmap.recycle();
   1004             mBitmap = null;
   1005         }
   1006 
   1007         private boolean isVisible() {
   1008             return mRow >= mStartRow && mRow < mEndRow;
   1009         }
   1010 
   1011         // Returns number of requests submitted to ImageLoader.
   1012         public int loadImages() {
   1013             Assert(mRow != -1);
   1014 
   1015             int columns = numColumns(mRow);
   1016 
   1017             // Calculate what we need.
   1018             int needMask = ((1 << columns) - 1)
   1019                     & ~(mCompletedMask | mRequestedMask);
   1020 
   1021             if (needMask == 0) {
   1022                 return 0;
   1023             }
   1024 
   1025             int retVal = 0;
   1026             int base = mRow * mColumns;
   1027 
   1028             for (int col = 0; col < columns; col++) {
   1029                 if ((needMask & (1 << col)) == 0) {
   1030                     continue;
   1031                 }
   1032 
   1033                 int pos = base + col;
   1034 
   1035                 final IImage image = mImageList.getImageAt(pos);
   1036                 if (image != null) {
   1037                     // This callback is passed to ImageLoader. It will invoke
   1038                     // loadImageDone() in the main thread. We limit the callback
   1039                     // thread to be in this very short function. All other
   1040                     // processing is done in the main thread.
   1041                     final int colFinal = col;
   1042                     ImageLoader.LoadedCallback cb =
   1043                             new ImageLoader.LoadedCallback() {
   1044                                     public void run(final Bitmap b) {
   1045                                         mHandler.post(new Runnable() {
   1046                                             public void run() {
   1047                                                 loadImageDone(image, b,
   1048                                                         colFinal);
   1049                                             }
   1050                                         });
   1051                                     }
   1052                                 };
   1053                     // Load Image
   1054                     mLoader.getBitmap(image, cb, pos);
   1055                     mRequestedMask |= (1 << col);
   1056                     retVal += 1;
   1057                 }
   1058             }
   1059 
   1060             return retVal;
   1061         }
   1062 
   1063         // Whether this block has pending requests.
   1064         public boolean hasPendingRequests() {
   1065             return mRequestedMask != 0;
   1066         }
   1067 
   1068         // Called when an image is loaded.
   1069         private void loadImageDone(IImage image, Bitmap b,
   1070                 int col) {
   1071             if (mBitmap == null) return;  // This block has been recycled.
   1072 
   1073             int spacing = mSpec.mCellSpacing;
   1074             int leftSpacing = mSpec.mLeftEdgePadding;
   1075             final int yPos = spacing;
   1076             final int xPos = leftSpacing
   1077                     + (col * (mSpec.mCellWidth + spacing));
   1078 
   1079             drawBitmap(image, b, xPos, yPos);
   1080 
   1081             if (b != null) {
   1082                 b.recycle();
   1083             }
   1084 
   1085             int mask = (1 << col);
   1086             Assert((mCompletedMask & mask) == 0);
   1087             Assert((mRequestedMask & mask) != 0);
   1088             mRequestedMask &= ~mask;
   1089             mCompletedMask |= mask;
   1090             mPendingRequest--;
   1091 
   1092             if (isVisible()) {
   1093                 mRedrawCallback.run();
   1094             }
   1095 
   1096             // Kick start next block loading.
   1097             continueLoading();
   1098         }
   1099 
   1100         // Draw the loaded bitmap to the block bitmap.
   1101         private void drawBitmap(
   1102                 IImage image, Bitmap b, int xPos, int yPos) {
   1103             mDrawAdapter.drawImage(mCanvas, image, b, xPos, yPos,
   1104                     mSpec.mCellWidth, mSpec.mCellHeight);
   1105             mCanvas.drawBitmap(mOutline, xPos, yPos, null);
   1106         }
   1107 
   1108         // Draw the block bitmap to the specified canvas.
   1109         public void doDraw(Canvas canvas, int xPos, int yPos) {
   1110             int cols = numColumns(mRow);
   1111 
   1112             if (cols == mColumns) {
   1113                 canvas.drawBitmap(mBitmap, xPos, yPos, null);
   1114             } else {
   1115 
   1116                 // This must be the last row -- we draw only part of the block.
   1117                 // Draw the background.
   1118                 canvas.drawRect(xPos, yPos, xPos + mBlockWidth,
   1119                         yPos + mBlockHeight, mBackgroundPaint);
   1120                 // Draw part of the block.
   1121                 int w = mSpec.mLeftEdgePadding
   1122                         + cols * (mSpec.mCellWidth + mSpec.mCellSpacing);
   1123                 Rect srcRect = new Rect(0, 0, w, mBlockHeight);
   1124                 Rect dstRect = new Rect(srcRect);
   1125                 dstRect.offset(xPos, yPos);
   1126                 canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
   1127             }
   1128 
   1129             // Draw the part which has not been loaded.
   1130             int isEmpty = ((1 << cols) - 1) & ~mCompletedMask;
   1131 
   1132             if (isEmpty != 0) {
   1133                 int x = xPos + mSpec.mLeftEdgePadding;
   1134                 int y = yPos + mSpec.mCellSpacing;
   1135 
   1136                 for (int i = 0; i < cols; i++) {
   1137                     if ((isEmpty & (1 << i)) != 0) {
   1138                         canvas.drawBitmap(mEmptyBitmap, x, y, null);
   1139                     }
   1140                     x += (mSpec.mCellWidth + mSpec.mCellSpacing);
   1141                 }
   1142             }
   1143         }
   1144 
   1145         // Mark a request as cancelled. The request has already been removed
   1146         // from the queue of ImageLoader, so we only need to mark the fact.
   1147         public void cancelRequest(int col) {
   1148             int mask = (1 << col);
   1149             Assert((mRequestedMask & mask) != 0);
   1150             mRequestedMask &= ~mask;
   1151             mPendingRequest--;
   1152         }
   1153 
   1154         // Try to cancel all pending requests for this block. After this
   1155         // completes there could still be requests not cancelled (because it is
   1156         // already in progress). We deal with that situation by setting mBitmap
   1157         // to null in recycle() and check this in loadImageDone().
   1158         private void cancelAllRequests() {
   1159             for (int i = 0; i < mColumns; i++) {
   1160                 int mask = (1 << i);
   1161                 if ((mRequestedMask & mask) != 0) {
   1162                     int pos = (mRow * mColumns) + i;
   1163                     if (mLoader.cancel(mImageList.getImageAt(pos))) {
   1164                         mRequestedMask &= ~mask;
   1165                         mPendingRequest--;
   1166                     }
   1167                 }
   1168             }
   1169         }
   1170     }
   1171 }
   1172