Add animation for ImageViewer.
Change-Id: If8e94d3d18a3fb8d867dc12bd0fcc17d2f95282b
diff --git a/new3d/src/com/android/gallery3d/ui/DownUpDetector.java b/new3d/src/com/android/gallery3d/ui/DownUpDetector.java
new file mode 100644
index 0000000..fbfec0a
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/DownUpDetector.java
@@ -0,0 +1,45 @@
+package com.android.gallery3d.ui;
+
+import android.view.MotionEvent;
+
+public class DownUpDetector {
+ public interface DownUpListener {
+ void onDown(MotionEvent e);
+ void onUp(MotionEvent e);
+ }
+
+ private boolean mStillDown;
+ private DownUpListener mListener;
+
+ public DownUpDetector(DownUpListener listener) {
+ mListener = listener;
+ }
+
+ private void setState(boolean down, MotionEvent e) {
+ if (down == mStillDown) return;
+ mStillDown = down;
+ if (down) {
+ mListener.onDown(e);
+ } else {
+ mListener.onUp(e);
+ }
+ }
+
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort.
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/ImageViewer.java b/new3d/src/com/android/gallery3d/ui/ImageViewer.java
index 35b70df..a4a7342 100644
--- a/new3d/src/com/android/gallery3d/ui/ImageViewer.java
+++ b/new3d/src/com/android/gallery3d/ui/ImageViewer.java
@@ -6,6 +6,7 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Bitmap.Config;
+import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
@@ -22,7 +23,7 @@
// texture to avoid seams between tiles.
private static final int TILE_SIZE = 254;
private static final int TILE_BORDER = 1;
- private static final int UPLOAD_LIMIT = 4;
+ private static final int UPLOAD_LIMIT = 1;
private final Bitmap mScaledBitmaps[];
private final BitmapTexture mBackupTexture;
@@ -54,6 +55,7 @@
private final ScaleGestureDetector mScaleDetector;
private final GestureDetector mGestureDetector;
+ private final DownUpDetector mDownUpDetector;
private final HashMap<Long, Tile> mActiveTiles = new HashMap<Long, Tile>();
private Iterator<Tile> mUploadIter;
@@ -69,6 +71,7 @@
private final Rect mActiveRange[] = {new Rect(), new Rect()};
private final Uploader mUploader = new Uploader();
+ private final AnimationController mAnimationController;
public ImageViewer(Context context, Bitmap scaledBitmaps[], Bitmap backup) {
mScaledBitmaps = scaledBitmaps;
@@ -77,30 +80,39 @@
mImageWidth = scaledBitmaps[0].getWidth();
mImageHeight = scaledBitmaps[0].getHeight();
- setPosition(mImageWidth / 2, mImageHeight / 2, 0.5f);
+
+ mAnimationController = new AnimationController(this,
+ mImageWidth, mImageHeight);
mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());
- mGestureDetector = new GestureDetector(context, new MyGestureListener());
+ mGestureDetector = new GestureDetector(context,
+ new MyGestureListener(),
+ null /* handler */,
+ true /* ignoreMultitouch */);
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
}
@Override
protected boolean onTouch(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
return true;
}
private static int ceilLog2(float value) {
int i;
for (i = 0; i < 30; i++) {
- if ((1 << i) > value) break;
+ if ((1 << i) >= value) break;
}
return i;
}
@Override
protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
- if (changeSize) layoutTiles(mCenterX, mCenterY, mScale);
+ if (changeSize) {
+ mAnimationController.updateViewSize(getWidth(), getHeight());
+ }
}
// Prepare the tiles we want to use for display.
@@ -189,8 +201,8 @@
int width = getWidth();
int height = getHeight();
- int left = Math.round(cX - width / (2f * scale));
- int top = Math.round(cY - height / (2f * scale));
+ int left = (int) Math.floor(cX - width / (2f * scale));
+ int top = (int) Math.floor(cY - height / (2f * scale));
int right = (int) Math.ceil(left + width / scale);
int bottom = (int) Math.ceil(top + height / scale);
@@ -205,10 +217,6 @@
}
public void setPosition(int centerX, int centerY, float scale) {
- if (centerX == mCenterX && centerY == mCenterY && scale == mScale) {
- return;
- }
-
mCenterX = centerX;
mCenterY = centerY;
mScale = scale;
@@ -217,6 +225,259 @@
invalidate();
}
+ private static class AnimationController {
+ private long mAnimationStartTime = NO_ANIMATION;
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ // Animation time in milliseconds.
+ private static final float ANIM_TIME_SCROLL = 0;
+ private static final float ANIM_TIME_SCALE = 50;
+ private static final float ANIM_TIME_SNAPBACK = 600;
+
+ private int mAnimationKind;
+ private final static int ANIM_KIND_SCROLL = 0;
+ private final static int ANIM_KIND_SCALE = 1;
+ private final static int ANIM_KIND_SNAPBACK = 2;
+
+ private ImageViewer mViewer;
+ private int mImageW, mImageH;
+ private int mViewW, mViewH;
+
+ // The X, Y are the coordinate on bitmap which shows on the center of
+ // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual
+ // values used currently.
+ private int mCurrentX, mFromX, mToX;
+ private int mCurrentY, mFromY, mToY;
+ private float mCurrentScale, mFromScale, mToScale;
+
+ // The offsets from the center of the view to the user's focus point,
+ // converted to the bitmap domain.
+ private float mPrevOffsetX;
+ private float mPrevOffsetY;
+ private boolean mInScale;
+
+ // The limits for position and scale.
+ private float mScaleMin = 0.25f, mScaleMax = 4f;
+
+ AnimationController(ImageViewer viewer, int imageW, int imageH) {
+ mViewer = viewer;
+ mImageW = imageW;
+ mImageH = imageH;
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = 0.5f;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+
+ public void updateViewSize(int viewW, int viewH) {
+ mViewW = viewW;
+ mViewH = viewH;
+ mScaleMin = Math.min((float) viewW / mImageW, (float) viewH / mImageH);
+ mScaleMin = Math.min(1f, mScaleMin);
+ mCurrentScale = Util.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+
+ public void scrollBy(float dx, float dy) {
+ startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
+ getTargetY() + Math.round(dy / mCurrentScale),
+ mCurrentScale,
+ ANIM_KIND_SCROLL);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ mInScale = true;
+ mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale;
+ }
+
+ public void scaleBy(float s, float focusX, float focusY) {
+ // The focus point should keep this position on the ImageView.
+ // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX.
+ // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY.
+ float offsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ float offsetY = (focusY - mViewH / 2f) / mCurrentScale;
+ startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX),
+ getTargetY() - Math.round(offsetY - mPrevOffsetY),
+ getTargetScale() * s,
+ ANIM_KIND_SCALE);
+ mPrevOffsetX = offsetX;
+ mPrevOffsetY = offsetY;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ startSnapbackIfNeeded();
+ }
+
+ public void up() {
+ startSnapbackIfNeeded();
+ }
+
+ private void startAnimation(int centerX, int centerY, float scale, int kind) {
+ if (centerX == mCurrentX && centerY == mCurrentY
+ && scale == mCurrentScale) {
+ return;
+ }
+
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+
+ mToX = centerX;
+ mToY = centerY;
+ mToScale = Util.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
+
+ // If the scaled dimension is smaller than the view,
+ // force it to be in the center.
+ if (mImageW * mToScale < mViewW) {
+ mToX = mImageW / 2;
+ }
+ if (mImageH * mToScale < mViewH) {
+ mToY = mImageH / 2;
+ }
+
+ mAnimationStartTime = SystemClock.uptimeMillis();
+ mAnimationKind = kind;
+ advanceAnimation();
+ }
+
+ // Returns true if redraw is needed.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ } else if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ return startSnapbackIfNeeded();
+ }
+
+ float animationTime;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ animationTime = ANIM_TIME_SCROLL;
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ animationTime = ANIM_TIME_SCALE;
+ } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ {
+ animationTime = ANIM_TIME_SNAPBACK;
+ }
+
+ float progress;
+ if (animationTime == 0) {
+ progress = 1;
+ } else {
+ long now = SystemClock.uptimeMillis();
+ progress = (now - mAnimationStartTime) / animationTime;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ mAnimationStartTime = LAST_ANIMATION;
+ } else {
+ float f = 1 - progress;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ f = 1 - f; // linear
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ f = 1 - f * f; // quadratic
+ } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK */ {
+ f = 1 - f * f * f * f * f; // x^5
+ }
+ mCurrentX = Math.round(mFromX + f * (mToX - mFromX));
+ mCurrentY = Math.round(mFromY + f * (mToY - mFromY));
+ mCurrentScale = mFromScale + f * (mToScale - mFromScale);
+ }
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ return true;
+ }
+
+ // Returns true if redraw is needed.
+ private boolean startSnapbackIfNeeded() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mInScale) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
+ return false;
+ }
+
+ boolean needAnimation = false;
+ int x = mCurrentX;
+ int y = mCurrentY;
+ float scale = mCurrentScale;
+
+ if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
+ needAnimation = true;
+ scale = Util.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ }
+
+ // The number of pixels when the edge is aliged.
+ int left = (int) Math.ceil(mViewW / (2 * scale));
+ int right = mImageW - left;
+ int top = (int) Math.ceil(mViewH / (2 * scale));
+ int bottom = mImageH - top;
+
+ if (mImageW * scale > mViewW) {
+ if (mCurrentX < left) {
+ needAnimation = true;
+ x = left;
+ } else if (mCurrentX > right) {
+ needAnimation = true;
+ x = right;
+ }
+ } else {
+ if (mCurrentX > left) {
+ needAnimation = true;
+ x = left;
+ } else if (mCurrentX < right) {
+ needAnimation = true;
+ x = right;
+ }
+ }
+
+ if (mImageH * scale > mViewH) {
+ if (mCurrentY < top) {
+ needAnimation = true;
+ y = top;
+ } else if (mCurrentY > bottom) {
+ needAnimation = true;
+ y = bottom;
+ }
+ } else {
+ if (mCurrentY > top) {
+ needAnimation = true;
+ y = top;
+ } else if (mCurrentY < bottom) {
+ needAnimation = true;
+ y = bottom;
+ }
+ }
+
+ if (needAnimation) {
+ startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
+ }
+
+ return needAnimation;
+ }
+
+ private float getTargetScale() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
+ return mToScale;
+ }
+
+ private int getTargetX() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
+ return mToX;
+ }
+
+ private int getTargetY() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
+ return mToY;
+ }
+ }
+
public void close() {
mUploadIter = null;
GLCanvas canvas = getGLRootView().getCanvas();
@@ -251,6 +512,11 @@
}
}
}
+
+ if (mAnimationController.advanceAnimation()) {
+ mRenderComplete = false;
+ }
+
if (mRenderComplete) {
if (mUploadIter.hasNext() && !mUploader.mActive) {
mUploader.mActive = true;
@@ -448,8 +714,7 @@
@Override
public boolean onScroll(
MotionEvent e1, MotionEvent e2, float dx, float dy) {
- setPosition((int) (mCenterX + dx / mScale),
- (int) (mCenterY + dy / mScale), mScale);
+ mAnimationController.scrollBy(dx, dy);
return true;
}
}
@@ -457,35 +722,38 @@
private class MyScaleListener
extends ScaleGestureDetector.SimpleOnScaleGestureListener {
- // The offsets of the focus point to the center of the image on the
- // image domain.
- private float mPrevOffsetX;
- private float mPrevOffsetY;
-
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
if (Float.isNaN(scale) || Float.isInfinite(scale)) return true;
- scale = Util.clamp(scale * mScale, 0.02f, 2);
-
- // The focus point should keep this position on the ImageView.
- // So, mCenterX + mPrevOffsetX = mCenterX' + offsetX.
- // mCenterY + mPrevOffsetY = mCenterY' + offsetY.
- float offsetX = (detector.getFocusX() - getWidth() / 2) / scale;
- float offsetY = (detector.getFocusY() - getHeight() / 2) / scale;
- setPosition((int) (mCenterX - (offsetX - mPrevOffsetX) + 0.5),
- (int) (mCenterY - (offsetY - mPrevOffsetY) + 0.5), scale);
- mPrevOffsetX = offsetX;
- mPrevOffsetY = offsetY;
-
+ mAnimationController.scaleBy(scale,
+ detector.getFocusX(), detector.getFocusY());
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
- mPrevOffsetX = (detector.getFocusX() - getWidth() / 2) / mScale;
- mPrevOffsetY = (detector.getFocusY() - getHeight() / 2) / mScale;
+ mAnimationController.beginScale(
+ detector.getFocusX(), detector.getFocusY());
return true;
}
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mAnimationController.endScale();
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ public void onDown(MotionEvent e) {
+ }
+
+ public void onUp(MotionEvent e) {
+ mAnimationController.up();
+ }
+ }
+
+ private boolean isDown() {
+ return mDownUpDetector.isDown();
}
}