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 com.android.gallery.R; 20 21 import static com.android.camera.Util.Assert; 22 23 import android.app.Activity; 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.media.AudioManager; 31 import android.os.Handler; 32 import android.util.AttributeSet; 33 import android.util.DisplayMetrics; 34 import android.view.GestureDetector; 35 import android.view.KeyEvent; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.view.GestureDetector.SimpleOnGestureListener; 40 import android.widget.Scroller; 41 42 import com.android.camera.gallery.IImage; 43 import com.android.camera.gallery.IImageList; 44 45 import java.util.HashMap; 46 47 class 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 { onImageClicked(int index)53 public void onImageClicked(int index); onImageTapped(int index)54 public void onImageTapped(int index); onLayoutComplete(boolean changed)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 */ onScroll(float scrollPosition)63 public void onScroll(float scrollPosition); 64 } 65 66 public static interface DrawAdapter { drawImage(Canvas canvas, IImage image, Bitmap b, int xPos, int yPos, int w, int h)67 public void drawImage(Canvas canvas, IImage image, 68 Bitmap b, int xPos, int yPos, int w, int h); drawDecoration(Canvas canvas, IImage image, 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); needsDecoration()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 { LayoutSpec(int w, int h, int intercellSpacing, int leftEdgePadding, DisplayMetrics metrics)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 initCellSize()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. dpToPx(int dp, DisplayMetrics metrics)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 GridViewSpecial(Context context, AttributeSet attrs)143 public GridViewSpecial(Context context, AttributeSet attrs) { 144 super(context, attrs); 145 init(context); 146 } 147 init(Context context)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 setLoader(ImageLoader loader)164 public void setLoader(ImageLoader loader) { 165 Assert(mRunning == false); 166 mLoader = loader; 167 } 168 setListener(Listener listener)169 public void setListener(Listener listener) { 170 Assert(mRunning == false); 171 mListener = listener; 172 } 173 setDrawAdapter(DrawAdapter adapter)174 public void setDrawAdapter(DrawAdapter adapter) { 175 Assert(mRunning == false); 176 mDrawAdapter = adapter; 177 } 178 setImageList(IImageList list)179 public void setImageList(IImageList list) { 180 Assert(mRunning == false); 181 mAllImages = list; 182 mCount = mAllImages.getCount(); 183 } 184 setSizeChoice(int choice)185 public void setSizeChoice(int choice) { 186 Assert(mRunning == false); 187 if (mSizeChoice == choice) return; 188 mSizeChoice = choice; 189 } 190 191 @Override onLayout(boolean changed, int left, int top, int right, int bottom)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 computeVerticalScrollRange()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 generateOutlineBitmap()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 moveDataWindow()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 onDown(MotionEvent e)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 onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)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 onLongPress(MotionEvent e)344 public void onLongPress(MotionEvent e) { 345 if (!canHandleEvent()) return; 346 performLongClick(); 347 } 348 349 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)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 onSingleTapConfirmed(MotionEvent e)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 getCurrentSelection()378 public int getCurrentSelection() { 379 return mCurrentSelection; 380 } 381 invalidateImage(int index)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 */ setSelectedIndex(int index)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 scrollToImage(int index)413 public void scrollToImage(int index) { 414 Rect r = getRectForPosition(index); 415 scrollTo(0, r.top); 416 } 417 scrollToVisible(int index)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 ensureVisible(int pos)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 start()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(); stop()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 onDraw(Canvas canvas)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 computeScroll()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. getRectForPosition(int pos)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. computeSelectedIndex(float xFloat, float yFloat)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 onTouchEvent(MotionEvent ev)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 scrollBy(int x, int y)550 public void scrollBy(int x, int y) { 551 scrollTo(mScrollX + x, mScrollY + y); 552 } 553 scrollTo(float scrollPosition)554 public void scrollTo(float scrollPosition) { 555 scrollTo(0, Math.round(scrollPosition * mMaxScrollY)); 556 } 557 558 @Override scrollTo(int x, int y)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 canHandleEvent()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 onKeyDown(int keyCode, KeyEvent event)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 onKeyUp(int keyCode, KeyEvent event)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 paintDecoration(Canvas canvas)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 paintSelection(Canvas canvas)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 712 class 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 ImageBlockManager(Handler handler, Runnable redrawCallback, IImageList imageList, ImageLoader loader, GridViewSpecial.DrawAdapter adapter, GridViewSpecial.LayoutSpec spec, int columns, int blockWidth, Bitmap outline)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). setVisibleRows(int startRow, int endRow)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). startLoading()786 private void startLoading() { 787 clearLoaderQueue(); 788 continueLoading(); 789 } 790 clearLoaderQueue()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. continueLoading()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. scanOne(int i)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. tryToLoad(int 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. getEmptyBlock()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 invalidateImage(int index)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. recycle()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. doDraw(Canvas canvas, int thisWidth, int thisHeight, int scrollPos)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). numColumns(int 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. drawEmptyBlock(Canvas canvas, int xPos, int yPos, int row)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 initGraphics()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 ImageBlock()982 public ImageBlock() { 983 mBitmap = Bitmap.createBitmap(mBlockWidth, mBlockHeight, 984 Bitmap.Config.RGB_565); 985 mCanvas = new Canvas(mBitmap); 986 mRow = -1; 987 } 988 setRow(int row)989 public void setRow(int row) { 990 mRow = row; 991 } 992 invalidate()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. recycle()1001 public void recycle() { 1002 cancelAllRequests(); 1003 mBitmap.recycle(); 1004 mBitmap = null; 1005 } 1006 isVisible()1007 private boolean isVisible() { 1008 return mRow >= mStartRow && mRow < mEndRow; 1009 } 1010 1011 // Returns number of requests submitted to ImageLoader. loadImages()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. hasPendingRequests()1064 public boolean hasPendingRequests() { 1065 return mRequestedMask != 0; 1066 } 1067 1068 // Called when an image is loaded. loadImageDone(IImage image, Bitmap b, int col)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. drawBitmap( IImage image, Bitmap b, int xPos, int yPos)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. doDraw(Canvas canvas, int xPos, int yPos)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. cancelRequest(int col)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(). cancelAllRequests()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