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