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