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