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