Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b3aab90b authored by Chih-Chung Chang's avatar Chih-Chung Chang
Browse files

Fix 5319007: Add fling to PhotoView.

Change-Id: Iacda65fbe1fcb3ad245ad99e0b062606ca6792b9
parent 26e87940
Loading
Loading
Loading
Loading
+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);
    }
}
+8 −5
Original line number Diff line number Diff line
@@ -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);
    }

@@ -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);
@@ -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;
        }
+142 −104
Original line number Diff line number Diff line
@@ -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[] = {
@@ -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
@@ -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;
@@ -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) {
@@ -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;

@@ -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);
    }
@@ -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);
    }
@@ -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) {
@@ -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();
    }

@@ -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) {
@@ -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:
@@ -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
@@ -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) {
@@ -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() {