From 4745319b4f5e1f5056502f20698eb5910d249333 Mon Sep 17 00:00:00 2001 From: Chih-Chung Chang Date: Tue, 20 Mar 2012 20:56:49 +0800 Subject: [PATCH] Patch for MR1. Change-Id: I3f4bba2854257008eed95b8eacce598abdca3180 --- .../android/gallery3d/app/PhotoDataAdapter.java | 5 +- .../android/gallery3d/ui/BitmapTileProvider.java | 25 ++- src/com/android/gallery3d/ui/PhotoView.java | 208 +++++++++++++++++---- src/com/android/gallery3d/ui/TileImageView.java | 89 +++++++-- .../android/gallery3d/ui/TileImageViewAdapter.java | 59 +++--- src/com/android/gallery3d/ui/UploadedTexture.java | 2 + 6 files changed, 312 insertions(+), 76 deletions(-) diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java index 9b1c8c4c0..d7d116882 100644 --- a/src/com/android/gallery3d/app/PhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -365,8 +365,9 @@ public class PhotoDataAdapter implements PhotoPage.Model { return mTileProvider.getLevelCount(); } - public Bitmap getTile(int level, int x, int y, int tileSize) { - return mTileProvider.getTile(level, x, y, tileSize); + public Bitmap getTile(int level, int x, int y, int tileSize, + int borderSize) { + return mTileProvider.getTile(level, x, y, tileSize, borderSize); } public boolean isFailedToLoad() { diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java index a47337fa2..3d4d4dc2c 100644 --- a/src/com/android/gallery3d/ui/BitmapTileProvider.java +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -65,11 +65,28 @@ public class BitmapTileProvider implements TileImageView.Model { return mMipmaps.length; } - public Bitmap getTile(int level, int x, int y, int tileSize) { - Bitmap result = Bitmap.createBitmap(tileSize, tileSize, mConfig); + public Bitmap getTile(int level, int x, int y, int tileSize, + int borderSize) { + x >>= level; + y >>= level; + int size = tileSize + 2 * borderSize; + Bitmap result = Bitmap.createBitmap(size, size, mConfig); + Bitmap mipmap = mMipmaps[level]; Canvas canvas = new Canvas(result); - canvas.drawBitmap(mMipmaps[level], -(x >> level), -(y >> level), null); - return result; + int offsetX = -x + borderSize; + int offsetY = -y + borderSize; + canvas.drawBitmap(mipmap, offsetX, offsetY, null); + + // If the valid region (covered by mipmap or border) is smaller than the + // result bitmap, subset it. + int endX = offsetX + mipmap.getWidth() + borderSize; + int endY = offsetY + mipmap.getHeight() + borderSize; + if (endX < size || endY < size) { + return Bitmap.createBitmap(result, 0, 0, Math.min(size, endX), + Math.min(size, endY)); + } else { + return result; + } } public void recycle() { diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java index 5062c0e8e..aeed57771 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -25,12 +25,15 @@ import com.android.gallery3d.ui.PositionRepository.Position; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.RectF; import android.os.Message; import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.animation.AccelerateInterpolator; public class PhotoView extends GLView { @SuppressWarnings("unused") @@ -64,6 +67,14 @@ public class PhotoView extends GLView { private static final float SWIPE_THRESHOLD = 300f; private static final float DEFAULT_TEXT_SIZE = 20; + private static float TRANSITION_SCALE_FACTOR = 0.74f; + + // Used to calculate the scaling factor for the fading animation. + private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); + + // Used to calculate the alpha factor for the fading animation. + private AccelerateInterpolator mAlphaInterpolator = + new AccelerateInterpolator(0.9f); public interface PhotoTapListener { public void onSingleTapUp(int x, int y); @@ -99,6 +110,7 @@ public class PhotoView extends GLView { private Path mOpenedItemPath; private GalleryActivity mActivity; + private Point mImageCenter = new Point(); public PhotoView(GalleryActivity activity) { mActivity = activity; @@ -164,24 +176,49 @@ public class PhotoView extends GLView { mPhotoTapListener = listener; } - private boolean setTileViewPosition(int centerX, int centerY, float scale) { + private void setTileViewPosition(int centerX, int centerY, float scale) { + TileImageView t = mTileView; + + // Calculate the move-out progress value. + RectF bounds = mPositionController.getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int width = getWidth(); + float progress = calculateMoveOutProgress(left, right, width); + progress = Utils.clamp(progress, -1f, 1f); + + // We only want to apply the fading animation if the scrolling movement + // is to the right. + if (progress < 0) { + if (right - left < width) { + // If the picture is narrower than the view, keep it at the center + // of the view. + centerX = mPositionController.getImageWidth() / 2; + } else { + // If the picture is wider than the view (it's zoomed-in), keep + // the left edge of the object align the the left edge of the view. + centerX = Math.round(width / 2f / scale); + } + scale *= getScrollScale(progress); + t.setAlpha(getScrollAlpha(progress)); + } + + // set the position of the tile view int inverseX = mPositionController.getImageWidth() - centerX; int inverseY = mPositionController.getImageHeight() - centerY; - TileImageView t = mTileView; int rotation = mImageRotation; switch (rotation) { - case 0: return t.setPosition(centerX, centerY, scale, 0); - case 90: return t.setPosition(centerY, inverseX, scale, 90); - case 180: return t.setPosition(inverseX, inverseY, scale, 180); - case 270: return t.setPosition(inverseY, centerX, scale, 270); + case 0: t.setPosition(centerX, centerY, scale, 0); break; + case 90: t.setPosition(centerY, inverseX, scale, 90); break; + case 180: t.setPosition(inverseX, inverseY, scale, 180); break; + case 270: t.setPosition(inverseY, centerX, scale, 270); break; default: throw new IllegalArgumentException(String.valueOf(rotation)); } } public void setPosition(int centerX, int centerY, float scale) { - if (setTileViewPosition(centerX, centerY, scale)) { - layoutScreenNails(); - } + setTileViewPosition(centerX, centerY, scale); + layoutScreenNails(); } private void updateScreenNailEntry(int which, ImageData data) { @@ -217,6 +254,7 @@ public class PhotoView extends GLView { case 0: { // mImageWidth and mImageHeight will get updated mTileView.notifyModelInvalidated(); + mTileView.setAlpha(1.0f); mImageRotation = mModel.getImageRotation(); if (((mImageRotation / 90) & 1) == 0) { @@ -264,6 +302,7 @@ public class PhotoView extends GLView { if (mModel == null) { mTileView.notifyModelInvalidated(); + mTileView.setAlpha(1.0f); mImageRotation = 0; mPositionController.setImageSize(0, 0); updateLoadingState(); @@ -341,23 +380,40 @@ public class PhotoView extends GLView { @Override protected void render(GLCanvas canvas) { PositionController p = mPositionController; + boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT + && mTransitionMode != TRANS_SLIDE_IN_RIGHT + && mTransitionMode != TRANS_OPEN_ANIMATION); + + // Draw the next photo + if (drawScreenNail) { + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + if (nextNail.mVisible) nextNail.draw(canvas, true); + } // Draw the current photo if (mLoadingState == LOADING_COMPLETE) { super.render(canvas); } - // Draw the previous and the next photo - if (mTransitionMode != TRANS_SLIDE_IN_LEFT - && mTransitionMode != TRANS_SLIDE_IN_RIGHT - && mTransitionMode != TRANS_OPEN_ANIMATION) { - ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; - ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + // If the photo is loaded, draw the message/icon at the center of it, + // otherwise draw the message/icon at the center of the view. + if (mLoadingState == LOADING_COMPLETE) { + mTileView.getImageCenter(mImageCenter); + renderMessage(canvas, mImageCenter.x, mImageCenter.y); + } else { + renderMessage(canvas, getWidth() / 2, getHeight() / 2); + } - if (prevNail.mVisible) prevNail.draw(canvas); - if (nextNail.mVisible) nextNail.draw(canvas); + // Draw the previous photo + if (drawScreenNail) { + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + if (prevNail.mVisible) prevNail.draw(canvas, false); } + if (mPositionController.advanceAnimation()) invalidate(); + } + + private void renderMessage(GLCanvas canvas, int x, int y) { // Draw the progress spinner and the text below it // // (x, y) is where we put the center of the spinner. @@ -366,8 +422,6 @@ public class PhotoView extends GLView { // play icon is shown instead of the spinner. int w = getWidth(); int h = getHeight(); - int x = Math.round(mPositionController.getImageBounds().centerX()); - int y = h / 2; int s = Math.min(getWidth(), getHeight()) / 6; if (mLoadingState == LOADING_TIMEOUT) { @@ -387,8 +441,6 @@ public class PhotoView extends GLView { && mLoadingState != LOADING_TIMEOUT) { mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s); } - - if (mPositionController.advanceAnimation()) invalidate(); } private void stopCurrentSwipingIfNeeded() { @@ -731,23 +783,107 @@ public class PhotoView extends GLView { return mEnabled; } - public void draw(GLCanvas canvas) { - int x = mOffsetX; - int y = getHeight() / 2; + public void draw(GLCanvas canvas, boolean applyFadingAnimation) { + if (mTexture == null) return; - if (mTexture != null) { - if (mRotation != 0) { - canvas.save(GLCanvas.SAVE_FLAG_MATRIX); - canvas.translate(x, y, 0); - canvas.rotate(mRotation, 0, 0, 1); //mRotation - canvas.translate(-x, -y, 0); - } - mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, - mDrawWidth, mDrawHeight); - if (mRotation != 0) { - canvas.restore(); - } + int w = getWidth(); + int x = applyFadingAnimation ? w / 2 : mOffsetX; + int y = getHeight() / 2; + int flags = GLCanvas.SAVE_FLAG_MATRIX; + + if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA; + canvas.save(flags); + canvas.translate(x, y, 0); + if (applyFadingAnimation) { + float progress = (float) (x - mOffsetX) / w; + float alpha = getScrollAlpha(progress); + float scale = getScrollScale(progress); + canvas.multiplyAlpha(alpha); + canvas.scale(scale, scale, 1); } + if (mRotation != 0) { + canvas.rotate(mRotation, 0, 0, 1); + } + canvas.translate(-x, -y, 0); + mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, + mDrawWidth, mDrawHeight); + canvas.restore(); + } + } + + // Returns the scrolling progress value for an object moving out of a + // view. The progress value measures how much the object has moving out of + // the view. The object currently displays in [left, right), and the view is + // at [0, viewWidth]. + // + // The returned value is negative when the object is moving right, and + // positive when the object is moving left. The value goes to -1 or 1 when + // the object just moves out of the view completely. The value is 0 if the + // object currently fills the view. + private static float calculateMoveOutProgress(int left, int right, + int viewWidth) { + // w = object width + // viewWidth = view width + int w = right - left; + + // If the object width is smaller than the view width, + // |....view....| + // |<-->| progress = -1 when left = viewWidth + // |<-->| progress = 1 when left = -w + // So progress = 1 - 2 * (left + w) / (viewWidth + w) + if (w < viewWidth) { + return 1f - 2f * (left + w) / (viewWidth + w); + } + + // If the object width is larger than the view width, + // |..view..| + // |<--------->| progress = -1 when left = viewWidth + // |<--------->| progress = 0 between left = 0 + // |<--------->| and right = viewWidth + // |<--------->| progress = 1 when right = 0 + if (left > 0) { + return -left / (float) viewWidth; + } + + if (right < viewWidth) { + return (viewWidth - right) / (float) viewWidth; + } + + return 0; + } + + // Maps a scrolling progress value to the alpha factor in the fading + // animation. + private float getScrollAlpha(float scrollProgress) { + return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( + 1 - Math.abs(scrollProgress)) : 1.0f; + } + + // Maps a scrolling progress value to the scaling factor in the fading + // animation. + private float getScrollScale(float scrollProgress) { + float interpolatedProgress = mScaleInterpolator.getInterpolation( + Math.abs(scrollProgress)); + float scale = (1 - interpolatedProgress) + + interpolatedProgress * TRANSITION_SCALE_FACTOR; + return scale; + } + + + // This interpolator emulates the rate at which the perceived scale of an + // object changes as its distance from a camera increases. When this + // interpolator is applied to a scale animation on a view, it evokes the + // sense that the object is shrinking due to moving away from the camera. + private static class ZInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); } } diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java index 980f7b208..5c9f3f41c 100644 --- a/src/com/android/gallery3d/ui/TileImageView.java +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -17,6 +17,7 @@ package com.android.gallery3d.ui; import android.graphics.Bitmap; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; @@ -108,6 +109,7 @@ public class TileImageView extends GLView { protected int mCenterY; protected float mScale; protected int mRotation; + protected float mAlpha = 1.0f; // Temp variables to avoid memory allocation private final Rect mTileRange = new Rect(); @@ -125,8 +127,20 @@ public class TileImageView extends GLView { public int getImageWidth(); public int getImageHeight(); - // The method would be called in another thread - public Bitmap getTile(int level, int x, int y, int tileSize); + // 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). Then + // extend this intersection region by borderSize pixels on each side. If + // in extending the region, we found some part of the region are 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 in another thread. + public Bitmap getTile(int level, int x, int y, int tileSize, + int borderSize); public boolean isFailedToLoad(); } @@ -308,6 +322,30 @@ public class TileImageView extends GLView { out.set(left, top, right, bottom); } + // Calculate where the center of the image is, in the view coordinates. + public void getImageCenter(Point center) { + // The width and height of this view. + int viewW = getWidth(); + int viewH = getHeight(); + + // The distance between the center of the view to the center of the + // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap + // coordinates correspond to the center of view) + int distW, distH; + if (mRotation % 180 == 0) { + distW = mImageWidth / 2 - mCenterX; + distH = mImageHeight / 2 - mCenterY; + } else { + distW = mImageHeight / 2 - mCenterY; + distH = mImageWidth / 2 - mCenterX; + } + + // Convert to view coordinates. mScale translates from bitmap units to + // view units. + center.x = Math.round(viewW / 2f + distW * mScale); + center.y = Math.round(viewH / 2f + distH * mScale); + } + public boolean setPosition(int centerX, int centerY, float scale, int rotation) { if (mCenterX == centerX && mCenterY == centerY && mScale == scale) return false; @@ -320,6 +358,13 @@ public class TileImageView extends GLView { return true; } + public boolean setAlpha(float alpha) { + if (mAlpha == alpha) return false; + mAlpha = alpha; + invalidate(); + return true; + } + public void freeTextures() { mIsTextureFreed = true; @@ -365,13 +410,19 @@ public class TileImageView extends GLView { int level = mLevel; int rotation = mRotation; - - if (rotation != 0) { - canvas.save(GLCanvas.SAVE_FLAG_MATRIX); - int centerX = getWidth() / 2, centerY = getHeight() / 2; - canvas.translate(centerX, centerY, 0); - canvas.rotate(rotation, 0, 0, 1); - canvas.translate(-centerX, -centerY, 0); + int flags = 0; + if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX; + if (mAlpha != 1.0f) flags |= GLCanvas.SAVE_FLAG_ALPHA; + + if (flags != 0) { + canvas.save(flags); + if (rotation != 0) { + int centerX = getWidth() / 2, centerY = getHeight() / 2; + canvas.translate(centerX, centerY, 0); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY, 0); + } + if (mAlpha != 1.0f) canvas.multiplyAlpha(mAlpha); } try { if (level != mLevelCount) { @@ -392,7 +443,7 @@ public class TileImageView extends GLView { Math.round(mImageHeight * mScale)); } } finally { - if (rotation != 0) canvas.restore(); + if (flags != 0) canvas.restore(); } if (mRenderComplete) { @@ -601,11 +652,9 @@ public class TileImageView extends GLView { boolean decode() { // Get a tile from the original image. The tile is down-scaled // by (1 << mTilelevel) from a region in the original image. - int tileLength = (TILE_SIZE + 2 * TILE_BORDER); - int borderLength = TILE_BORDER << mTileLevel; try { mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile( - mTileLevel, mX - borderLength, mY - borderLength, tileLength)); + mTileLevel, mX, mY, TILE_SIZE, TILE_BORDER)); } catch (Throwable t) { Log.w(TAG, "fail to decode tile", t); } @@ -621,6 +670,20 @@ public class TileImageView extends GLView { return bitmap; } + // We override getTextureWidth() and getTextureHeight() here, so the + // texture can be re-used for different tiles regardless of the actual + // size of the tile (which may be small because it is a tile at the + // boundary). + @Override + public int getTextureWidth() { + return TILE_SIZE + TILE_BORDER * 2; + } + + @Override + public int getTextureHeight() { + return TILE_SIZE + TILE_BORDER * 2; + } + public void update(int x, int y, int level) { mX = x; mY = y; diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java index 63bb0b2f7..be255d28b 100644 --- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -34,9 +34,6 @@ public class TileImageViewAdapter implements TileImageView.Model { protected int mLevelCount; protected boolean mFailedToLoad; - private final Rect mIntersectRect = new Rect(); - private final Rect mRegionRect = new Rect(); - public TileImageViewAdapter() { } @@ -80,16 +77,24 @@ public class TileImageViewAdapter implements TileImageView.Model { } @Override - public synchronized Bitmap getTile(int level, int x, int y, int length) { + public synchronized Bitmap getTile(int level, int x, int y, int tileSize, + int borderSize) { if (mRegionDecoder == null) return null; - Rect region = mRegionRect; - Rect intersectRect = mIntersectRect; - region.set(x, y, x + (length << level), y + (length << level)); - intersectRect.set(0, 0, mImageWidth, mImageHeight); + // wantRegion is the rectangle on the original image we want. askRegion + // is the rectangle on the original image that we will ask from + // mRegionDecoder. Both are in the coordinates of the original image, + // not the coordinates of the scaled-down images. + Rect wantRegion = new Rect(); + Rect askRegion = new Rect(); + + int b = borderSize << level; + wantRegion.set(x - b, y - b, x + (tileSize << level) + b, + y + (tileSize << level) + b); - // Get the intersected rect of the requested region and the image. - Utils.assertTrue(intersectRect.intersect(region)); + // askRegion is the intersection of wantRegion and the original image. + askRegion.set(0, 0, mImageWidth, mImageHeight); + Utils.assertTrue(askRegion.intersect(wantRegion)); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Config.ARGB_8888; @@ -100,25 +105,37 @@ public class TileImageViewAdapter implements TileImageView.Model { // In CropImage, we may call the decodeRegion() concurrently. synchronized (mRegionDecoder) { - bitmap = mRegionDecoder.decodeRegion(intersectRect, options); + bitmap = mRegionDecoder.decodeRegion(askRegion, options); } - // The returned region may not match with the targetLength. - // If so, we fill black pixels on it. - if (intersectRect.equals(region)) return bitmap; - if (bitmap == null) { Log.w(TAG, "fail in decoding region"); return null; } - Bitmap tile = Bitmap.createBitmap(length, length, Config.ARGB_8888); - Canvas canvas = new Canvas(tile); - canvas.drawBitmap(bitmap, - (intersectRect.left - region.left) >> level, - (intersectRect.top - region.top) >> level, null); + if (wantRegion.equals(askRegion)) return bitmap; + + // Now the wantRegion does not match the askRegion. This means we are at + // a boundary tile, and we need to add paddings. Create a new Bitmap + // and copy over. + int size = tileSize + 2 * borderSize; + Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + int offsetX = (askRegion.left - wantRegion.left) >> level; + int offsetY = (askRegion.top - wantRegion.top) >> level; + canvas.drawBitmap(bitmap, offsetX, offsetY, null); + + // If the valid region (covered by bitmap or border) is smaller than the + // result bitmap, subset it. + int endX = offsetX + bitmap.getWidth() + borderSize; + int endY = offsetY + bitmap.getHeight() + borderSize; bitmap.recycle(); - return tile; + if (endX < size || endY < size) { + return Bitmap.createBitmap(result, 0, 0, Math.min(size, endX), + Math.min(size, endY)); + } else { + return result; + } } @Override diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java index b2b8cd514..177704895 100644 --- a/src/com/android/gallery3d/ui/UploadedTexture.java +++ b/src/com/android/gallery3d/ui/UploadedTexture.java @@ -161,6 +161,8 @@ abstract class UploadedTexture extends BasicTexture { protected void invalidateContent() { if (mBitmap != null) freeBitmap(); mContentValid = false; + mWidth = UNSPECIFIED; + mHeight = UNSPECIFIED; } /** -- 2.11.0