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