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 {
onImageClicked(int index)53         public void onImageClicked(int index);
onImageTapped(int index)54         public void onImageTapped(int index);
onLayoutComplete(boolean changed)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          */
onScroll(float scrollPosition)63         public void onScroll(float scrollPosition);
64     }
65 
66     public static interface DrawAdapter {
drawImage(Canvas canvas, IImage image, Bitmap b, int xPos, int yPos, int w, int h)67         public void drawImage(Canvas canvas, IImage image,
68                 Bitmap b, int xPos, int yPos, int w, int h);
drawDecoration(Canvas canvas, IImage image, 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);
needsDecoration()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 {
LayoutSpec(int w, int h, int intercellSpacing, int leftEdgePadding, DisplayMetrics metrics)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 
initCellSize()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.
dpToPx(int dp, DisplayMetrics metrics)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 
GridViewSpecial(Context context, AttributeSet attrs)143     public GridViewSpecial(Context context, AttributeSet attrs) {
144         super(context, attrs);
145         init(context);
146     }
147 
init(Context context)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 
setLoader(ImageLoader loader)164     public void setLoader(ImageLoader loader) {
165         Assert(mRunning == false);
166         mLoader = loader;
167     }
168 
setListener(Listener listener)169     public void setListener(Listener listener) {
170         Assert(mRunning == false);
171         mListener = listener;
172     }
173 
setDrawAdapter(DrawAdapter adapter)174     public void setDrawAdapter(DrawAdapter adapter) {
175         Assert(mRunning == false);
176         mDrawAdapter = adapter;
177     }
178 
setImageList(IImageList list)179     public void setImageList(IImageList list) {
180         Assert(mRunning == false);
181         mAllImages = list;
182         mCount = mAllImages.getCount();
183     }
184 
setSizeChoice(int choice)185     public void setSizeChoice(int choice) {
186         Assert(mRunning == false);
187         if (mSizeChoice == choice) return;
188         mSizeChoice = choice;
189     }
190 
191     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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
computeVerticalScrollRange()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 
generateOutlineBitmap()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 
moveDataWindow()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
onDown(MotionEvent e)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
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)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
onLongPress(MotionEvent e)344         public void onLongPress(MotionEvent e) {
345             if (!canHandleEvent()) return;
346             performLongClick();
347         }
348 
349         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)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
onSingleTapConfirmed(MotionEvent e)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 
getCurrentSelection()378     public int getCurrentSelection() {
379         return mCurrentSelection;
380     }
381 
invalidateImage(int index)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      */
setSelectedIndex(int index)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 
scrollToImage(int index)413     public void scrollToImage(int index) {
414         Rect r = getRectForPosition(index);
415         scrollTo(0, r.top);
416     }
417 
scrollToVisible(int index)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 
ensureVisible(int pos)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 
start()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();
stop()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
onDraw(Canvas canvas)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
computeScroll()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.
getRectForPosition(int pos)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.
computeSelectedIndex(float xFloat, float yFloat)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
onTouchEvent(MotionEvent ev)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
scrollBy(int x, int y)550     public void scrollBy(int x, int y) {
551         scrollTo(mScrollX + x, mScrollY + y);
552     }
553 
scrollTo(float scrollPosition)554     public void scrollTo(float scrollPosition) {
555         scrollTo(0, Math.round(scrollPosition * mMaxScrollY));
556     }
557 
558     @Override
scrollTo(int x, int y)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 
canHandleEvent()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
onKeyDown(int keyCode, KeyEvent event)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
onKeyUp(int keyCode, KeyEvent event)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 
paintDecoration(Canvas canvas)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 
paintSelection(Canvas canvas)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 
ImageBlockManager(Handler handler, Runnable redrawCallback, IImageList imageList, ImageLoader loader, GridViewSpecial.DrawAdapter adapter, GridViewSpecial.LayoutSpec spec, int columns, int blockWidth, Bitmap outline)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).
setVisibleRows(int startRow, int endRow)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).
startLoading()786     private void startLoading() {
787         clearLoaderQueue();
788         continueLoading();
789     }
790 
clearLoaderQueue()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.
continueLoading()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.
scanOne(int i)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.
tryToLoad(int 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.
getEmptyBlock()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 
invalidateImage(int index)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.
recycle()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.
doDraw(Canvas canvas, int thisWidth, int thisHeight, int scrollPos)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).
numColumns(int 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.
drawEmptyBlock(Canvas canvas, int xPos, int yPos, int row)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 
initGraphics()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 
ImageBlock()982         public ImageBlock() {
983             mBitmap = Bitmap.createBitmap(mBlockWidth, mBlockHeight,
984                     Bitmap.Config.RGB_565);
985             mCanvas = new Canvas(mBitmap);
986             mRow = -1;
987         }
988 
setRow(int row)989         public void setRow(int row) {
990             mRow = row;
991         }
992 
invalidate()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.
recycle()1001         public void recycle() {
1002             cancelAllRequests();
1003             mBitmap.recycle();
1004             mBitmap = null;
1005         }
1006 
isVisible()1007         private boolean isVisible() {
1008             return mRow >= mStartRow && mRow < mEndRow;
1009         }
1010 
1011         // Returns number of requests submitted to ImageLoader.
loadImages()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.
hasPendingRequests()1064         public boolean hasPendingRequests() {
1065             return mRequestedMask != 0;
1066         }
1067 
1068         // Called when an image is loaded.
loadImageDone(IImage image, Bitmap b, int col)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.
drawBitmap( IImage image, Bitmap b, int xPos, int yPos)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.
doDraw(Canvas canvas, int xPos, int yPos)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.
cancelRequest(int col)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().
cancelAllRequests()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