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