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