Loading src/com/android/gallery3d/ui/FlingScroller.java 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.ui; import com.android.gallery3d.common.Utils; // This is a customized version of Scroller, with a interface similar to // android.widget.Scroller. It does fling only, not scroll. // // The differences between the this Scroller and the system one are: // // (1) The velocity does not change because of min/max limit. // (2) The duration is different. // (3) The deceleration curve is different. class FlingScroller { private static final String TAG = "FlingController"; // The fling duration (in milliseconds) when velocity is 1 pixel/second private static final float FLING_DURATION_PARAM = 50f; private static final int DECELERATED_FACTOR = 4; private int mStartX, mStartY; private int mMinX, mMinY, mMaxX, mMaxY; private double mSinAngle; private double mCosAngle; private int mDuration; private int mDistance; private int mFinalX, mFinalY; private int mCurrX, mCurrY; public int getFinalX() { return mFinalX; } public int getFinalY() { return mFinalY; } public int getDuration() { return mDuration; } public int getCurrX() { return mCurrX; } public int getCurrY() { return mCurrY; } public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { mStartX = startX; mStartY = startY; mMinX = minX; mMinY = minY; mMaxX = maxX; mMaxY = maxY; double velocity = Math.hypot(velocityX, velocityY); mSinAngle = velocityY / velocity; mCosAngle = velocityX / velocity; // // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d) // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T // Thus, // v0 = d * (e - s) / T => (e - s) = v0 * T / d // // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second; mDuration = (int)Math.round(FLING_DURATION_PARAM * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1))); // (e - s) = v0 * T / d mDistance = (int)Math.round( velocity * mDuration / DECELERATED_FACTOR / 1000); mFinalX = getX(1.0f); mFinalY = getY(1.0f); } public void computeScrollOffset(float progress) { progress = Math.min(progress, 1); float f = 1 - progress; f = 1 - (float) Math.pow(f, DECELERATED_FACTOR); mCurrX = getX(f); mCurrY = getY(f); } private int getX(float f) { return (int) Utils.clamp( Math.round(mStartX + f * mDistance * mCosAngle), mMinX, mMaxX); } private int getY(float f) { return (int) Utils.clamp( Math.round(mStartY + f * mDistance * mSinAngle), mMinY, mMaxY); } } src/com/android/gallery3d/ui/PhotoView.java +8 −5 Original line number Diff line number Diff line Loading @@ -145,7 +145,7 @@ public class PhotoView extends GLView { mScreenNails[i] = new ScreenNailEntry(); } mPositionController = new PositionController(this); mPositionController = new PositionController(this, context); mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); } Loading Loading @@ -437,7 +437,7 @@ public class PhotoView extends GLView { if (mTransitionMode != TRANS_NONE) return false; // Decide whether to swiping to the next/prev image in the zoom-in case RectF bounds = mPositionController.getImageBounds(); RectF bounds = controller.getImageBounds(); int left = Math.round(bounds.left); int right = Math.round(bounds.right); int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); Loading Loading @@ -480,9 +480,12 @@ public class PhotoView extends GLView { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (swipeImages(velocityX)) { mIgnoreUpEvent = true; } else if (mTransitionMode != TRANS_NONE) { // do nothing } else if (mPositionController.fling(velocityX, velocityY)) { mIgnoreUpEvent = true; if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) { mPositionController.up(); } return true; } Loading src/com/android/gallery3d/ui/PositionController.java +142 −104 Original line number Diff line number Diff line Loading @@ -31,18 +31,22 @@ import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.Scroller; class PositionController { private static final String TAG = "PositionController"; private long mAnimationStartTime = NO_ANIMATION; private static final long NO_ANIMATION = -1; private static final long LAST_ANIMATION = -2; private int mAnimationKind; private float mAnimationDuration; 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 final static int ANIM_KIND_SLIDE = 3; private final static int ANIM_KIND_ZOOM = 4; private final static int ANIM_KIND_FLING = 5; // Animation time in milliseconds. The order must match ANIM_KIND_* above. private final static int ANIM_TIME[] = { Loading @@ -51,6 +55,7 @@ class PositionController { 600, // ANIM_KIND_SNAPBACK 400, // ANIM_KIND_SLIDE 300, // ANIM_KIND_ZOOM 0, // ANIM_KIND_FLING (the duration is calculated dynamically) }; // We try to scale up the image to fill the screen. But in order not to Loading @@ -69,13 +74,20 @@ class PositionController { private float mCurrentScale, mFromScale, mToScale; // The focus point of the scaling gesture (in bitmap coordinates). private float mFocusBitmapX; private float mFocusBitmapY; private int mFocusBitmapX; private int mFocusBitmapY; private boolean mInScale; // The minimum and maximum scale we allow. private float mScaleMin, mScaleMax = SCALE_LIMIT; // This is used by the fling animation private FlingScroller mScroller; // The bound of the stable region, see the comments above // calculateStableBound() for details. private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; // Assume the image size is the same as view size before we know the actual // size of image. private boolean mUseViewSize = true; Loading @@ -83,8 +95,9 @@ class PositionController { private RectF mTempRect = new RectF(); private float[] mTempPoints = new float[8]; public PositionController(PhotoView viewer) { public PositionController(PhotoView viewer, Context context) { mViewer = viewer; mScroller = new FlingScroller(); } public void setImageSize(int width, int height) { Loading Loading @@ -120,6 +133,9 @@ class PositionController { mToY = translate(mToY, mImageH, height, ratio); mToScale = mToScale * ratio; mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio); mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio); mImageW = width; mImageH = height; Loading @@ -144,28 +160,14 @@ class PositionController { public void zoomIn(float tapX, float tapY, float targetScale) { if (targetScale > mScaleMax) targetScale = mScaleMax; float scale = mCurrentScale; // Convert the tap position to image coordinate float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX; float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY; // We want to make sure that after zoom-in, we don't see black regions // because we zoom too close to the border. The conditions are: // // (mViewW / 2) / targetScale + mCurrentX < mImageW // -(mViewW / 2) / targetScale + mCurrentX > 0 float min = mViewW / 2.0f / targetScale; float max = mImageW - mViewW / 2.0f / targetScale; int targetX = (int) Utils.clamp(tempX, min, max); min = mViewH / 2.0f / targetScale; max = mImageH - mViewH / 2.0f / targetScale; int targetY = (int) Utils.clamp(tempY, min, max); int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX); int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY); // If the width of the image is less then the view, center the image if (mImageW * targetScale < mViewW) targetX = mImageW / 2; if (mImageH * targetScale < mViewH) targetY = mImageH / 2; calculateStableBound(targetScale); int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight); int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom); startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); } Loading @@ -179,37 +181,31 @@ class PositionController { Math.min((float) mViewW / w, (float) mViewH / h)); } // Translate the coordinate if the aspect ratio of the image changes. // When the user slides a image before it's loaded, we don't know the // actual aspect ratio, so we will assume one. When we receive the actual // aspect ratio, we need to translate the coordinate from the old (assumed) // bitmap into the new (actual) bitmap. // Translate a coordinate on bitmap if the bitmap size changes. // If the aspect ratio doesn't change, it's easy: // // +-------------------------+ "o" is where center of the view // | +--------+ | is. mCurrent{X,Y} is the coordinate of // | | | | "o" relative to the old bitmap. Assume // | | o | | the old bitmap size is (w, h). The new // | +--------+ | bitmap size is (w', h'). First we adjust // | | mCurrentScale by factor r = min(w/w', // +-------------------------+ h/h'), so one of the sides matches the old // | bitmap (w'*r == w or h'*r == h). // v // +-------------------------+ Then we put the new scaled bitmap to the // | +--+ .......... | center of the original bitmap's bounding // | | | . . | box. The center of the old bitmap and the // | | | . o . | new bitmap must match in view coordinate: // | +--+ .......... | // | | (w/2 - mCurrentX) * mCurrentScale = // +-------------------------+ (w'/2 - mCurrentX') * mCurrentScale * r // | // v Solve for mCurrentX' we have: // +-------------------------+ // | ...+--+... | mCurrentX' = w'/2 + (mCurrentX - w/2) / r // | . | | . | // | . o| | . | // | ...+--+... | // | | // +-------------------------+ // r = w / w' (= h / h') // x' = x / r // y' = y / r // // However the aspect ratio may change. That happens when the user slides // a image before it's loaded, we don't know the actual aspect ratio, so // we will assume one. When we receive the actual bitmap size, we need to // translate the coordinate from the old bitmap into the new bitmap. // // What we want to do is center the bitmap at the original position. // // ...+--+... // . | | . // . | | . // ...+--+... // // First we scale down the new bitmap by a factor r = min(w/w', h/h'). // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of // the old bitmap maps to (x', y') in the new bitmap, where // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r private static int translate(int value, int size, int newSize, float ratio) { return Math.round(newSize / 2f + (value - size / 2f) / ratio); } Loading Loading @@ -261,8 +257,10 @@ class PositionController { public void beginScale(float focusX, float focusY) { mInScale = true; mFocusBitmapX = mCurrentX + (focusX - mViewW / 2f) / mCurrentScale; mFocusBitmapY = mCurrentY + (focusY - mViewH / 2f) / mCurrentScale; mFocusBitmapX = Math.round(mCurrentX + (focusX - mViewW / 2f) / mCurrentScale); mFocusBitmapY = Math.round(mCurrentY + (focusY - mViewH / 2f) / mCurrentScale); } public void scaleBy(float s, float focusX, float focusY) { Loading Loading @@ -337,27 +335,51 @@ class PositionController { mCurrentScale, type); } public boolean fling(float velocityX, float velocityY) { // We only want to do fling when the picture is zoomed-in. if (mImageW * mCurrentScale <= mViewW && mImageH * mCurrentScale <= mViewH) { return false; } calculateStableBound(mCurrentScale); mScroller.fling(mCurrentX, mCurrentY, Math.round(-velocityX / mCurrentScale), Math.round(-velocityY / mCurrentScale), mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); int targetX = mScroller.getFinalX(); int targetY = mScroller.getFinalY(); mAnimationDuration = mScroller.getDuration(); startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING); return true; } private void startAnimation( int centerX, int centerY, float scale, int kind) { if (centerX == mCurrentX && centerY == mCurrentY int targetX, int targetY, float scale, int kind) { if (targetX == mCurrentX && targetY == mCurrentY && scale == mCurrentScale) return; mFromX = mCurrentX; mFromY = mCurrentY; mFromScale = mCurrentScale; mToX = centerX; mToY = centerY; mToX = targetX; mToY = targetY; mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); // If the scaled dimension is smaller than the view, // If the scaled height is smaller than the view height, // force it to be in the center. // (We do for height only, not width, because the user may // want to scroll to the previous/next image.) if (Math.floor(mImageH * mToScale) <= mViewH) { mToY = mImageH / 2; } mAnimationStartTime = SystemClock.uptimeMillis(); mAnimationKind = kind; if (mAnimationKind != ANIM_KIND_FLING) { mAnimationDuration = ANIM_TIME[mAnimationKind]; } if (advanceAnimation()) mViewer.invalidate(); } Loading @@ -375,13 +397,12 @@ class PositionController { } } float animationTime = ANIM_TIME[mAnimationKind]; long now = SystemClock.uptimeMillis(); float progress; if (animationTime == 0) { if (mAnimationDuration == 0) { progress = 1; } else { long now = SystemClock.uptimeMillis(); progress = (now - mAnimationStartTime) / animationTime; progress = (now - mAnimationStartTime) / mAnimationDuration; } if (progress >= 1) { Loading @@ -394,6 +415,7 @@ class PositionController { float f = 1 - progress; switch (mAnimationKind) { case ANIM_KIND_SCROLL: case ANIM_KIND_FLING: progress = 1 - f; // linear break; case ANIM_KIND_SCALE: Loading @@ -405,12 +427,23 @@ class PositionController { progress = 1 - f * f * f * f * f; // x^5 break; } if (mAnimationKind == ANIM_KIND_FLING) { flingInterpolate(progress); } else { linearInterpolate(progress); } } mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); return true; } private void flingInterpolate(float progress) { mScroller.computeScrollOffset(progress); mCurrentX = mScroller.getCurrX(); mCurrentY = mScroller.getCurrY(); mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); } // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1]. private void linearInterpolate(float progress) { // To linearly interpolate the position on view coordinates, we do the Loading Loading @@ -452,8 +485,6 @@ class PositionController { public boolean startSnapback() { boolean needAnimation = false; int x = mCurrentX; int y = mCurrentY; float scale = mCurrentScale; if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { Loading @@ -461,62 +492,69 @@ class PositionController { scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); } // The number of pixels between the center of the view // and the edge when the edge is aligned. int left = (int) Math.ceil(mViewW / (2 * scale)); int right = mImageW - left; int top = (int) Math.ceil(mViewH / (2 * scale)); int bottom = mImageH - top; calculateStableBound(scale); int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight); int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom); if (mImageW * scale > mViewW) { if (mCurrentX < left) { needAnimation = true; x = left; } else if (mCurrentX > right) { if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) { needAnimation = true; x = right; } } else if (mCurrentX != mImageW / 2) { needAnimation = true; x = mImageW / 2; if (needAnimation) { startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); } if (mImageH * scale > mViewH) { if (mCurrentY < top) { needAnimation = true; y = top; } else if (mCurrentY > bottom) { needAnimation = true; y = bottom; return needAnimation; } } else if (mCurrentY != mImageH / 2) { needAnimation = true; y = mImageH / 2; // Calculates the stable region of mCurrent{X/Y}, where "stable" means // // (1) If the dimension of scaled image >= view dimension, we will not // see black region outside the image (at that dimension). // (2) If the dimension of scaled image < view dimension, we will center // the scaled image. // // We might temporarily go out of this stable during user interaction, // but will "snap back" after user stops interaction. // // The results are stored in mBound{Left/Right/Top/Bottom}. // private void calculateStableBound(float scale) { // The number of pixels between the center of the view // and the edge when the edge is aligned. mBoundLeft = (int) Math.ceil(mViewW / (2 * scale)); mBoundRight = mImageW - mBoundLeft; mBoundTop = (int) Math.ceil(mViewH / (2 * scale)); mBoundBottom = mImageH - mBoundTop; // If the scaled height is smaller than the view height, // force it to be in the center. if (Math.floor(mImageH * scale) <= mViewH) { mBoundTop = mBoundBottom = mImageH / 2; } if (needAnimation) { startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); // Same for width if (Math.floor(mImageW * scale) <= mViewW) { mBoundLeft = mBoundRight = mImageW / 2; } } return needAnimation; private boolean useCurrentValueAsTarget() { return mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK || mAnimationKind == ANIM_KIND_FLING; } private float getTargetScale() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale; return mToScale; return useCurrentValueAsTarget() ? mCurrentScale : mToScale; } private int getTargetX() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX; return mToX; return useCurrentValueAsTarget() ? mCurrentX : mToX; } private int getTargetY() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY; return mToY; return useCurrentValueAsTarget() ? mCurrentY : mToY; } public RectF getImageBounds() { Loading Loading
src/com/android/gallery3d/ui/FlingScroller.java 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.ui; import com.android.gallery3d.common.Utils; // This is a customized version of Scroller, with a interface similar to // android.widget.Scroller. It does fling only, not scroll. // // The differences between the this Scroller and the system one are: // // (1) The velocity does not change because of min/max limit. // (2) The duration is different. // (3) The deceleration curve is different. class FlingScroller { private static final String TAG = "FlingController"; // The fling duration (in milliseconds) when velocity is 1 pixel/second private static final float FLING_DURATION_PARAM = 50f; private static final int DECELERATED_FACTOR = 4; private int mStartX, mStartY; private int mMinX, mMinY, mMaxX, mMaxY; private double mSinAngle; private double mCosAngle; private int mDuration; private int mDistance; private int mFinalX, mFinalY; private int mCurrX, mCurrY; public int getFinalX() { return mFinalX; } public int getFinalY() { return mFinalY; } public int getDuration() { return mDuration; } public int getCurrX() { return mCurrX; } public int getCurrY() { return mCurrY; } public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { mStartX = startX; mStartY = startY; mMinX = minX; mMinY = minY; mMaxX = maxX; mMaxY = maxY; double velocity = Math.hypot(velocityX, velocityY); mSinAngle = velocityY / velocity; mCosAngle = velocityX / velocity; // // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d) // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T // Thus, // v0 = d * (e - s) / T => (e - s) = v0 * T / d // // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second; mDuration = (int)Math.round(FLING_DURATION_PARAM * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1))); // (e - s) = v0 * T / d mDistance = (int)Math.round( velocity * mDuration / DECELERATED_FACTOR / 1000); mFinalX = getX(1.0f); mFinalY = getY(1.0f); } public void computeScrollOffset(float progress) { progress = Math.min(progress, 1); float f = 1 - progress; f = 1 - (float) Math.pow(f, DECELERATED_FACTOR); mCurrX = getX(f); mCurrY = getY(f); } private int getX(float f) { return (int) Utils.clamp( Math.round(mStartX + f * mDistance * mCosAngle), mMinX, mMaxX); } private int getY(float f) { return (int) Utils.clamp( Math.round(mStartY + f * mDistance * mSinAngle), mMinY, mMaxY); } }
src/com/android/gallery3d/ui/PhotoView.java +8 −5 Original line number Diff line number Diff line Loading @@ -145,7 +145,7 @@ public class PhotoView extends GLView { mScreenNails[i] = new ScreenNailEntry(); } mPositionController = new PositionController(this); mPositionController = new PositionController(this, context); mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); } Loading Loading @@ -437,7 +437,7 @@ public class PhotoView extends GLView { if (mTransitionMode != TRANS_NONE) return false; // Decide whether to swiping to the next/prev image in the zoom-in case RectF bounds = mPositionController.getImageBounds(); RectF bounds = controller.getImageBounds(); int left = Math.round(bounds.left); int right = Math.round(bounds.right); int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); Loading Loading @@ -480,9 +480,12 @@ public class PhotoView extends GLView { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (swipeImages(velocityX)) { mIgnoreUpEvent = true; } else if (mTransitionMode != TRANS_NONE) { // do nothing } else if (mPositionController.fling(velocityX, velocityY)) { mIgnoreUpEvent = true; if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) { mPositionController.up(); } return true; } Loading
src/com/android/gallery3d/ui/PositionController.java +142 −104 Original line number Diff line number Diff line Loading @@ -31,18 +31,22 @@ import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.Scroller; class PositionController { private static final String TAG = "PositionController"; private long mAnimationStartTime = NO_ANIMATION; private static final long NO_ANIMATION = -1; private static final long LAST_ANIMATION = -2; private int mAnimationKind; private float mAnimationDuration; 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 final static int ANIM_KIND_SLIDE = 3; private final static int ANIM_KIND_ZOOM = 4; private final static int ANIM_KIND_FLING = 5; // Animation time in milliseconds. The order must match ANIM_KIND_* above. private final static int ANIM_TIME[] = { Loading @@ -51,6 +55,7 @@ class PositionController { 600, // ANIM_KIND_SNAPBACK 400, // ANIM_KIND_SLIDE 300, // ANIM_KIND_ZOOM 0, // ANIM_KIND_FLING (the duration is calculated dynamically) }; // We try to scale up the image to fill the screen. But in order not to Loading @@ -69,13 +74,20 @@ class PositionController { private float mCurrentScale, mFromScale, mToScale; // The focus point of the scaling gesture (in bitmap coordinates). private float mFocusBitmapX; private float mFocusBitmapY; private int mFocusBitmapX; private int mFocusBitmapY; private boolean mInScale; // The minimum and maximum scale we allow. private float mScaleMin, mScaleMax = SCALE_LIMIT; // This is used by the fling animation private FlingScroller mScroller; // The bound of the stable region, see the comments above // calculateStableBound() for details. private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; // Assume the image size is the same as view size before we know the actual // size of image. private boolean mUseViewSize = true; Loading @@ -83,8 +95,9 @@ class PositionController { private RectF mTempRect = new RectF(); private float[] mTempPoints = new float[8]; public PositionController(PhotoView viewer) { public PositionController(PhotoView viewer, Context context) { mViewer = viewer; mScroller = new FlingScroller(); } public void setImageSize(int width, int height) { Loading Loading @@ -120,6 +133,9 @@ class PositionController { mToY = translate(mToY, mImageH, height, ratio); mToScale = mToScale * ratio; mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio); mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio); mImageW = width; mImageH = height; Loading @@ -144,28 +160,14 @@ class PositionController { public void zoomIn(float tapX, float tapY, float targetScale) { if (targetScale > mScaleMax) targetScale = mScaleMax; float scale = mCurrentScale; // Convert the tap position to image coordinate float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX; float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY; // We want to make sure that after zoom-in, we don't see black regions // because we zoom too close to the border. The conditions are: // // (mViewW / 2) / targetScale + mCurrentX < mImageW // -(mViewW / 2) / targetScale + mCurrentX > 0 float min = mViewW / 2.0f / targetScale; float max = mImageW - mViewW / 2.0f / targetScale; int targetX = (int) Utils.clamp(tempX, min, max); min = mViewH / 2.0f / targetScale; max = mImageH - mViewH / 2.0f / targetScale; int targetY = (int) Utils.clamp(tempY, min, max); int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX); int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY); // If the width of the image is less then the view, center the image if (mImageW * targetScale < mViewW) targetX = mImageW / 2; if (mImageH * targetScale < mViewH) targetY = mImageH / 2; calculateStableBound(targetScale); int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight); int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom); startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); } Loading @@ -179,37 +181,31 @@ class PositionController { Math.min((float) mViewW / w, (float) mViewH / h)); } // Translate the coordinate if the aspect ratio of the image changes. // When the user slides a image before it's loaded, we don't know the // actual aspect ratio, so we will assume one. When we receive the actual // aspect ratio, we need to translate the coordinate from the old (assumed) // bitmap into the new (actual) bitmap. // Translate a coordinate on bitmap if the bitmap size changes. // If the aspect ratio doesn't change, it's easy: // // +-------------------------+ "o" is where center of the view // | +--------+ | is. mCurrent{X,Y} is the coordinate of // | | | | "o" relative to the old bitmap. Assume // | | o | | the old bitmap size is (w, h). The new // | +--------+ | bitmap size is (w', h'). First we adjust // | | mCurrentScale by factor r = min(w/w', // +-------------------------+ h/h'), so one of the sides matches the old // | bitmap (w'*r == w or h'*r == h). // v // +-------------------------+ Then we put the new scaled bitmap to the // | +--+ .......... | center of the original bitmap's bounding // | | | . . | box. The center of the old bitmap and the // | | | . o . | new bitmap must match in view coordinate: // | +--+ .......... | // | | (w/2 - mCurrentX) * mCurrentScale = // +-------------------------+ (w'/2 - mCurrentX') * mCurrentScale * r // | // v Solve for mCurrentX' we have: // +-------------------------+ // | ...+--+... | mCurrentX' = w'/2 + (mCurrentX - w/2) / r // | . | | . | // | . o| | . | // | ...+--+... | // | | // +-------------------------+ // r = w / w' (= h / h') // x' = x / r // y' = y / r // // However the aspect ratio may change. That happens when the user slides // a image before it's loaded, we don't know the actual aspect ratio, so // we will assume one. When we receive the actual bitmap size, we need to // translate the coordinate from the old bitmap into the new bitmap. // // What we want to do is center the bitmap at the original position. // // ...+--+... // . | | . // . | | . // ...+--+... // // First we scale down the new bitmap by a factor r = min(w/w', h/h'). // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of // the old bitmap maps to (x', y') in the new bitmap, where // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r private static int translate(int value, int size, int newSize, float ratio) { return Math.round(newSize / 2f + (value - size / 2f) / ratio); } Loading Loading @@ -261,8 +257,10 @@ class PositionController { public void beginScale(float focusX, float focusY) { mInScale = true; mFocusBitmapX = mCurrentX + (focusX - mViewW / 2f) / mCurrentScale; mFocusBitmapY = mCurrentY + (focusY - mViewH / 2f) / mCurrentScale; mFocusBitmapX = Math.round(mCurrentX + (focusX - mViewW / 2f) / mCurrentScale); mFocusBitmapY = Math.round(mCurrentY + (focusY - mViewH / 2f) / mCurrentScale); } public void scaleBy(float s, float focusX, float focusY) { Loading Loading @@ -337,27 +335,51 @@ class PositionController { mCurrentScale, type); } public boolean fling(float velocityX, float velocityY) { // We only want to do fling when the picture is zoomed-in. if (mImageW * mCurrentScale <= mViewW && mImageH * mCurrentScale <= mViewH) { return false; } calculateStableBound(mCurrentScale); mScroller.fling(mCurrentX, mCurrentY, Math.round(-velocityX / mCurrentScale), Math.round(-velocityY / mCurrentScale), mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); int targetX = mScroller.getFinalX(); int targetY = mScroller.getFinalY(); mAnimationDuration = mScroller.getDuration(); startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING); return true; } private void startAnimation( int centerX, int centerY, float scale, int kind) { if (centerX == mCurrentX && centerY == mCurrentY int targetX, int targetY, float scale, int kind) { if (targetX == mCurrentX && targetY == mCurrentY && scale == mCurrentScale) return; mFromX = mCurrentX; mFromY = mCurrentY; mFromScale = mCurrentScale; mToX = centerX; mToY = centerY; mToX = targetX; mToY = targetY; mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); // If the scaled dimension is smaller than the view, // If the scaled height is smaller than the view height, // force it to be in the center. // (We do for height only, not width, because the user may // want to scroll to the previous/next image.) if (Math.floor(mImageH * mToScale) <= mViewH) { mToY = mImageH / 2; } mAnimationStartTime = SystemClock.uptimeMillis(); mAnimationKind = kind; if (mAnimationKind != ANIM_KIND_FLING) { mAnimationDuration = ANIM_TIME[mAnimationKind]; } if (advanceAnimation()) mViewer.invalidate(); } Loading @@ -375,13 +397,12 @@ class PositionController { } } float animationTime = ANIM_TIME[mAnimationKind]; long now = SystemClock.uptimeMillis(); float progress; if (animationTime == 0) { if (mAnimationDuration == 0) { progress = 1; } else { long now = SystemClock.uptimeMillis(); progress = (now - mAnimationStartTime) / animationTime; progress = (now - mAnimationStartTime) / mAnimationDuration; } if (progress >= 1) { Loading @@ -394,6 +415,7 @@ class PositionController { float f = 1 - progress; switch (mAnimationKind) { case ANIM_KIND_SCROLL: case ANIM_KIND_FLING: progress = 1 - f; // linear break; case ANIM_KIND_SCALE: Loading @@ -405,12 +427,23 @@ class PositionController { progress = 1 - f * f * f * f * f; // x^5 break; } if (mAnimationKind == ANIM_KIND_FLING) { flingInterpolate(progress); } else { linearInterpolate(progress); } } mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); return true; } private void flingInterpolate(float progress) { mScroller.computeScrollOffset(progress); mCurrentX = mScroller.getCurrX(); mCurrentY = mScroller.getCurrY(); mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); } // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1]. private void linearInterpolate(float progress) { // To linearly interpolate the position on view coordinates, we do the Loading Loading @@ -452,8 +485,6 @@ class PositionController { public boolean startSnapback() { boolean needAnimation = false; int x = mCurrentX; int y = mCurrentY; float scale = mCurrentScale; if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { Loading @@ -461,62 +492,69 @@ class PositionController { scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); } // The number of pixels between the center of the view // and the edge when the edge is aligned. int left = (int) Math.ceil(mViewW / (2 * scale)); int right = mImageW - left; int top = (int) Math.ceil(mViewH / (2 * scale)); int bottom = mImageH - top; calculateStableBound(scale); int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight); int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom); if (mImageW * scale > mViewW) { if (mCurrentX < left) { needAnimation = true; x = left; } else if (mCurrentX > right) { if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) { needAnimation = true; x = right; } } else if (mCurrentX != mImageW / 2) { needAnimation = true; x = mImageW / 2; if (needAnimation) { startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); } if (mImageH * scale > mViewH) { if (mCurrentY < top) { needAnimation = true; y = top; } else if (mCurrentY > bottom) { needAnimation = true; y = bottom; return needAnimation; } } else if (mCurrentY != mImageH / 2) { needAnimation = true; y = mImageH / 2; // Calculates the stable region of mCurrent{X/Y}, where "stable" means // // (1) If the dimension of scaled image >= view dimension, we will not // see black region outside the image (at that dimension). // (2) If the dimension of scaled image < view dimension, we will center // the scaled image. // // We might temporarily go out of this stable during user interaction, // but will "snap back" after user stops interaction. // // The results are stored in mBound{Left/Right/Top/Bottom}. // private void calculateStableBound(float scale) { // The number of pixels between the center of the view // and the edge when the edge is aligned. mBoundLeft = (int) Math.ceil(mViewW / (2 * scale)); mBoundRight = mImageW - mBoundLeft; mBoundTop = (int) Math.ceil(mViewH / (2 * scale)); mBoundBottom = mImageH - mBoundTop; // If the scaled height is smaller than the view height, // force it to be in the center. if (Math.floor(mImageH * scale) <= mViewH) { mBoundTop = mBoundBottom = mImageH / 2; } if (needAnimation) { startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); // Same for width if (Math.floor(mImageW * scale) <= mViewW) { mBoundLeft = mBoundRight = mImageW / 2; } } return needAnimation; private boolean useCurrentValueAsTarget() { return mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK || mAnimationKind == ANIM_KIND_FLING; } private float getTargetScale() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale; return mToScale; return useCurrentValueAsTarget() ? mCurrentScale : mToScale; } private int getTargetX() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX; return mToX; return useCurrentValueAsTarget() ? mCurrentX : mToX; } private int getTargetY() { if (mAnimationStartTime == NO_ANIMATION || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY; return mToY; return useCurrentValueAsTarget() ? mCurrentY : mToY; } public RectF getImageBounds() { Loading