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