blob: 5326ebb69d3112c10bab57ce9b5724247cf5894f [file] [log] [blame]
Chih-Chung Chang6b270502009-04-29 11:57:06 +08001/*
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
Owen Lin1153ba12009-04-17 17:26:14 +080017package com.android.camera;
18
Owen Lindeb57252009-05-27 14:55:45 -070019import static com.android.camera.Util.Assert;
20
Chih-Chung Changab8c77f2009-07-28 18:41:23 +080021import android.app.Activity;
Owen Lin1153ba12009-04-17 17:26:14 +080022import android.content.Context;
23import android.graphics.Bitmap;
Owen Lin1153ba12009-04-17 17:26:14 +080024import android.graphics.Canvas;
Owen Lin1153ba12009-04-17 17:26:14 +080025import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
Wu-cheng Lib0663472009-08-12 11:36:49 +080028import android.media.AudioManager;
Owen Lin1153ba12009-04-17 17:26:14 +080029import android.os.Handler;
30import android.util.AttributeSet;
Chih-Chung Changab8c77f2009-07-28 18:41:23 +080031import android.util.DisplayMetrics;
Owen Lin1153ba12009-04-17 17:26:14 +080032import android.view.GestureDetector;
Owen Linf310a3d2009-04-20 14:05:17 +080033import android.view.KeyEvent;
Owen Lin1153ba12009-04-17 17:26:14 +080034import android.view.MotionEvent;
35import android.view.View;
Owen Linf310a3d2009-04-20 14:05:17 +080036import android.view.ViewConfiguration;
Owen Lin0e27f8d2009-04-29 01:11:45 -070037import android.view.GestureDetector.SimpleOnGestureListener;
Owen Lin1153ba12009-04-17 17:26:14 +080038import android.widget.Scroller;
39
Owen Line594b192009-08-13 18:04:45 +080040import com.android.camera.gallery.IImage;
41import com.android.camera.gallery.IImageList;
42
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080043import java.util.HashMap;
44
Owen Lin1153ba12009-04-17 17:26:14 +080045class GridViewSpecial extends View {
Owen Lindeb57252009-05-27 14:55:45 -070046 @SuppressWarnings("unused")
Owen Lin1153ba12009-04-17 17:26:14 +080047 private static final String TAG = "GridViewSpecial";
Owen Lin4c2f8572009-07-08 15:13:54 -070048 private static final float MAX_FLING_VELOCITY = 2500;
Owen Lin1153ba12009-04-17 17:26:14 +080049
Owen Linf310a3d2009-04-20 14:05:17 +080050 public static interface Listener {
Owen Lin0e27f8d2009-04-29 01:11:45 -070051 public void onImageClicked(int index);
Owen Lin4c2f8572009-07-08 15:13:54 -070052 public void onImageTapped(int index);
Owen Lin0e27f8d2009-04-29 01:11:45 -070053 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
Chih-Chung Chang724e9b32009-05-19 14:35:15 +080059 * [0, 1], when 0 means on the top and 1 means on the buttom
Owen Lin0e27f8d2009-04-29 01:11:45 -070060 */
61 public void onScroll(float scrollPosition);
Owen Linf310a3d2009-04-20 14:05:17 +080062 }
63
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080064 public static interface DrawAdapter {
65 public void drawImage(Canvas canvas, IImage image,
66 Bitmap b, int xPos, int yPos, int w, int h);
Chih-Chung Changcb4b88e2009-07-07 20:01:55 +080067 public void drawDecoration(Canvas canvas, IImage image,
68 int xPos, int yPos, int w, int h);
69 public boolean needsDecoration();
Owen Lin1153ba12009-04-17 17:26:14 +080070 }
71
Owen Lin4c2f8572009-07-08 15:13:54 -070072 public static final int INDEX_NONE = -1;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080073
74 // There are two cell size we will use. It can be set by setSizeChoice().
Chih-Chung Chang33435072009-05-14 12:36:58 +080075 // The mLeftEdgePadding fields is filled in onLayout(). See the comments
76 // in onLayout() for details.
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080077 static class LayoutSpec {
Chih-Chung Changab8c77f2009-07-28 18:41:23 +080078 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);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080084 }
85 int mCellWidth, mCellHeight;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080086 int mCellSpacing;
Chih-Chung Chang33435072009-05-14 12:36:58 +080087 int mLeftEdgePadding;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +080088 }
89
Chih-Chung Changab8c77f2009-07-28 18:41:23 +080090 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 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800106
107 // These are set in init().
Owen Lin77e110b2009-05-12 19:10:57 -0700108 private final Handler mHandler = new Handler();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800109 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;
Chih-Chung Changf5bf8ca2009-08-25 18:28:29 +0800116 private IImageList mAllImages = ImageManager.makeEmptyImageList();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800117 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
Owen Lin4c2f8572009-07-08 15:13:54 -0700128 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;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800132
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;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800139 private Scroller mScroller = null;
140
Owen Lin1153ba12009-04-17 17:26:14 +0800141 public GridViewSpecial(Context context, AttributeSet attrs) {
142 super(context, attrs);
143 init(context);
144 }
145
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800146 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);
Chih-Chung Changab8c77f2009-07-28 18:41:23 +0800153 initCellSize();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800154 }
155
Owen Lin77e110b2009-05-12 19:10:57 -0700156 private final Runnable mRedrawCallback = new Runnable() {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800157 public void run() {
158 invalidate();
159 }
160 };
161
162 public void setLoader(ImageLoader loader) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800163 Assert(mRunning == false);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800164 mLoader = loader;
165 }
166
167 public void setListener(Listener listener) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800168 Assert(mRunning == false);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800169 mListener = listener;
170 }
171
172 public void setDrawAdapter(DrawAdapter adapter) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800173 Assert(mRunning == false);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800174 mDrawAdapter = adapter;
Owen Lin1153ba12009-04-17 17:26:14 +0800175 }
176
Owen Linf310a3d2009-04-20 14:05:17 +0800177 public void setImageList(IImageList list) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800178 Assert(mRunning == false);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800179 mAllImages = list;
180 mCount = mAllImages.getCount();
181 }
182
183 public void setSizeChoice(int choice) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800184 Assert(mRunning == false);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800185 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
Chih-Chung Chang33435072009-05-14 12:36:58 +0800207 // padding are derived. We make left and right paddings the same size.
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800208 //
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;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800219
220 mRows = (mCount + mColumns - 1) / mColumns;
221 mBlockHeight = mSpec.mCellSpacing + mSpec.mCellHeight;
222 mMaxScrollY = mSpec.mCellSpacing + (mRows * mBlockHeight)
223 - (bottom - top);
224
Chih-Chung Chang56a86432009-05-13 16:42:57 +0800225 // 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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800229 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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800239 mListener.onLayoutComplete(changed);
240
Chih-Chung Chang56a86432009-05-13 16:42:57 +0800241 moveDataWindow();
242
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800243 mLayoutComplete = true;
Owen Linf310a3d2009-04-20 14:05:17 +0800244 }
245
Owen Lin1153ba12009-04-17 17:26:14 +0800246 @Override
247 protected int computeVerticalScrollRange() {
248 return mMaxScrollY + getHeight();
249 }
250
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800251 // 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]);
Owen Lin4c2f8572009-07-08 15:13:54 -0700278 cellOutline.setState(
279 PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800280 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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800287 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)
Chih-Chung Changf1474b02009-05-15 11:22:37 +0800291 / mBlockHeight + 1;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800292
Chih-Chung Chang56a86432009-05-13 16:42:57 +0800293 // Limit startRow and endRow to the valid range.
Chih-Chung Changf1474b02009-05-15 11:22:37 +0800294 // 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);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800298 }
299
Chih-Chung Chang938aad02009-05-22 13:32:46 +0800300 // 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.
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800303 private class MyGestureDetector extends SimpleOnGestureListener {
Wu-cheng Lib0663472009-08-12 11:36:49 +0800304 private AudioManager mAudioManager;
305
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800306 @Override
307 public boolean onDown(MotionEvent e) {
Chih-Chung Chang938aad02009-05-22 13:32:46 +0800308 if (!canHandleEvent()) return false;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800309 if (mScroller != null && !mScroller.isFinished()) {
310 mScroller.forceFinished(true);
311 return false;
312 }
Owen Lin4c2f8572009-07-08 15:13:54 -0700313 int index = computeSelectedIndex(e.getX(), e.getY());
314 if (index >= 0 && index < mCount) {
315 setSelectedIndex(index);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800316 } else {
Owen Lin4c2f8572009-07-08 15:13:54 -0700317 setSelectedIndex(INDEX_NONE);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800318 }
319 return true;
320 }
321
322 @Override
323 public boolean onFling(MotionEvent e1, MotionEvent e2,
324 float velocityX, float velocityY) {
Chih-Chung Chang938aad02009-05-22 13:32:46 +0800325 if (!canHandleEvent()) return false;
Owen Lin4c2f8572009-07-08 15:13:54 -0700326 if (velocityY > MAX_FLING_VELOCITY) {
327 velocityY = MAX_FLING_VELOCITY;
328 } else if (velocityY < -MAX_FLING_VELOCITY) {
329 velocityY = -MAX_FLING_VELOCITY;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800330 }
331
Owen Lin4c2f8572009-07-08 15:13:54 -0700332 setSelectedIndex(INDEX_NONE);
Chih-Chung Chang33435072009-05-14 12:36:58 +0800333 mScroller = new Scroller(getContext());
334 mScroller.fling(0, mScrollY, 0, -(int) velocityY, 0, 0, 0,
335 mMaxScrollY);
336 computeScroll();
337
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800338 return true;
339 }
340
341 @Override
342 public void onLongPress(MotionEvent e) {
Chih-Chung Chang4952a3f2009-05-14 10:35:07 +0800343 if (!canHandleEvent()) return;
344 performLongClick();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800345 }
346
347 @Override
348 public boolean onScroll(MotionEvent e1, MotionEvent e2,
349 float distanceX, float distanceY) {
Chih-Chung Chang938aad02009-05-22 13:32:46 +0800350 if (!canHandleEvent()) return false;
Owen Lin4c2f8572009-07-08 15:13:54 -0700351 setSelectedIndex(INDEX_NONE);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800352 scrollBy(0, (int) distanceY);
353 invalidate();
354 return true;
355 }
356
357 @Override
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800358 public boolean onSingleTapConfirmed(MotionEvent e) {
Chih-Chung Chang938aad02009-05-22 13:32:46 +0800359 if (!canHandleEvent()) return false;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800360 int index = computeSelectedIndex(e.getX(), e.getY());
361 if (index >= 0 && index < mCount) {
Wu-cheng Lib0663472009-08-12 11:36:49 +0800362 // Play click sound.
363 if (mAudioManager == null) {
Chih-Chung Chang522e8362009-08-26 16:12:34 +0800364 mAudioManager = (AudioManager) getContext()
365 .getSystemService(Context.AUDIO_SERVICE);
Wu-cheng Lib0663472009-08-12 11:36:49 +0800366 }
367 mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
368
Owen Lin4c2f8572009-07-08 15:13:54 -0700369 mListener.onImageTapped(index);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800370 return true;
371 }
372 return false;
373 }
374 }
375
376 public int getCurrentSelection() {
377 return mCurrentSelection;
378 }
379
Ray Chen92eedd62009-07-15 18:38:29 +0800380 public void invalidateImage(int index) {
Owen Lin4c2f8572009-07-08 15:13:54 -0700381 if (index != INDEX_NONE) {
Ray Chen92eedd62009-07-15 18:38:29 +0800382 mImageBlockManager.invalidateImage(index);
383 }
384 }
385
Owen Lin1153ba12009-04-17 17:26:14 +0800386 /**
387 *
Owen Lin4c2f8572009-07-08 15:13:54 -0700388 * @param index <code>INDEX_NONE</code> (-1) means remove selection.
Owen Lin1153ba12009-04-17 17:26:14 +0800389 */
Owen Lin4c2f8572009-07-08 15:13:54 -0700390 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
Chih-Chung Chang522e8362009-08-26 16:12:34 +0800394 // 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.
Owen Lin4c2f8572009-07-08 15:13:54 -0700398
399 if (mCurrentSelection == index) {
Owen Lin1153ba12009-04-17 17:26:14 +0800400 return;
401 }
Chih-Chung Chang724e9b32009-05-19 14:35:15 +0800402 // This happens when the last picture is deleted.
Owen Lin4c2f8572009-07-08 15:13:54 -0700403 mCurrentSelection = Math.min(index, mCount - 1);
Chih-Chung Chang724e9b32009-05-19 14:35:15 +0800404
Owen Lin4c2f8572009-07-08 15:13:54 -0700405 if (mCurrentSelection != INDEX_NONE) {
406 ensureVisible(mCurrentSelection);
Owen Lin1153ba12009-04-17 17:26:14 +0800407 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800408 invalidate();
Owen Lin1153ba12009-04-17 17:26:14 +0800409 }
410
Owen Linf310a3d2009-04-20 14:05:17 +0800411 public void scrollToImage(int index) {
412 Rect r = getRectForPosition(index);
413 scrollTo(0, r.top);
414 }
415
Owen Lin0e27f8d2009-04-29 01:11:45 -0700416 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
Owen Lin1153ba12009-04-17 17:26:14 +0800427 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 }
Owen Lin1153ba12009-04-17 17:26:14 +0800442 }
443
444 public void start() {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800445 // These must be set before start().
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800446 Assert(mLoader != null);
447 Assert(mListener != null);
448 Assert(mDrawAdapter != null);
Owen Linf310a3d2009-04-20 14:05:17 +0800449 mRunning = true;
450 requestLayout();
Owen Lin1153ba12009-04-17 17:26:14 +0800451 }
452
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800453 // 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();
Owen Linf310a3d2009-04-20 14:05:17 +0800461 public void stop() {
Owen Lin77e110b2009-05-12 19:10:57 -0700462 // Remove the long press callback from the queue if we are going to
Chih-Chung Chang4952a3f2009-05-14 10:35:07 +0800463 // stop.
Owen Lin77e110b2009-05-12 19:10:57 -0700464 mHandler.removeCallbacks(mLongPressCallback);
Owen Lin1153ba12009-04-17 17:26:14 +0800465 mScroller = null;
Owen Lin1153ba12009-04-17 17:26:14 +0800466 if (mImageBlockManager != null) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800467 mImageBlockManager.recycle();
Owen Lin1153ba12009-04-17 17:26:14 +0800468 mImageBlockManager = null;
469 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800470 mRunning = false;
Owen Lin4c2f8572009-07-08 15:13:54 -0700471 mCurrentSelection = INDEX_NONE;
Owen Lin1153ba12009-04-17 17:26:14 +0800472 }
473
474 @Override
475 public void onDraw(Canvas canvas) {
476 super.onDraw(canvas);
Chih-Chung Chang4952a3f2009-05-14 10:35:07 +0800477 if (!canHandleEvent()) return;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800478 mImageBlockManager.doDraw(canvas, getWidth(), getHeight(), mScrollY);
Chih-Chung Changcb4b88e2009-07-07 20:01:55 +0800479 paintDecoration(canvas);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800480 paintSelection(canvas);
481 moveDataWindow();
Owen Lin1153ba12009-04-17 17:26:14 +0800482 }
483
484 @Override
485 public void computeScroll() {
486 if (mScroller != null) {
487 boolean more = mScroller.computeScrollOffset();
488 scrollTo(0, mScroller.getCurrY());
489 if (more) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800490 invalidate(); // So we draw again
Owen Lin1153ba12009-04-17 17:26:14 +0800491 } 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) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800501 int row = pos / mColumns;
502 int col = pos - (row * mColumns);
Owen Lin1153ba12009-04-17 17:26:14 +0800503
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800504 int left = mSpec.mLeftEdgePadding
505 + (col * (mSpec.mCellWidth + mSpec.mCellSpacing));
506 int top = row * mBlockHeight;
Owen Lin1153ba12009-04-17 17:26:14 +0800507
508 return new Rect(left, top,
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800509 left + mSpec.mCellWidth + mSpec.mCellSpacing,
510 top + mSpec.mCellHeight + mSpec.mCellSpacing);
Owen Lin1153ba12009-04-17 17:26:14 +0800511 }
512
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800513 // 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;
Owen Lin1153ba12009-04-17 17:26:14 +0800517
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800518 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;
Owen Lin1153ba12009-04-17 17:26:14 +0800525 }
526
527 @Override
528 public boolean onTouchEvent(MotionEvent ev) {
Owen Linf310a3d2009-04-20 14:05:17 +0800529 if (!canHandleEvent()) {
Owen Lin1153ba12009-04-17 17:26:14 +0800530 return false;
531 }
Owen Lin4c2f8572009-07-08 15:13:54 -0700532 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 }
Owen Lin1153ba12009-04-17 17:26:14 +0800542 mGestureDetector.onTouchEvent(ev);
Owen Lin4c2f8572009-07-08 15:13:54 -0700543 // Consume all events
Owen Lin1153ba12009-04-17 17:26:14 +0800544 return true;
545 }
546
547 @Override
548 public void scrollBy(int x, int y) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800549 scrollTo(mScrollX + x, mScrollY + y);
Owen Lin1153ba12009-04-17 17:26:14 +0800550 }
551
Owen Lin0e27f8d2009-04-29 01:11:45 -0700552 public void scrollTo(float scrollPosition) {
553 scrollTo(0, Math.round(scrollPosition * mMaxScrollY));
554 }
555
Owen Lin1153ba12009-04-17 17:26:14 +0800556 @Override
557 public void scrollTo(int x, int y) {
Owen Lin0e27f8d2009-04-29 01:11:45 -0700558 y = Math.max(0, Math.min(mMaxScrollY, y));
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800559 if (mSpec != null) {
Owen Lin0e27f8d2009-04-29 01:11:45 -0700560 mListener.onScroll((float) mScrollY / mMaxScrollY);
Owen Lin1153ba12009-04-17 17:26:14 +0800561 }
562 super.scrollTo(x, y);
563 }
Owen Linf310a3d2009-04-20 14:05:17 +0800564
565 private boolean canHandleEvent() {
566 return mRunning && mLayoutComplete;
567 }
568
569 private final Runnable mLongPressCallback = new Runnable() {
570 public void run() {
Owen Lin4c2f8572009-07-08 15:13:54 -0700571 mCurrentPressState &= ~CLICKING_FLAG;
Owen Linf310a3d2009-04-20 14:05:17 +0800572 showContextMenu();
573 }
574 };
575
Owen Linf310a3d2009-04-20 14:05:17 +0800576 @Override
577 public boolean onKeyDown(int keyCode, KeyEvent event) {
578 if (!canHandleEvent()) return false;
Owen Linf310a3d2009-04-20 14:05:17 +0800579 int sel = mCurrentSelection;
Owen Lin4c2f8572009-07-08 15:13:54 -0700580 if (sel != INDEX_NONE) {
Owen Linf310a3d2009-04-20 14:05:17 +0800581 switch (keyCode) {
582 case KeyEvent.KEYCODE_DPAD_RIGHT:
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800583 if (sel != mCount - 1 && (sel % mColumns < mColumns - 1)) {
Owen Linf310a3d2009-04-20 14:05:17 +0800584 sel += 1;
585 }
586 break;
587 case KeyEvent.KEYCODE_DPAD_LEFT:
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800588 if (sel > 0 && (sel % mColumns != 0)) {
Owen Linf310a3d2009-04-20 14:05:17 +0800589 sel -= 1;
590 }
591 break;
592 case KeyEvent.KEYCODE_DPAD_UP:
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800593 if (sel >= mColumns) {
594 sel -= mColumns;
Owen Linf310a3d2009-04-20 14:05:17 +0800595 }
596 break;
597 case KeyEvent.KEYCODE_DPAD_DOWN:
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800598 sel = Math.min(mCount - 1, sel + mColumns);
Owen Linf310a3d2009-04-20 14:05:17 +0800599 break;
600 case KeyEvent.KEYCODE_DPAD_CENTER:
Chih-Chung Changd998ce92009-09-27 11:45:07 -0700601 if (event.getRepeatCount() == 0) {
602 mCurrentPressState |= CLICKING_FLAG;
603 mHandler.postDelayed(mLongPressCallback,
604 ViewConfiguration.getLongPressTimeout());
605 }
Owen Linf310a3d2009-04-20 14:05:17 +0800606 break;
607 default:
Owen Lin4c2f8572009-07-08 15:13:54 -0700608 return super.onKeyDown(keyCode, event);
Owen Linf310a3d2009-04-20 14:05:17 +0800609 }
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:
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800616 int startRow =
617 (mScrollY - mSpec.mCellSpacing) / mBlockHeight;
618 int topPos = startRow * mColumns;
Owen Linf310a3d2009-04-20 14:05:17 +0800619 Rect r = getRectForPosition(topPos);
620 if (r.top < getScrollY()) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800621 topPos += mColumns;
Owen Linf310a3d2009-04-20 14:05:17 +0800622 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800623 topPos = Math.min(mCount - 1, topPos);
Owen Linf310a3d2009-04-20 14:05:17 +0800624 sel = topPos;
Owen Linf310a3d2009-04-20 14:05:17 +0800625 break;
626 default:
Owen Lin4c2f8572009-07-08 15:13:54 -0700627 return super.onKeyDown(keyCode, event);
Owen Linf310a3d2009-04-20 14:05:17 +0800628 }
629 }
Owen Lin4c2f8572009-07-08 15:13:54 -0700630 setSelectedIndex(sel);
631 return true;
Owen Linf310a3d2009-04-20 14:05:17 +0800632 }
633
634 @Override
635 public boolean onKeyUp(int keyCode, KeyEvent event) {
636 if (!canHandleEvent()) return false;
637
638 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
Owen Lin4c2f8572009-07-08 15:13:54 -0700639 mCurrentPressState &= ~CLICKING_FLAG;
640 invalidate();
Owen Linf310a3d2009-04-20 14:05:17 +0800641
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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800648 mListener.onImageClicked(mCurrentSelection);
Owen Linf310a3d2009-04-20 14:05:17 +0800649 return true;
650 }
651 return super.onKeyUp(keyCode, event);
652 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800653
Chih-Chung Changcb4b88e2009-07-07 20:01:55 +0800654 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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800691 private void paintSelection(Canvas canvas) {
Owen Lin4c2f8572009-07-08 15:13:54 -0700692 if (mCurrentSelection == INDEX_NONE) return;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800693
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);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800701
702 int type = OUTLINE_SELECTED;
Owen Lin4c2f8572009-07-08 15:13:54 -0700703 if (mCurrentPressState != 0) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800704 type = OUTLINE_PRESSED;
705 }
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800706 canvas.drawBitmap(mOutline[type], xPos, yTop, null);
707 }
708}
709
710class ImageBlockManager {
Owen Lindeb57252009-05-27 14:55:45 -0700711 @SuppressWarnings("unused")
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800712 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).
Chih-Chung Chang724e9b32009-05-19 14:35:15 +0800775 // We want to keep enough requests in ImageLoader's queue, but not too
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800776 // many.
777 static final int REQUESTS_LOW = 3;
778 static final int REQUESTS_HIGH = 6;
779
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800780 // 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).
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800784 private void startLoading() {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800785 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() {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800803 // 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;
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800817 if (after >= mRows && before < 0) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800818 break; // Nothing more the scan.
819 }
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800820 if (after < mRows && scanOne(after)) return;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800821 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) {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800833 Assert(row >= 0 && row < mRows);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800834 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.
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800857 if (mCache.get(index).hasPendingRequests()) {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800858 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
Ray Chen92eedd62009-07-15 18:38:29 +0800877 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
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800888 // 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;
Owen Lin77e110b2009-05-12 19:10:57 -0700969 private final Canvas mCanvas;
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800970
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() {
Chih-Chung Chang341ad982009-05-12 14:44:49 +0800992 // We do not change mRequestedMask or do cancelAllRequests()
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +0800993 // 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() {
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001000 cancelAllRequests();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001001 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() {
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001011 Assert(mRow != -1);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001012
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
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001052 mLoader.getBitmap(image, cb, pos);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001053 mRequestedMask |= (1 << col);
1054 retVal += 1;
1055 }
1056 }
1057
1058 return retVal;
1059 }
1060
1061 // Whether this block has pending requests.
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001062 public boolean hasPendingRequests() {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001063 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);
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001084 Assert((mCompletedMask & mask) == 0);
1085 Assert((mRequestedMask & mask) != 0);
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001086 mRequestedMask &= ~mask;
1087 mCompletedMask |= mask;
1088 mPendingRequest--;
1089
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001090 if (isVisible()) {
1091 mRedrawCallback.run();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001092 }
1093
1094 // Kick start next block loading.
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001095 continueLoading();
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001096 }
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
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001143 // 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
Chih-Chung Chang91acfc92009-07-06 15:37:24 +08001154 // already in progress). We deal with that situation by setting mBitmap
1155 // to null in recycle() and check this in loadImageDone().
Chih-Chung Chang341ad982009-05-12 14:44:49 +08001156 private void cancelAllRequests() {
Chih-Chung Changa3be1aa2009-05-11 19:09:14 +08001157 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 }
Owen Lin1153ba12009-04-17 17:26:14 +08001169}