1 /* 2 * Copyright (C) 2013 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.photos.views; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Rect; 22 import android.graphics.RectF; 23 import android.support.v4.util.LongSparseArray; 24 import android.util.DisplayMetrics; 25 import android.util.Log; 26 import android.util.Pools.Pool; 27 import android.util.Pools.SynchronizedPool; 28 import android.view.View; 29 import android.view.WindowManager; 30 31 import com.android.gallery3d.common.Utils; 32 import com.android.gallery3d.glrenderer.BasicTexture; 33 import com.android.gallery3d.glrenderer.GLCanvas; 34 import com.android.gallery3d.glrenderer.UploadedTexture; 35 36 /** 37 * Handles laying out, decoding, and drawing of tiles in GL 38 */ 39 public class TiledImageRenderer { 40 public static final int SIZE_UNKNOWN = -1; 41 42 private static final String TAG = "TiledImageRenderer"; 43 private static final int UPLOAD_LIMIT = 1; 44 45 /* 46 * This is the tile state in the CPU side. 47 * Life of a Tile: 48 * ACTIVATED (initial state) 49 * --> IN_QUEUE - by queueForDecode() 50 * --> RECYCLED - by recycleTile() 51 * IN_QUEUE --> DECODING - by decodeTile() 52 * --> RECYCLED - by recycleTile) 53 * DECODING --> RECYCLING - by recycleTile() 54 * --> DECODED - by decodeTile() 55 * --> DECODE_FAIL - by decodeTile() 56 * RECYCLING --> RECYCLED - by decodeTile() 57 * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) 58 * DECODED --> RECYCLED - by recycleTile() 59 * DECODE_FAIL -> RECYCLED - by recycleTile() 60 * RECYCLED --> ACTIVATED - by obtainTile() 61 */ 62 private static final int STATE_ACTIVATED = 0x01; 63 private static final int STATE_IN_QUEUE = 0x02; 64 private static final int STATE_DECODING = 0x04; 65 private static final int STATE_DECODED = 0x08; 66 private static final int STATE_DECODE_FAIL = 0x10; 67 private static final int STATE_RECYCLING = 0x20; 68 private static final int STATE_RECYCLED = 0x40; 69 70 private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64); 71 72 // TILE_SIZE must be 2^N 73 private int mTileSize; 74 75 private TileSource mModel; 76 private BasicTexture mPreview; 77 protected int mLevelCount; // cache the value of mScaledBitmaps.length 78 79 // The mLevel variable indicates which level of bitmap we should use. 80 // Level 0 means the original full-sized bitmap, and a larger value means 81 // a smaller scaled bitmap (The width and height of each scaled bitmap is 82 // half size of the previous one). If the value is in [0, mLevelCount), we 83 // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value 84 // is mLevelCount 85 private int mLevel = 0; 86 87 private int mOffsetX; 88 private int mOffsetY; 89 90 private int mUploadQuota; 91 private boolean mRenderComplete; 92 93 private final RectF mSourceRect = new RectF(); 94 private final RectF mTargetRect = new RectF(); 95 96 private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>(); 97 98 // The following three queue are guarded by mQueueLock 99 private final Object mQueueLock = new Object(); 100 private final TileQueue mRecycledQueue = new TileQueue(); 101 private final TileQueue mUploadQueue = new TileQueue(); 102 private final TileQueue mDecodeQueue = new TileQueue(); 103 104 // The width and height of the full-sized bitmap 105 protected int mImageWidth = SIZE_UNKNOWN; 106 protected int mImageHeight = SIZE_UNKNOWN; 107 108 protected int mCenterX; 109 protected int mCenterY; 110 protected float mScale; 111 protected int mRotation; 112 113 private boolean mLayoutTiles; 114 115 // Temp variables to avoid memory allocation 116 private final Rect mTileRange = new Rect(); 117 private final Rect mActiveRange[] = {new Rect(), new Rect()}; 118 119 private TileDecoder mTileDecoder; 120 private boolean mBackgroundTileUploaded; 121 122 private int mViewWidth, mViewHeight; 123 private View mParent; 124 125 /** 126 * Interface for providing tiles to a {@link TiledImageRenderer} 127 */ 128 public static interface TileSource { 129 130 /** 131 * If the source does not care about the tile size, it should use 132 * {@link TiledImageRenderer#suggestedTileSize(Context)} 133 */ getTileSize()134 public int getTileSize(); getImageWidth()135 public int getImageWidth(); getImageHeight()136 public int getImageHeight(); getRotation()137 public int getRotation(); 138 139 /** 140 * Return a Preview image if available. This will be used as the base layer 141 * if higher res tiles are not yet available 142 */ getPreview()143 public BasicTexture getPreview(); 144 145 /** 146 * The tile returned by this method can be specified this way: Assuming 147 * the image size is (width, height), first take the intersection of (0, 148 * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If 149 * in extending the region, we found some part of the region is outside 150 * the image, those pixels are filled with black. 151 * 152 * If level > 0, it does the same operation on a down-scaled version of 153 * the original image (down-scaled by a factor of 2^level), but (x, y) 154 * still refers to the coordinate on the original image. 155 * 156 * The method would be called by the decoder thread. 157 */ getTile(int level, int x, int y, Bitmap reuse)158 public Bitmap getTile(int level, int x, int y, Bitmap reuse); 159 } 160 suggestedTileSize(Context context)161 public static int suggestedTileSize(Context context) { 162 return isHighResolution(context) ? 512 : 256; 163 } 164 isHighResolution(Context context)165 private static boolean isHighResolution(Context context) { 166 DisplayMetrics metrics = new DisplayMetrics(); 167 WindowManager wm = (WindowManager) 168 context.getSystemService(Context.WINDOW_SERVICE); 169 wm.getDefaultDisplay().getMetrics(metrics); 170 return metrics.heightPixels > 2048 || metrics.widthPixels > 2048; 171 } 172 TiledImageRenderer(View parent)173 public TiledImageRenderer(View parent) { 174 mParent = parent; 175 mTileDecoder = new TileDecoder(); 176 mTileDecoder.start(); 177 } 178 getViewWidth()179 public int getViewWidth() { 180 return mViewWidth; 181 } 182 getViewHeight()183 public int getViewHeight() { 184 return mViewHeight; 185 } 186 invalidate()187 private void invalidate() { 188 mParent.postInvalidate(); 189 } 190 setModel(TileSource model, int rotation)191 public void setModel(TileSource model, int rotation) { 192 if (mModel != model) { 193 mModel = model; 194 notifyModelInvalidated(); 195 } 196 if (mRotation != rotation) { 197 mRotation = rotation; 198 mLayoutTiles = true; 199 } 200 } 201 calculateLevelCount()202 private void calculateLevelCount() { 203 if (mPreview != null) { 204 mLevelCount = Math.max(0, Utils.ceilLog2( 205 mImageWidth / (float) mPreview.getWidth())); 206 } else { 207 int levels = 1; 208 int maxDim = Math.max(mImageWidth, mImageHeight); 209 int t = mTileSize; 210 while (t < maxDim) { 211 t <<= 1; 212 levels++; 213 } 214 mLevelCount = levels; 215 } 216 } 217 notifyModelInvalidated()218 public void notifyModelInvalidated() { 219 invalidateTiles(); 220 if (mModel == null) { 221 mImageWidth = 0; 222 mImageHeight = 0; 223 mLevelCount = 0; 224 mPreview = null; 225 } else { 226 mImageWidth = mModel.getImageWidth(); 227 mImageHeight = mModel.getImageHeight(); 228 mPreview = mModel.getPreview(); 229 mTileSize = mModel.getTileSize(); 230 calculateLevelCount(); 231 } 232 mLayoutTiles = true; 233 } 234 setViewSize(int width, int height)235 public void setViewSize(int width, int height) { 236 mViewWidth = width; 237 mViewHeight = height; 238 } 239 setPosition(int centerX, int centerY, float scale)240 public void setPosition(int centerX, int centerY, float scale) { 241 if (mCenterX == centerX && mCenterY == centerY 242 && mScale == scale) { 243 return; 244 } 245 mCenterX = centerX; 246 mCenterY = centerY; 247 mScale = scale; 248 mLayoutTiles = true; 249 } 250 251 // Prepare the tiles we want to use for display. 252 // 253 // 1. Decide the tile level we want to use for display. 254 // 2. Decide the tile levels we want to keep as texture (in addition to 255 // the one we use for display). 256 // 3. Recycle unused tiles. 257 // 4. Activate the tiles we want. layoutTiles()258 private void layoutTiles() { 259 if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) { 260 return; 261 } 262 mLayoutTiles = false; 263 264 // The tile levels we want to keep as texture is in the range 265 // [fromLevel, endLevel). 266 int fromLevel; 267 int endLevel; 268 269 // We want to use a texture larger than or equal to the display size. 270 mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount); 271 272 // We want to keep one more tile level as texture in addition to what 273 // we use for display. So it can be faster when the scale moves to the 274 // next level. We choose the level closest to the current scale. 275 if (mLevel != mLevelCount) { 276 Rect range = mTileRange; 277 getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation); 278 mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale); 279 mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale); 280 fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; 281 } else { 282 // Activate the tiles of the smallest two levels. 283 fromLevel = mLevel - 2; 284 mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale); 285 mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale); 286 } 287 288 fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); 289 endLevel = Math.min(fromLevel + 2, mLevelCount); 290 291 Rect range[] = mActiveRange; 292 for (int i = fromLevel; i < endLevel; ++i) { 293 getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation); 294 } 295 296 // If rotation is transient, don't update the tile. 297 if (mRotation % 90 != 0) { 298 return; 299 } 300 301 synchronized (mQueueLock) { 302 mDecodeQueue.clean(); 303 mUploadQueue.clean(); 304 mBackgroundTileUploaded = false; 305 306 // Recycle unused tiles: if the level of the active tile is outside the 307 // range [fromLevel, endLevel) or not in the visible range. 308 int n = mActiveTiles.size(); 309 for (int i = 0; i < n; i++) { 310 Tile tile = mActiveTiles.valueAt(i); 311 int level = tile.mTileLevel; 312 if (level < fromLevel || level >= endLevel 313 || !range[level - fromLevel].contains(tile.mX, tile.mY)) { 314 mActiveTiles.removeAt(i); 315 i--; 316 n--; 317 recycleTile(tile); 318 } 319 } 320 } 321 322 for (int i = fromLevel; i < endLevel; ++i) { 323 int size = mTileSize << i; 324 Rect r = range[i - fromLevel]; 325 for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { 326 for (int x = r.left, right = r.right; x < right; x += size) { 327 activateTile(x, y, i); 328 } 329 } 330 } 331 invalidate(); 332 } 333 invalidateTiles()334 private void invalidateTiles() { 335 synchronized (mQueueLock) { 336 mDecodeQueue.clean(); 337 mUploadQueue.clean(); 338 339 // TODO(xx): disable decoder 340 int n = mActiveTiles.size(); 341 for (int i = 0; i < n; i++) { 342 Tile tile = mActiveTiles.valueAt(i); 343 recycleTile(tile); 344 } 345 mActiveTiles.clear(); 346 } 347 } 348 getRange(Rect out, int cX, int cY, int level, int rotation)349 private void getRange(Rect out, int cX, int cY, int level, int rotation) { 350 getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); 351 } 352 353 // If the bitmap is scaled by the given factor "scale", return the 354 // rectangle containing visible range. The left-top coordinate returned is 355 // aligned to the tile boundary. 356 // 357 // (cX, cY) is the point on the original bitmap which will be put in the 358 // center of the ImageViewer. getRange(Rect out, int cX, int cY, int level, float scale, int rotation)359 private void getRange(Rect out, 360 int cX, int cY, int level, float scale, int rotation) { 361 362 double radians = Math.toRadians(-rotation); 363 double w = mViewWidth; 364 double h = mViewHeight; 365 366 double cos = Math.cos(radians); 367 double sin = Math.sin(radians); 368 int width = (int) Math.ceil(Math.max( 369 Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); 370 int height = (int) Math.ceil(Math.max( 371 Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); 372 373 int left = (int) Math.floor(cX - width / (2f * scale)); 374 int top = (int) Math.floor(cY - height / (2f * scale)); 375 int right = (int) Math.ceil(left + width / scale); 376 int bottom = (int) Math.ceil(top + height / scale); 377 378 // align the rectangle to tile boundary 379 int size = mTileSize << level; 380 left = Math.max(0, size * (left / size)); 381 top = Math.max(0, size * (top / size)); 382 right = Math.min(mImageWidth, right); 383 bottom = Math.min(mImageHeight, bottom); 384 385 out.set(left, top, right, bottom); 386 } 387 freeTextures()388 public void freeTextures() { 389 mLayoutTiles = true; 390 391 mTileDecoder.finishAndWait(); 392 synchronized (mQueueLock) { 393 mUploadQueue.clean(); 394 mDecodeQueue.clean(); 395 Tile tile = mRecycledQueue.pop(); 396 while (tile != null) { 397 tile.recycle(); 398 tile = mRecycledQueue.pop(); 399 } 400 } 401 402 int n = mActiveTiles.size(); 403 for (int i = 0; i < n; i++) { 404 Tile texture = mActiveTiles.valueAt(i); 405 texture.recycle(); 406 } 407 mActiveTiles.clear(); 408 mTileRange.set(0, 0, 0, 0); 409 410 while (sTilePool.acquire() != null) {} 411 } 412 draw(GLCanvas canvas)413 public boolean draw(GLCanvas canvas) { 414 layoutTiles(); 415 uploadTiles(canvas); 416 417 mUploadQuota = UPLOAD_LIMIT; 418 mRenderComplete = true; 419 420 int level = mLevel; 421 int rotation = mRotation; 422 int flags = 0; 423 if (rotation != 0) { 424 flags |= GLCanvas.SAVE_FLAG_MATRIX; 425 } 426 427 if (flags != 0) { 428 canvas.save(flags); 429 if (rotation != 0) { 430 int centerX = mViewWidth / 2, centerY = mViewHeight / 2; 431 canvas.translate(centerX, centerY); 432 canvas.rotate(rotation, 0, 0, 1); 433 canvas.translate(-centerX, -centerY); 434 } 435 } 436 try { 437 if (level != mLevelCount) { 438 int size = (mTileSize << level); 439 float length = size * mScale; 440 Rect r = mTileRange; 441 442 for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { 443 float y = mOffsetY + i * length; 444 for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { 445 float x = mOffsetX + j * length; 446 drawTile(canvas, tx, ty, level, x, y, length); 447 } 448 } 449 } else if (mPreview != null) { 450 mPreview.draw(canvas, mOffsetX, mOffsetY, 451 Math.round(mImageWidth * mScale), 452 Math.round(mImageHeight * mScale)); 453 } 454 } finally { 455 if (flags != 0) { 456 canvas.restore(); 457 } 458 } 459 460 if (mRenderComplete) { 461 if (!mBackgroundTileUploaded) { 462 uploadBackgroundTiles(canvas); 463 } 464 } else { 465 invalidate(); 466 } 467 return mRenderComplete || mPreview != null; 468 } 469 uploadBackgroundTiles(GLCanvas canvas)470 private void uploadBackgroundTiles(GLCanvas canvas) { 471 mBackgroundTileUploaded = true; 472 int n = mActiveTiles.size(); 473 for (int i = 0; i < n; i++) { 474 Tile tile = mActiveTiles.valueAt(i); 475 if (!tile.isContentValid()) { 476 queueForDecode(tile); 477 } 478 } 479 } 480 queueForDecode(Tile tile)481 private void queueForDecode(Tile tile) { 482 synchronized (mQueueLock) { 483 if (tile.mTileState == STATE_ACTIVATED) { 484 tile.mTileState = STATE_IN_QUEUE; 485 if (mDecodeQueue.push(tile)) { 486 mQueueLock.notifyAll(); 487 } 488 } 489 } 490 } 491 decodeTile(Tile tile)492 private void decodeTile(Tile tile) { 493 synchronized (mQueueLock) { 494 if (tile.mTileState != STATE_IN_QUEUE) { 495 return; 496 } 497 tile.mTileState = STATE_DECODING; 498 } 499 boolean decodeComplete = tile.decode(); 500 synchronized (mQueueLock) { 501 if (tile.mTileState == STATE_RECYCLING) { 502 tile.mTileState = STATE_RECYCLED; 503 if (tile.mDecodedTile != null) { 504 sTilePool.release(tile.mDecodedTile); 505 tile.mDecodedTile = null; 506 } 507 mRecycledQueue.push(tile); 508 return; 509 } 510 tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL; 511 if (!decodeComplete) { 512 return; 513 } 514 mUploadQueue.push(tile); 515 } 516 invalidate(); 517 } 518 obtainTile(int x, int y, int level)519 private Tile obtainTile(int x, int y, int level) { 520 synchronized (mQueueLock) { 521 Tile tile = mRecycledQueue.pop(); 522 if (tile != null) { 523 tile.mTileState = STATE_ACTIVATED; 524 tile.update(x, y, level); 525 return tile; 526 } 527 return new Tile(x, y, level); 528 } 529 } 530 recycleTile(Tile tile)531 private void recycleTile(Tile tile) { 532 synchronized (mQueueLock) { 533 if (tile.mTileState == STATE_DECODING) { 534 tile.mTileState = STATE_RECYCLING; 535 return; 536 } 537 tile.mTileState = STATE_RECYCLED; 538 if (tile.mDecodedTile != null) { 539 sTilePool.release(tile.mDecodedTile); 540 tile.mDecodedTile = null; 541 } 542 mRecycledQueue.push(tile); 543 } 544 } 545 activateTile(int x, int y, int level)546 private void activateTile(int x, int y, int level) { 547 long key = makeTileKey(x, y, level); 548 Tile tile = mActiveTiles.get(key); 549 if (tile != null) { 550 if (tile.mTileState == STATE_IN_QUEUE) { 551 tile.mTileState = STATE_ACTIVATED; 552 } 553 return; 554 } 555 tile = obtainTile(x, y, level); 556 mActiveTiles.put(key, tile); 557 } 558 getTile(int x, int y, int level)559 private Tile getTile(int x, int y, int level) { 560 return mActiveTiles.get(makeTileKey(x, y, level)); 561 } 562 makeTileKey(int x, int y, int level)563 private static long makeTileKey(int x, int y, int level) { 564 long result = x; 565 result = (result << 16) | y; 566 result = (result << 16) | level; 567 return result; 568 } 569 uploadTiles(GLCanvas canvas)570 private void uploadTiles(GLCanvas canvas) { 571 int quota = UPLOAD_LIMIT; 572 Tile tile = null; 573 while (quota > 0) { 574 synchronized (mQueueLock) { 575 tile = mUploadQueue.pop(); 576 } 577 if (tile == null) { 578 break; 579 } 580 if (!tile.isContentValid()) { 581 if (tile.mTileState == STATE_DECODED) { 582 tile.updateContent(canvas); 583 --quota; 584 } else { 585 Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState); 586 } 587 } 588 } 589 if (tile != null) { 590 invalidate(); 591 } 592 } 593 594 // Draw the tile to a square at canvas that locates at (x, y) and 595 // has a side length of length. drawTile(GLCanvas canvas, int tx, int ty, int level, float x, float y, float length)596 private void drawTile(GLCanvas canvas, 597 int tx, int ty, int level, float x, float y, float length) { 598 RectF source = mSourceRect; 599 RectF target = mTargetRect; 600 target.set(x, y, x + length, y + length); 601 source.set(0, 0, mTileSize, mTileSize); 602 603 Tile tile = getTile(tx, ty, level); 604 if (tile != null) { 605 if (!tile.isContentValid()) { 606 if (tile.mTileState == STATE_DECODED) { 607 if (mUploadQuota > 0) { 608 --mUploadQuota; 609 tile.updateContent(canvas); 610 } else { 611 mRenderComplete = false; 612 } 613 } else if (tile.mTileState != STATE_DECODE_FAIL){ 614 mRenderComplete = false; 615 queueForDecode(tile); 616 } 617 } 618 if (drawTile(tile, canvas, source, target)) { 619 return; 620 } 621 } 622 if (mPreview != null) { 623 int size = mTileSize << level; 624 float scaleX = (float) mPreview.getWidth() / mImageWidth; 625 float scaleY = (float) mPreview.getHeight() / mImageHeight; 626 source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, 627 (ty + size) * scaleY); 628 canvas.drawTexture(mPreview, source, target); 629 } 630 } 631 drawTile( Tile tile, GLCanvas canvas, RectF source, RectF target)632 private boolean drawTile( 633 Tile tile, GLCanvas canvas, RectF source, RectF target) { 634 while (true) { 635 if (tile.isContentValid()) { 636 canvas.drawTexture(tile, source, target); 637 return true; 638 } 639 640 // Parent can be divided to four quads and tile is one of the four. 641 Tile parent = tile.getParentTile(); 642 if (parent == null) { 643 return false; 644 } 645 if (tile.mX == parent.mX) { 646 source.left /= 2f; 647 source.right /= 2f; 648 } else { 649 source.left = (mTileSize + source.left) / 2f; 650 source.right = (mTileSize + source.right) / 2f; 651 } 652 if (tile.mY == parent.mY) { 653 source.top /= 2f; 654 source.bottom /= 2f; 655 } else { 656 source.top = (mTileSize + source.top) / 2f; 657 source.bottom = (mTileSize + source.bottom) / 2f; 658 } 659 tile = parent; 660 } 661 } 662 663 private class Tile extends UploadedTexture { 664 public int mX; 665 public int mY; 666 public int mTileLevel; 667 public Tile mNext; 668 public Bitmap mDecodedTile; 669 public volatile int mTileState = STATE_ACTIVATED; 670 Tile(int x, int y, int level)671 public Tile(int x, int y, int level) { 672 mX = x; 673 mY = y; 674 mTileLevel = level; 675 } 676 677 @Override onFreeBitmap(Bitmap bitmap)678 protected void onFreeBitmap(Bitmap bitmap) { 679 sTilePool.release(bitmap); 680 } 681 decode()682 boolean decode() { 683 // Get a tile from the original image. The tile is down-scaled 684 // by (1 << mTilelevel) from a region in the original image. 685 try { 686 Bitmap reuse = sTilePool.acquire(); 687 if (reuse != null && reuse.getWidth() != mTileSize) { 688 reuse = null; 689 } 690 mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse); 691 } catch (Throwable t) { 692 Log.w(TAG, "fail to decode tile", t); 693 } 694 return mDecodedTile != null; 695 } 696 697 @Override onGetBitmap()698 protected Bitmap onGetBitmap() { 699 Utils.assertTrue(mTileState == STATE_DECODED); 700 701 // We need to override the width and height, so that we won't 702 // draw beyond the boundaries. 703 int rightEdge = ((mImageWidth - mX) >> mTileLevel); 704 int bottomEdge = ((mImageHeight - mY) >> mTileLevel); 705 setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge)); 706 707 Bitmap bitmap = mDecodedTile; 708 mDecodedTile = null; 709 mTileState = STATE_ACTIVATED; 710 return bitmap; 711 } 712 713 // We override getTextureWidth() and getTextureHeight() here, so the 714 // texture can be re-used for different tiles regardless of the actual 715 // size of the tile (which may be small because it is a tile at the 716 // boundary). 717 @Override getTextureWidth()718 public int getTextureWidth() { 719 return mTileSize; 720 } 721 722 @Override getTextureHeight()723 public int getTextureHeight() { 724 return mTileSize; 725 } 726 update(int x, int y, int level)727 public void update(int x, int y, int level) { 728 mX = x; 729 mY = y; 730 mTileLevel = level; 731 invalidateContent(); 732 } 733 getParentTile()734 public Tile getParentTile() { 735 if (mTileLevel + 1 == mLevelCount) { 736 return null; 737 } 738 int size = mTileSize << (mTileLevel + 1); 739 int x = size * (mX / size); 740 int y = size * (mY / size); 741 return getTile(x, y, mTileLevel + 1); 742 } 743 744 @Override toString()745 public String toString() { 746 return String.format("tile(%s, %s, %s / %s)", 747 mX / mTileSize, mY / mTileSize, mLevel, mLevelCount); 748 } 749 } 750 751 private static class TileQueue { 752 private Tile mHead; 753 pop()754 public Tile pop() { 755 Tile tile = mHead; 756 if (tile != null) { 757 mHead = tile.mNext; 758 } 759 return tile; 760 } 761 push(Tile tile)762 public boolean push(Tile tile) { 763 if (contains(tile)) { 764 Log.w(TAG, "Attempting to add a tile already in the queue!"); 765 return false; 766 } 767 boolean wasEmpty = mHead == null; 768 tile.mNext = mHead; 769 mHead = tile; 770 return wasEmpty; 771 } 772 contains(Tile tile)773 private boolean contains(Tile tile) { 774 Tile other = mHead; 775 while (other != null) { 776 if (other == tile) { 777 return true; 778 } 779 other = other.mNext; 780 } 781 return false; 782 } 783 clean()784 public void clean() { 785 mHead = null; 786 } 787 } 788 789 private class TileDecoder extends Thread { 790 finishAndWait()791 public void finishAndWait() { 792 interrupt(); 793 try { 794 join(); 795 } catch (InterruptedException e) { 796 Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!"); 797 } 798 } 799 waitForTile()800 private Tile waitForTile() throws InterruptedException { 801 synchronized (mQueueLock) { 802 while (true) { 803 Tile tile = mDecodeQueue.pop(); 804 if (tile != null) { 805 return tile; 806 } 807 mQueueLock.wait(); 808 } 809 } 810 } 811 812 @Override run()813 public void run() { 814 try { 815 while (!isInterrupted()) { 816 Tile tile = waitForTile(); 817 decodeTile(tile); 818 } 819 } catch (InterruptedException ex) { 820 // We were finished 821 } 822 } 823 824 } 825 } 826