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