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