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