diff options
author | John Reck <jreck@google.com> | 2013-05-29 17:07:21 -0700 |
---|---|---|
committer | John Reck <jreck@google.com> | 2013-05-30 09:26:42 -0700 |
commit | 9cbfe8d5ea68308ece1e57b4b8e38bf5f0344951 (patch) | |
tree | be83713aabfb72c8007efbb55128b784774b4a90 /src | |
parent | c58d6414da1cc38f07f0512f4debf92e8c738ca0 (diff) | |
download | android_packages_apps_Snap-9cbfe8d5ea68308ece1e57b4b8e38bf5f0344951.tar.gz android_packages_apps_Snap-9cbfe8d5ea68308ece1e57b4b8e38bf5f0344951.tar.bz2 android_packages_apps_Snap-9cbfe8d5ea68308ece1e57b4b8e38bf5f0344951.zip |
Copy Tiling changes from G+
Change-Id: Id229728182a002c29699884289f1354b6cb6e714
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/photos/BitmapRegionTileSource.java | 128 | ||||
-rw-r--r-- | src/com/android/photos/FullscreenViewer.java | 2 | ||||
-rw-r--r-- | src/com/android/photos/views/BlockingGLTextureView.java | 33 | ||||
-rw-r--r-- | src/com/android/photos/views/TiledImageRenderer.java | 202 | ||||
-rw-r--r-- | src/com/android/photos/views/TiledImageView.java | 303 |
5 files changed, 481 insertions, 187 deletions
diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java index 1c7115191..d7d52f67a 100644 --- a/src/com/android/photos/BitmapRegionTileSource.java +++ b/src/com/android/photos/BitmapRegionTileSource.java @@ -16,66 +16,131 @@ package com.android.photos; +import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.util.Log; + +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.BitmapTexture; import com.android.photos.views.TiledImageRenderer; import java.io.IOException; +/** + * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using + * {@link BitmapRegionDecoder} to wrap a local file + */ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { - BitmapRegionDecoder mDecoder; + private static final String TAG = "BitmapRegionTileSource"; + private static final boolean REUSE_BITMAP = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + private static final int MAX_PREVIEW_SIZE = 1024; - public BitmapRegionTileSource(String path) { + BitmapRegionDecoder mDecoder; + int mWidth; + int mHeight; + int mTileSize; + private BasicTexture mPreview; + private final int mRotation; + + // For use only by getTile + private Rect mWantRegion = new Rect(); + private Rect mOverlapRegion = new Rect(); + private BitmapFactory.Options mOptions; + private Canvas mCanvas; + + public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) { + mTileSize = TiledImageRenderer.suggestedTileSize(context); + mRotation = rotation; try { mDecoder = BitmapRegionDecoder.newInstance(path, true); + mWidth = mDecoder.getWidth(); + mHeight = mDecoder.getHeight(); } catch (IOException e) { Log.w("BitmapRegionTileSource", "ctor failed", e); } + mOptions = new BitmapFactory.Options(); + mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + mOptions.inPreferQualityOverSpeed = true; + mOptions.inTempStorage = new byte[16 * 1024]; + if (previewSize != 0) { + previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); + // Although this is the same size as the Bitmap that is likely already + // loaded, the lifecycle is different and interactions are on a different + // thread. Thus to simplify, this source will decode its own bitmap. + int sampleSize = (int) Math.ceil(Math.max( + mWidth / (float) previewSize, mHeight / (float) previewSize)); + mOptions.inSampleSize = Math.max(sampleSize, 1); + Bitmap preview = mDecoder.decodeRegion( + new Rect(0, 0, mWidth, mHeight), mOptions); + if (preview.getWidth() <= MAX_PREVIEW_SIZE && preview.getHeight() <= MAX_PREVIEW_SIZE) { + mPreview = new BitmapTexture(preview); + } else { + Log.w(TAG, String.format( + "Failed to create preview of apropriate size! " + + " in: %dx%d, sample: %d, out: %dx%d", + mWidth, mHeight, sampleSize, + preview.getWidth(), preview.getHeight())); + } + } } @Override public int getTileSize() { - return 256; + return mTileSize; } @Override public int getImageWidth() { - return mDecoder.getWidth(); + return mWidth; } @Override public int getImageHeight() { - return mDecoder.getHeight(); + return mHeight; + } + + @Override + public BasicTexture getPreview() { + return mPreview; + } + + @Override + public int getRotation() { + return mRotation; } @Override public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { int tileSize = getTileSize(); - int t = tileSize << level; + if (!REUSE_BITMAP) { + return getTileWithoutReusingBitmap(level, x, y, tileSize); + } - Rect wantRegion = new Rect(x, y, x + t, y + t); + int t = tileSize << level; + mWantRegion.set(x, y, x + t, y + t); if (bitmap == null) { bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); } - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - options.inPreferQualityOverSpeed = true; - options.inSampleSize = (1 << level); - options.inBitmap = bitmap; + mOptions.inSampleSize = (1 << level); + mOptions.inBitmap = bitmap; try { - // In CropImage, we may call the decodeRegion() concurrently. - bitmap = mDecoder.decodeRegion(wantRegion, options); + bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); } finally { - if (options.inBitmap != bitmap && options.inBitmap != null) { - options.inBitmap = null; + if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { + mOptions.inBitmap = null; } } @@ -84,4 +149,35 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } return bitmap; } + + private Bitmap getTileWithoutReusingBitmap( + int level, int x, int y, int tileSize) { + + int t = tileSize << level; + mWantRegion.set(x, y, x + t, y + t); + + mOverlapRegion.set(0, 0, mWidth, mHeight); + + mOptions.inSampleSize = (1 << level); + Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + + if (mWantRegion.equals(mOverlapRegion)) { + return bitmap; + } + + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + if (mCanvas == null) { + mCanvas = new Canvas(); + } + mCanvas.setBitmap(result); + mCanvas.drawBitmap(bitmap, + (mOverlapRegion.left - mWantRegion.left) >> level, + (mOverlapRegion.top - mWantRegion.top) >> level, null); + mCanvas.setBitmap(null); + return result; + } } diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java index 50ea1ba71..a3761395e 100644 --- a/src/com/android/photos/FullscreenViewer.java +++ b/src/com/android/photos/FullscreenViewer.java @@ -31,7 +31,7 @@ public class FullscreenViewer extends Activity { String path = getIntent().getData().toString(); mTextureView = new TiledImageView(this); - mTextureView.setTileSource(new BitmapRegionTileSource(path)); + mTextureView.setTileSource(new BitmapRegionTileSource(this, path, 0, 0), null); setContentView(mTextureView); } diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java index c38f8f7a4..8a0505185 100644 --- a/src/com/android/photos/views/BlockingGLTextureView.java +++ b/src/com/android/photos/views/BlockingGLTextureView.java @@ -16,7 +16,6 @@ package com.android.photos.views; -import android.annotation.SuppressLint; import android.content.Context; import android.graphics.SurfaceTexture; import android.opengl.GLSurfaceView.Renderer; @@ -32,7 +31,9 @@ import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL10; - +/** + * A TextureView that supports blocking rendering for synchronous drawing + */ public class BlockingGLTextureView extends TextureView implements SurfaceTextureListener { @@ -90,7 +91,9 @@ public class BlockingGLTextureView extends TextureView protected void finalize() throws Throwable { try { destroy(); - } catch (Throwable t) {} + } catch (Throwable t) { + // Ignore + } super.finalize(); } @@ -135,8 +138,8 @@ public class BlockingGLTextureView extends TextureView } EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { - int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; - return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList); } /** @@ -161,7 +164,7 @@ public class BlockingGLTextureView extends TextureView * We can now initialize EGL for that display */ int[] version = new int[2]; - if(!mEgl.eglInitialize(mEglDisplay, version)) { + if (!mEgl.eglInitialize(mEglDisplay, version)) { throw new RuntimeException("eglInitialize failed"); } mEglConfig = chooseEglConfig(); @@ -251,7 +254,7 @@ public class BlockingGLTextureView extends TextureView * @return the EGL error code from eglSwapBuffers. */ public int swap() { - if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { return mEgl.eglGetError(); } return EGL10.EGL_SUCCESS; @@ -368,19 +371,24 @@ public class BlockingGLTextureView extends TextureView exec(FINISH); try { join(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + // Ignore + } } private void exec(int msgid) { synchronized (mLock) { if (mExecMsgId != INVALID) { - throw new IllegalArgumentException("Message already set - multithreaded access?"); + throw new IllegalArgumentException( + "Message already set - multithreaded access?"); } mExecMsgId = msgid; mLock.notify(); try { mLock.wait(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + // Ignore + } } } @@ -415,12 +423,15 @@ public class BlockingGLTextureView extends TextureView while (mExecMsgId == INVALID) { try { mLock.wait(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + // Ignore + } } handleMessageLocked(mExecMsgId); mExecMsgId = INVALID; mLock.notify(); } + mExecMsgId = FINISH; } } } diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java index a1f7107f2..c4e493b34 100644 --- a/src/com/android/photos/views/TiledImageRenderer.java +++ b/src/com/android/photos/views/TiledImageRenderer.java @@ -23,14 +23,19 @@ import android.graphics.RectF; import android.support.v4.util.LongSparseArray; import android.util.DisplayMetrics; import android.util.Log; +import android.util.Pools.Pool; +import android.util.Pools.SynchronizedPool; import android.view.View; import android.view.WindowManager; import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLCanvas; import com.android.gallery3d.glrenderer.UploadedTexture; -import com.android.photos.data.GalleryBitmapPool; +/** + * Handles laying out, decoding, and drawing of tiles in GL + */ public class TiledImageRenderer { public static final int SIZE_UNKNOWN = -1; @@ -62,12 +67,13 @@ public class TiledImageRenderer { private static final int STATE_RECYCLING = 0x20; private static final int STATE_RECYCLED = 0x40; - private static GalleryBitmapPool sTilePool = GalleryBitmapPool.getInstance(); + private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64); // TILE_SIZE must be 2^N private int mTileSize; private TileSource mModel; + private BasicTexture mPreview; protected int mLevelCount; // cache the value of mScaledBitmaps.length // The mLevel variable indicates which level of bitmap we should use. @@ -116,22 +122,39 @@ public class TiledImageRenderer { private int mViewWidth, mViewHeight; private View mParent; + /** + * Interface for providing tiles to a {@link TiledImageRenderer} + */ public static interface TileSource { + + /** + * If the source does not care about the tile size, it should use + * {@link TiledImageRenderer#suggestedTileSize(Context)} + */ public int getTileSize(); public int getImageWidth(); public int getImageHeight(); - - // The tile returned by this method can be specified this way: Assuming - // the image size is (width, height), first take the intersection of (0, - // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If - // in extending the region, we found some part of the region is outside - // the image, those pixels are filled with black. - // - // If level > 0, it does the same operation on a down-scaled version of - // the original image (down-scaled by a factor of 2^level), but (x, y) - // still refers to the coordinate on the original image. - // - // The method would be called by the decoder thread. + public int getRotation(); + + /** + * Return a Preview image if available. This will be used as the base layer + * if higher res tiles are not yet available + */ + public BasicTexture getPreview(); + + /** + * The tile returned by this method can be specified this way: Assuming + * the image size is (width, height), first take the intersection of (0, + * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If + * in extending the region, we found some part of the region is outside + * the image, those pixels are filled with black. + * + * If level > 0, it does the same operation on a down-scaled version of + * the original image (down-scaled by a factor of 2^level), but (x, y) + * still refers to the coordinate on the original image. + * + * The method would be called by the decoder thread. + */ public Bitmap getTile(int level, int x, int y, Bitmap reuse); } @@ -173,19 +196,23 @@ public class TiledImageRenderer { if (mRotation != rotation) { mRotation = rotation; mLayoutTiles = true; - invalidate(); } } - private static int calulateLevelCount(TileSource source) { - int levels = 1; - int maxDim = Math.max(source.getImageWidth(), source.getImageHeight()); - int t = source.getTileSize(); - while (t < maxDim) { - t <<= 1; - levels++; + private void calculateLevelCount() { + if (mPreview != null) { + mLevelCount = Math.max(0, Utils.ceilLog2( + mImageWidth / (float) mPreview.getWidth())); + } else { + int levels = 1; + int maxDim = Math.max(mImageWidth, mImageHeight); + int t = mTileSize; + while (t < maxDim) { + t <<= 1; + levels++; + } + mLevelCount = levels; } - return levels; } public void notifyModelInvalidated() { @@ -194,14 +221,15 @@ public class TiledImageRenderer { mImageWidth = 0; mImageHeight = 0; mLevelCount = 0; + mPreview = null; } else { mImageWidth = mModel.getImageWidth(); mImageHeight = mModel.getImageHeight(); - mLevelCount = calulateLevelCount(mModel); + mPreview = mModel.getPreview(); mTileSize = mModel.getTileSize(); + calculateLevelCount(); } mLayoutTiles = true; - invalidate(); } public void setViewSize(int width, int height) { @@ -211,12 +239,13 @@ public class TiledImageRenderer { public void setPosition(int centerX, int centerY, float scale) { if (mCenterX == centerX && mCenterY == centerY - && mScale == scale) return; + && mScale == scale) { + return; + } mCenterX = centerX; mCenterY = centerY; mScale = scale; mLayoutTiles = true; - invalidate(); } // Prepare the tiles we want to use for display. @@ -265,7 +294,9 @@ public class TiledImageRenderer { } // If rotation is transient, don't update the tile. - if (mRotation % 90 != 0) return; + if (mRotation % 90 != 0) { + return; + } synchronized (mQueueLock) { mDecodeQueue.clean(); @@ -305,7 +336,7 @@ public class TiledImageRenderer { mDecodeQueue.clean(); mUploadQueue.clean(); - // TODO disable decoder + // TODO(xx): disable decoder int n = mActiveTiles.size(); for (int i = 0; i < n; i++) { Tile tile = mActiveTiles.valueAt(i); @@ -357,6 +388,7 @@ public class TiledImageRenderer { public void freeTextures() { mLayoutTiles = true; + mTileDecoder.finishAndWait(); synchronized (mQueueLock) { mUploadQueue.clean(); mDecodeQueue.clean(); @@ -375,10 +407,10 @@ public class TiledImageRenderer { mActiveTiles.clear(); mTileRange.set(0, 0, 0, 0); - if (sTilePool != null) sTilePool.clear(); + while (sTilePool.acquire() != null) {} } - public void draw(GLCanvas canvas) { + public boolean draw(GLCanvas canvas) { layoutTiles(); uploadTiles(canvas); @@ -388,7 +420,9 @@ public class TiledImageRenderer { int level = mLevel; int rotation = mRotation; int flags = 0; - if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX; + if (rotation != 0) { + flags |= GLCanvas.SAVE_FLAG_MATRIX; + } if (flags != 0) { canvas.save(flags); @@ -412,9 +446,15 @@ public class TiledImageRenderer { drawTile(canvas, tx, ty, level, x, y, length); } } + } else if (mPreview != null) { + mPreview.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); } } finally { - if (flags != 0) canvas.restore(); + if (flags != 0) { + canvas.restore(); + } } if (mRenderComplete) { @@ -424,6 +464,7 @@ public class TiledImageRenderer { } else { invalidate(); } + return mRenderComplete || mPreview != null; } private void uploadBackgroundTiles(GLCanvas canvas) { @@ -437,17 +478,6 @@ public class TiledImageRenderer { } } - private void queueForUpload(Tile tile) { - synchronized (mQueueLock) { - mUploadQueue.push(tile); - } - invalidate(); - // TODO -// if (mTileUploader.mActive.compareAndSet(false, true)) { -// getGLRoot().addOnGLIdleListener(mTileUploader); -// } - } - private void queueForDecode(Tile tile) { synchronized (mQueueLock) { if (tile.mTileState == STATE_ACTIVATED) { @@ -459,9 +489,11 @@ public class TiledImageRenderer { } } - private boolean decodeTile(Tile tile) { + private void decodeTile(Tile tile) { synchronized (mQueueLock) { - if (tile.mTileState != STATE_IN_QUEUE) return false; + if (tile.mTileState != STATE_IN_QUEUE) { + return; + } tile.mTileState = STATE_DECODING; } boolean decodeComplete = tile.decode(); @@ -469,15 +501,19 @@ public class TiledImageRenderer { if (tile.mTileState == STATE_RECYCLING) { tile.mTileState = STATE_RECYCLED; if (tile.mDecodedTile != null) { - if (sTilePool != null) sTilePool.put(tile.mDecodedTile); + sTilePool.release(tile.mDecodedTile); tile.mDecodedTile = null; } mRecycledQueue.push(tile); - return false; + return; } tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL; - return decodeComplete; + if (!decodeComplete) { + return; + } + mUploadQueue.push(tile); } + invalidate(); } private Tile obtainTile(int x, int y, int level) { @@ -500,7 +536,7 @@ public class TiledImageRenderer { } tile.mTileState = STATE_RECYCLED; if (tile.mDecodedTile != null) { - if (sTilePool != null) sTilePool.put(tile.mDecodedTile); + sTilePool.release(tile.mDecodedTile); tile.mDecodedTile = null; } mRecycledQueue.push(tile); @@ -538,11 +574,16 @@ public class TiledImageRenderer { synchronized (mQueueLock) { tile = mUploadQueue.pop(); } - if (tile == null) break; + if (tile == null) { + break; + } if (!tile.isContentValid()) { - Utils.assertTrue(tile.mTileState == STATE_DECODED); - tile.updateContent(canvas); - --quota; + if (tile.mTileState == STATE_DECODED) { + tile.updateContent(canvas); + --quota; + } else { + Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState); + } } } if (tile != null) { @@ -574,7 +615,17 @@ public class TiledImageRenderer { queueForDecode(tile); } } - drawTile(tile, canvas, source, target); + if (drawTile(tile, canvas, source, target)) { + return; + } + } + if (mPreview != null) { + int size = mTileSize << level; + float scaleX = (float) mPreview.getWidth() / mImageWidth; + float scaleY = (float) mPreview.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + canvas.drawTexture(mPreview, source, target); } } @@ -588,7 +639,9 @@ public class TiledImageRenderer { // Parent can be divided to four quads and tile is one of the four. Tile parent = tile.getParentTile(); - if (parent == null) return false; + if (parent == null) { + return false; + } if (tile.mX == parent.mX) { source.left /= 2f; source.right /= 2f; @@ -623,14 +676,17 @@ public class TiledImageRenderer { @Override protected void onFreeBitmap(Bitmap bitmap) { - if (sTilePool != null) sTilePool.put(bitmap); + sTilePool.release(bitmap); } boolean decode() { // Get a tile from the original image. The tile is down-scaled // by (1 << mTilelevel) from a region in the original image. try { - Bitmap reuse = sTilePool.get(mTileSize, mTileSize); + Bitmap reuse = sTilePool.acquire(); + if (reuse != null && reuse.getWidth() != mTileSize) { + reuse = null; + } mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse); } catch (Throwable t) { Log.w(TAG, "fail to decode tile", t); @@ -676,7 +732,9 @@ public class TiledImageRenderer { } public Tile getParentTile() { - if (mTileLevel + 1 == mLevelCount) return null; + if (mTileLevel + 1 == mLevelCount) { + return null; + } int size = mTileSize << (mTileLevel + 1); int x = size * (mX / size); int y = size * (mY / size); @@ -695,17 +753,34 @@ public class TiledImageRenderer { public Tile pop() { Tile tile = mHead; - if (tile != null) mHead = tile.mNext; + if (tile != null) { + mHead = tile.mNext; + } return tile; } public boolean push(Tile tile) { + if (contains(tile)) { + Log.w(TAG, "Attempting to add a tile already in the queue!"); + return false; + } boolean wasEmpty = mHead == null; tile.mNext = mHead; mHead = tile; return wasEmpty; } + private boolean contains(Tile tile) { + Tile other = mHead; + while (other != null) { + if (other == tile) { + return true; + } + other = other.mNext; + } + return false; + } + public void clean() { mHead = null; } @@ -723,7 +798,7 @@ public class TiledImageRenderer { } private Tile waitForTile() throws InterruptedException { - synchronized(mQueueLock) { + synchronized (mQueueLock) { while (true) { Tile tile = mDecodeQueue.pop(); if (tile != null) { @@ -739,11 +814,10 @@ public class TiledImageRenderer { try { while (!isInterrupted()) { Tile tile = waitForTile(); - if (decodeTile(tile)) { - queueForUpload(tile); - } + decodeTile(tile); } } catch (InterruptedException ex) { + // We were finished } } diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java index 6fe030dae..8bc07c051 100644 --- a/src/com/android/photos/views/TiledImageView.java +++ b/src/com/android/photos/views/TiledImageView.java @@ -16,29 +16,48 @@ package com.android.photos.views; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView.Renderer; +import android.os.Build; import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.Choreographer; +import android.view.Choreographer.FrameCallback; +import android.view.View; import android.widget.FrameLayout; + +import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLES20Canvas; import com.android.photos.views.TiledImageRenderer.TileSource; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; +/** + * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView} + * or {@link BlockingGLTextureView}. + */ +public class TiledImageView extends FrameLayout { -public class TiledImageView extends FrameLayout implements OnScaleGestureListener { + private static final boolean USE_TEXTURE_VIEW = false; + private static final boolean IS_SUPPORTED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + private static final boolean USE_CHOREOGRAPHER = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; private BlockingGLTextureView mTextureView; - private float mLastX, mLastY; + private GLSurfaceView mGLSurfaceView; + private boolean mInvalPending = false; + private FrameCallback mFrameCallback; private static class ImageRendererWrapper { // Guarded by locks @@ -46,20 +65,19 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene int centerX, centerY; int rotation; TileSource source; + Runnable isReadyCallback; // GL thread only TiledImageRenderer image; } - // TODO: left/right paging - private ImageRendererWrapper mRenderers[] = new ImageRendererWrapper[1]; - private ImageRendererWrapper mFocusedRenderer; + private float[] mValues = new float[9]; // ------------------------- // Guarded by mLock // ------------------------- private Object mLock = new Object(); - private ScaleGestureDetector mScaleGestureDetector; + private ImageRendererWrapper mRenderer; public TiledImageView(Context context) { this(context, null); @@ -67,102 +85,99 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene public TiledImageView(Context context, AttributeSet attrs) { super(context, attrs); - mTextureView = new BlockingGLTextureView(context); - addView(mTextureView, new LayoutParams( + if (!IS_SUPPORTED) { + return; + } + + mRenderer = new ImageRendererWrapper(); + mRenderer.image = new TiledImageRenderer(this); + View view; + if (USE_TEXTURE_VIEW) { + mTextureView = new BlockingGLTextureView(context); + mTextureView.setRenderer(new TileRenderer()); + view = mTextureView; + } else { + mGLSurfaceView = new GLSurfaceView(context); + mGLSurfaceView.setEGLContextClientVersion(2); + mGLSurfaceView.setRenderer(new TileRenderer()); + mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + view = mGLSurfaceView; + } + addView(view, new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - mTextureView.setRenderer(new TileRenderer()); - setTileSource(new ColoredTiles()); - mScaleGestureDetector = new ScaleGestureDetector(context, this); + //setTileSource(new ColoredTiles()); } public void destroy() { - mTextureView.destroy(); - } - - public void setTileSource(TileSource source) { - synchronized (mLock) { - for (int i = 0; i < mRenderers.length; i++) { - ImageRendererWrapper renderer = mRenderers[i]; - if (renderer == null) { - renderer = mRenderers[i] = new ImageRendererWrapper(); - } - renderer.source = source; - renderer.centerX = renderer.source.getImageWidth() / 2; - renderer.centerY = renderer.source.getImageHeight() / 2; - renderer.rotation = 0; - renderer.scale = 0; - renderer.image = new TiledImageRenderer(this); - updateScaleIfNecessaryLocked(renderer); - } + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + mTextureView.destroy(); + } else { + mGLSurfaceView.queueEvent(mFreeTextures); } - mFocusedRenderer = mRenderers[0]; - invalidate(); } - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - return true; - } + private Runnable mFreeTextures = new Runnable() { - @Override - public boolean onScale(ScaleGestureDetector detector) { - // Don't need the lock because this will only fire inside of onTouchEvent - mFocusedRenderer.scale *= detector.getScaleFactor(); - invalidate(); - return true; - } + @Override + public void run() { + mRenderer.image.freeTextures(); + } + }; - @Override - public void onScaleEnd(ScaleGestureDetector detector) { + public void onPause() { + if (!IS_SUPPORTED) { + return; + } + if (!USE_TEXTURE_VIEW) { + mGLSurfaceView.onPause(); + } } - @Override - public boolean onTouchEvent(MotionEvent event) { - int action = event.getActionMasked(); - final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; - final int skipIndex = pointerUp ? event.getActionIndex() : -1; - - // Determine focal point - float sumX = 0, sumY = 0; - final int count = event.getPointerCount(); - for (int i = 0; i < count; i++) { - if (skipIndex == i) continue; - sumX += event.getX(i); - sumY += event.getY(i); + public void onResume() { + if (!IS_SUPPORTED) { + return; + } + if (!USE_TEXTURE_VIEW) { + mGLSurfaceView.onResume(); } - final int div = pointerUp ? count - 1 : count; - float x = sumX / div; - float y = sumY / div; + } + public void setTileSource(TileSource source, Runnable isReadyCallback) { + if (!IS_SUPPORTED) { + return; + } synchronized (mLock) { - mScaleGestureDetector.onTouchEvent(event); - switch (action) { - case MotionEvent.ACTION_MOVE: - mFocusedRenderer.centerX += (mLastX - x) / mFocusedRenderer.scale; - mFocusedRenderer.centerY += (mLastY - y) / mFocusedRenderer.scale; - invalidate(); - break; - } + mRenderer.source = source; + mRenderer.isReadyCallback = isReadyCallback; + mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0; + mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0; + mRenderer.rotation = source != null ? source.getRotation() : 0; + mRenderer.scale = 0; + updateScaleIfNecessaryLocked(mRenderer); } - - mLastX = x; - mLastY = y; - return true; + invalidate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); + if (!IS_SUPPORTED) { + return; + } synchronized (mLock) { - for (ImageRendererWrapper renderer : mRenderers) { - updateScaleIfNecessaryLocked(renderer); - } + updateScaleIfNecessaryLocked(mRenderer); } } private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) { - if (renderer.scale > 0 || getWidth() == 0) return; + if (renderer == null || renderer.source == null + || renderer.scale > 0 || getWidth() == 0) { + return; + } renderer.scale = Math.min( (float) getWidth() / (float) renderer.source.getImageWidth(), (float) getHeight() / (float) renderer.source.getImageHeight()); @@ -170,14 +185,93 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene @Override protected void dispatchDraw(Canvas canvas) { - mTextureView.render(); + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + mTextureView.render(); + } super.dispatchDraw(canvas); } + @SuppressLint("NewApi") + @Override + public void setTranslationX(float translationX) { + if (!IS_SUPPORTED) { + return; + } + super.setTranslationX(translationX); + } + @Override public void invalidate() { - super.invalidate(); - mTextureView.invalidate(); + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + super.invalidate(); + mTextureView.invalidate(); + } else { + if (USE_CHOREOGRAPHER) { + invalOnVsync(); + } else { + mGLSurfaceView.requestRender(); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void invalOnVsync() { + if (!mInvalPending) { + mInvalPending = true; + if (mFrameCallback == null) { + mFrameCallback = new FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + mInvalPending = false; + mGLSurfaceView.requestRender(); + } + }; + } + Choreographer.getInstance().postFrameCallback(mFrameCallback); + } + } + + private RectF mTempRectF = new RectF(); + public void positionFromMatrix(Matrix matrix) { + if (!IS_SUPPORTED) { + return; + } + if (mRenderer.source != null) { + final int rotation = mRenderer.source.getRotation(); + final boolean swap = !(rotation % 180 == 0); + final int width = swap ? mRenderer.source.getImageHeight() + : mRenderer.source.getImageWidth(); + final int height = swap ? mRenderer.source.getImageWidth() + : mRenderer.source.getImageHeight(); + mTempRectF.set(0, 0, width, height); + matrix.mapRect(mTempRectF); + matrix.getValues(mValues); + int cx = width / 2; + int cy = height / 2; + float scale = mValues[Matrix.MSCALE_X]; + int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale); + int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale); + if (rotation == 90 || rotation == 180) { + cx += (mTempRectF.left / scale) - xoffset; + } else { + cx -= (mTempRectF.left / scale) - xoffset; + } + if (rotation == 180 || rotation == 270) { + cy += (mTempRectF.top / scale) - yoffset; + } else { + cy -= (mTempRectF.top / scale) - yoffset; + } + mRenderer.scale = scale; + mRenderer.centerX = swap ? cy : cx; + mRenderer.centerY = swap ? cx : cy; + invalidate(); + } } private class TileRenderer implements Renderer { @@ -187,37 +281,46 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { mCanvas = new GLES20Canvas(); - for (ImageRendererWrapper renderer : mRenderers) { - renderer.image.setModel(renderer.source, renderer.rotation); - } + BasicTexture.invalidateAllTextures(); + mRenderer.image.setModel(mRenderer.source, mRenderer.rotation); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { mCanvas.setSize(width, height); - for (ImageRendererWrapper renderer : mRenderers) { - renderer.image.setViewSize(width, height); - } + mRenderer.image.setViewSize(width, height); } @Override public void onDrawFrame(GL10 gl) { mCanvas.clearBuffer(); + Runnable readyCallback; synchronized (mLock) { - for (ImageRendererWrapper renderer : mRenderers) { - renderer.image.setModel(renderer.source, renderer.rotation); - renderer.image.setPosition(renderer.centerX, renderer.centerY, renderer.scale); - } + readyCallback = mRenderer.isReadyCallback; + mRenderer.image.setModel(mRenderer.source, mRenderer.rotation); + mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY, + mRenderer.scale); } - for (ImageRendererWrapper renderer : mRenderers) { - renderer.image.draw(mCanvas); + boolean complete = mRenderer.image.draw(mCanvas); + if (complete && readyCallback != null) { + synchronized (mLock) { + // Make sure we don't trample on a newly set callback/source + // if it changed while we were rendering + if (mRenderer.isReadyCallback == readyCallback) { + mRenderer.isReadyCallback = null; + } + } + if (readyCallback != null) { + post(readyCallback); + } } } } + @SuppressWarnings("unused") private static class ColoredTiles implements TileSource { - private static int[] COLORS = new int[] { + private static final int[] COLORS = new int[] { Color.RED, Color.BLUE, Color.YELLOW, @@ -246,6 +349,11 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene } @Override + public int getRotation() { + return 0; + } + + @Override public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { int tileSize = getTileSize(); if (bitmap == null) { @@ -265,5 +373,10 @@ public class TiledImageView extends FrameLayout implements OnScaleGestureListene mCanvas.setBitmap(null); return bitmap; } + + @Override + public BasicTexture getPreview() { + return null; + } } } |