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