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")
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);
private Path mOpenedItemPath;
private GalleryActivity mActivity;
+ private Point mImageCenter = new Point();
public PhotoView(GalleryActivity activity) {
mActivity = activity;
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) {
case 0: {
// mImageWidth and mImageHeight will get updated
mTileView.notifyModelInvalidated();
+ mTileView.setAlpha(1.0f);
mImageRotation = mModel.getImageRotation();
if (((mImageRotation / 90) & 1) == 0) {
if (mModel == null) {
mTileView.notifyModelInvalidated();
+ mTileView.setAlpha(1.0f);
mImageRotation = 0;
mPositionController.setImageSize(0, 0);
updateLoadingState();
@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.
// 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) {
&& mLoadingState != LOADING_TIMEOUT) {
mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
}
-
- if (mPositionController.advanceAnimation()) invalidate();
}
private void stopCurrentSwipingIfNeeded() {
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));
}
}
package com.android.gallery3d.ui;
import android.graphics.Bitmap;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
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();
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();
}
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;
return true;
}
+ public boolean setAlpha(float alpha) {
+ if (mAlpha == alpha) return false;
+ mAlpha = alpha;
+ invalidate();
+ return true;
+ }
+
public void freeTextures() {
mIsTextureFreed = true;
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) {
Math.round(mImageHeight * mScale));
}
} finally {
- if (rotation != 0) canvas.restore();
+ if (flags != 0) canvas.restore();
}
if (mRenderComplete) {
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);
}
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;
protected int mLevelCount;
protected boolean mFailedToLoad;
- private final Rect mIntersectRect = new Rect();
- private final Rect mRegionRect = new Rect();
-
public TileImageViewAdapter() {
}
}
@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;
// 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