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