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