summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ui/PositionController.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/ui/PositionController.java')
-rw-r--r--src/com/android/gallery3d/ui/PositionController.java1821
1 files changed, 1821 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
new file mode 100644
index 000000000..6a4bcea87
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -0,0 +1,1821 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Rect;
+import android.util.Log;
+import android.widget.Scroller;
+
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PhotoView.Size;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeIntArray;
+
+class PositionController {
+ private static final String TAG = "PositionController";
+
+ public static final int IMAGE_AT_LEFT_EDGE = 1;
+ public static final int IMAGE_AT_RIGHT_EDGE = 2;
+ public static final int IMAGE_AT_TOP_EDGE = 4;
+ public static final int IMAGE_AT_BOTTOM_EDGE = 8;
+
+ public static final int CAPTURE_ANIMATION_TIME = 700;
+ public static final int SNAPBACK_ANIMATION_TIME = 600;
+
+ // Special values for animation time.
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ private static final int ANIM_KIND_NONE = -1;
+ private static final int ANIM_KIND_SCROLL = 0;
+ private static final int ANIM_KIND_SCALE = 1;
+ private static final int ANIM_KIND_SNAPBACK = 2;
+ private static final int ANIM_KIND_SLIDE = 3;
+ private static final int ANIM_KIND_ZOOM = 4;
+ private static final int ANIM_KIND_OPENING = 5;
+ private static final int ANIM_KIND_FLING = 6;
+ private static final int ANIM_KIND_FLING_X = 7;
+ private static final int ANIM_KIND_DELETE = 8;
+ private static final int ANIM_KIND_CAPTURE = 9;
+
+ // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+ //
+ // The values for ANIM_KIND_FLING_X does't matter because we use
+ // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
+ // faster for Animatable.advanceAnimation() to calculate the progress
+ // (always 1).
+ private static final int ANIM_TIME[] = {
+ 0, // ANIM_KIND_SCROLL
+ 0, // ANIM_KIND_SCALE
+ SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK
+ 400, // ANIM_KIND_SLIDE
+ 300, // ANIM_KIND_ZOOM
+ 300, // ANIM_KIND_OPENING
+ 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
+ 0, // ANIM_KIND_FLING_X (see the comment above)
+ 0, // ANIM_KIND_DELETE (the duration is calculated dynamically)
+ CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE
+ };
+
+ // We try to scale up the image to fill the screen. But in order not to
+ // scale too much for small icons, we limit the max up-scaling factor here.
+ private static final float SCALE_LIMIT = 4;
+
+ // For user's gestures, we give a temporary extra scaling range which goes
+ // above or below the usual scaling limits.
+ private static final float SCALE_MIN_EXTRA = 0.7f;
+ private static final float SCALE_MAX_EXTRA = 1.4f;
+
+ // Setting this true makes the extra scaling range permanent (until this is
+ // set to false again).
+ private boolean mExtraScalingRange = false;
+
+ // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+ private boolean mFilmMode = false;
+
+ // These are the limits for width / height of the picture in film mode.
+ private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
+ private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
+
+ // In addition to the focused box (index == 0). We also keep information
+ // about this many boxes on each side.
+ private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
+
+ private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
+ private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
+
+ // These are constants for the delete gesture.
+ private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
+ private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
+
+ private Listener mListener;
+ private volatile Rect mOpenAnimationRect;
+
+ // Use a large enough value, so we won't see the gray shadow in the beginning.
+ private int mViewW = 1200;
+ private int mViewH = 1200;
+
+ // A scaling gesture is in progress.
+ private boolean mInScale;
+ // The focus point of the scaling gesture, relative to the center of the
+ // picture in bitmap pixels.
+ private float mFocusX, mFocusY;
+
+ // whether there is a previous/next picture.
+ private boolean mHasPrev, mHasNext;
+
+ // This is used by the fling animation (page mode).
+ private FlingScroller mPageScroller;
+
+ // This is used by the fling animation (film mode).
+ private Scroller mFilmScroller;
+
+ // The bound of the stable region that the focused box can stay, see the
+ // comments above calculateStableBound() for details.
+ private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
+ // Constrained frame is a rectangle that the focused box should fit into if
+ // it is constrained. It has two effects:
+ //
+ // (1) In page mode, if the focused box is constrained, scaling for the
+ // focused box is adjusted to fit into the constrained frame, instead of the
+ // whole view.
+ //
+ // (2) In page mode, if the focused box is constrained, the mPlatform's
+ // default center (mDefaultX/Y) is moved to the center of the constrained
+ // frame, instead of the view center.
+ //
+ private Rect mConstrainedFrame = new Rect();
+
+ // Whether the focused box is constrained.
+ //
+ // Our current program's first call to moveBox() sets constrained = true, so
+ // we set the initial value of this variable to true, and we will not see
+ // see unwanted transition animation.
+ private boolean mConstrained = true;
+
+ //
+ // ___________________________________________________________
+ // | _____ _____ _____ _____ _____ |
+ // | | | | | | | | | | | |
+ // | | Box | | Box | | Box*| | Box | | Box | |
+ // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
+ // | Gap Gap Gap Gap |
+ // |___________________________________________________________|
+ //
+ // <-- Platform -->
+ //
+ // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
+
+ private Platform mPlatform = new Platform();
+ private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ // The gap at the right of a Box i is at index i. The gap at the left of a
+ // Box i is at index i - 1.
+ private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+ private FilmRatio mFilmRatio = new FilmRatio();
+
+ // These are only used during moveBox().
+ private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ private RangeArray<Gap> mTempGaps =
+ new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+
+ // The output of the PositionController. Available through getPosition().
+ private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
+
+ // The direction of a new picture should appear. New pictures pop from top
+ // if this value is true, or from bottom if this value is false.
+ boolean mPopFromTop;
+
+ public interface Listener {
+ void invalidate();
+ boolean isHoldingDown();
+ boolean isHoldingDelete();
+
+ // EdgeView
+ void onPull(int offset, int direction);
+ void onRelease();
+ void onAbsorb(int velocity, int direction);
+ }
+
+ static {
+ // Initialize the CENTER_OUT_INDEX array.
+ // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
+ // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
+ for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
+ int j = (i + 1) / 2;
+ if ((i & 1) == 0) j = -j;
+ CENTER_OUT_INDEX[i] = j;
+ }
+ }
+
+ public PositionController(Context context, Listener listener) {
+ mListener = listener;
+ mPageScroller = new FlingScroller();
+ mFilmScroller = new Scroller(context, null, false);
+
+ // Initialize the areas.
+ initPlatform();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.put(i, new Box());
+ initBox(i);
+ mRects.put(i, new Rect());
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.put(i, new Gap());
+ initGap(i);
+ }
+ }
+
+ public void setOpenAnimationRect(Rect r) {
+ mOpenAnimationRect = r;
+ }
+
+ public void setViewSize(int viewW, int viewH) {
+ if (viewW == mViewW && viewH == mViewH) return;
+
+ boolean wasMinimal = isAtMinimalScale();
+
+ mViewW = viewW;
+ mViewH = viewH;
+ initPlatform();
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ setBoxSize(i, viewW, viewH, true);
+ }
+
+ updateScaleAndGapLimit();
+
+ // If the focused box was at minimal scale, we try to make it the
+ // minimal scale under the new view size.
+ if (wasMinimal) {
+ Box b = mBoxes.get(0);
+ b.mCurrentScale = b.mScaleMin;
+ }
+
+ // If we have the opening animation, do it. Otherwise go directly to the
+ // right position.
+ if (!startOpeningAnimationIfNeeded()) {
+ skipToFinalPosition();
+ }
+ }
+
+ public void setConstrainedFrame(Rect cFrame) {
+ if (mConstrainedFrame.equals(cFrame)) return;
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ public void forceImageSize(int index, Size s) {
+ if (s.width == 0 || s.height == 0) return;
+ Box b = mBoxes.get(index);
+ b.mImageW = s.width;
+ b.mImageH = s.height;
+ return;
+ }
+
+ public void setImageSize(int index, Size s, Rect cFrame) {
+ if (s.width == 0 || s.height == 0) return;
+
+ boolean needUpdate = false;
+ if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ needUpdate = true;
+ }
+ needUpdate |= setBoxSize(index, s.width, s.height, false);
+
+ if (!needUpdate) return;
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ // Returns false if the box size doesn't change.
+ private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
+ Box b = mBoxes.get(i);
+ boolean wasViewSize = b.mUseViewSize;
+
+ // If we already have an image size, we don't want to use the view size.
+ if (!wasViewSize && isViewSize) return false;
+
+ b.mUseViewSize = isViewSize;
+
+ if (width == b.mImageW && height == b.mImageH) {
+ return false;
+ }
+
+ // The ratio of the old size and the new size.
+ //
+ // If the aspect ratio changes, we don't know if it is because one side
+ // grows or the other side shrinks. Currently we just assume the view
+ // angle of the longer side doesn't change (so the aspect ratio change
+ // is because the view angle of the shorter side changes). This matches
+ // what camera preview does.
+ float ratio = (width > height)
+ ? (float) b.mImageW / width
+ : (float) b.mImageH / height;
+
+ b.mImageW = width;
+ b.mImageH = height;
+
+ // If this is the first time we receive an image size or we are in fullscreen,
+ // we change the scale directly. Otherwise adjust the scales by a ratio,
+ // and snapback will animate the scale into the min/max bounds if necessary.
+ if ((wasViewSize && !isViewSize) || !mFilmMode) {
+ b.mCurrentScale = getMinimalScale(b);
+ b.mAnimationStartTime = NO_ANIMATION;
+ } else {
+ b.mCurrentScale *= ratio;
+ b.mFromScale *= ratio;
+ b.mToScale *= ratio;
+ }
+
+ if (i == 0) {
+ mFocusX /= ratio;
+ mFocusY /= ratio;
+ }
+
+ return true;
+ }
+
+ private boolean startOpeningAnimationIfNeeded() {
+ if (mOpenAnimationRect == null) return false;
+ Box b = mBoxes.get(0);
+ if (b.mUseViewSize) return false;
+
+ // Start animation from the saved rectangle if we have one.
+ Rect r = mOpenAnimationRect;
+ mOpenAnimationRect = null;
+
+ mPlatform.mCurrentX = r.centerX() - mViewW / 2;
+ b.mCurrentY = r.centerY() - mViewH / 2;
+ b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+ r.height() / (float) b.mImageH);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
+ ANIM_KIND_OPENING);
+
+ // Animate from large gaps for neighbor boxes to avoid them
+ // shown on the screen during opening animation.
+ for (int i = -1; i < 1; i++) {
+ Gap g = mGaps.get(i);
+ g.mCurrentGap = mViewW;
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
+ }
+
+ return true;
+ }
+
+ public void setFilmMode(boolean enabled) {
+ if (enabled == mFilmMode) return;
+ mFilmMode = enabled;
+
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ stopAnimation();
+ snapAndRedraw();
+ }
+
+ public void setExtraScalingRange(boolean enabled) {
+ if (mExtraScalingRange == enabled) return;
+ mExtraScalingRange = enabled;
+ if (!enabled) {
+ snapAndRedraw();
+ }
+ }
+
+ // This should be called whenever the scale range of boxes or the default
+ // gap size may change. Currently this can happen due to change of view
+ // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
+ private void updateScaleAndGapLimit() {
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ }
+
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ g.mDefaultSize = getDefaultGapSize(i);
+ }
+ }
+
+ // Returns the default gap size according the the size of the boxes around
+ // the gap and the current mode.
+ private int getDefaultGapSize(int i) {
+ if (mFilmMode) return IMAGE_GAP;
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
+ }
+
+ // Here is how we layout the boxes in the page mode.
+ //
+ // previous current next
+ // ___________ ________________ __________
+ // | _______ | | __________ | | ______ |
+ // | | | | | | right->| | | | | |
+ // | | |<-------->|<--left | | | | | |
+ // | |_______| | | | |__________| | | |______| |
+ // |___________| | |________________| |__________|
+ // | <--> gapToSide()
+ // |
+ // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
+ private int gapToSide(Box b) {
+ return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
+ }
+
+ // Stop all animations at where they are now.
+ public void stopAnimation() {
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ }
+
+ public void skipAnimation() {
+ if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+ mPlatform.mCurrentX = mPlatform.mToX;
+ mPlatform.mCurrentY = mPlatform.mToY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ if (b.mAnimationStartTime == NO_ANIMATION) continue;
+ b.mCurrentY = b.mToY;
+ b.mCurrentScale = b.mToScale;
+ b.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ if (g.mAnimationStartTime == NO_ANIMATION) continue;
+ g.mCurrentGap = g.mToGap;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+ redraw();
+ }
+
+ public void snapback() {
+ snapAndRedraw();
+ }
+
+ public void skipToFinalPosition() {
+ stopAnimation();
+ snapAndRedraw();
+ skipAnimation();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Start an animations for the focused box
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ tapX -= mViewW / 2;
+ tapY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+
+ // Convert the tap position to distance to center in bitmap coordinates
+ float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+ float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+ int x = (int) (-tempX * targetScale + 0.5f);
+ int y = (int) (-tempY * targetScale + 0.5f);
+
+ calculateStableBound(targetScale);
+ int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
+ int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
+ targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
+
+ startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+ }
+
+ public void resetToFullView() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ mInScale = true;
+ mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
+ mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
+ }
+
+ // Scales the image by the given factor.
+ // Returns an out-of-range indicator:
+ // 1 if the intended scale is too large for the stable range.
+ // 0 if the intended scale is in the stable range.
+ // -1 if the intended scale is too small for the stable range.
+ public int scaleBy(float s, float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We want to keep the focus point (on the bitmap) the same as when we
+ // begin the scale gesture, that is,
+ //
+ // (focusX' - currentX') / scale' = (focusX - currentX) / scale
+ //
+ s = b.clampScale(s * getTargetScale(b));
+ int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
+ int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
+ startAnimation(x, y, s, ANIM_KIND_SCALE);
+ if (s < b.mScaleMin) return -1;
+ if (s > b.mScaleMax) return 1;
+ return 0;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ snapAndRedraw();
+ }
+
+ // Slide the focused box to the center of the view.
+ public void startHorizontalSlide() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
+ }
+
+ // Slide the focused box to the center of the view with the capture
+ // animation. In addition to the sliding, the animation will also scale the
+ // the focused box, the specified neighbor box, and the gap between the
+ // two. The specified offset should be 1 or -1.
+ public void startCaptureAnimationSlide(int offset) {
+ Box b = mBoxes.get(0);
+ Box n = mBoxes.get(offset); // the neighbor box
+ Gap g = mGaps.get(offset); // the gap between the two boxes
+
+ mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
+ ANIM_KIND_CAPTURE);
+ b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
+ n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+ redraw();
+ }
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ private boolean canScroll() {
+ Box b = mBoxes.get(0);
+ if (b.mAnimationStartTime == NO_ANIMATION) return true;
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ return true;
+ }
+ return false;
+ }
+
+ public void scrollPage(int dx, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ calculateStableBound(b.mCurrentScale);
+
+ int x = p.mCurrentX + dx;
+ int y = b.mCurrentY + dy;
+
+ // Vertical direction: If we have space to move in the vertical
+ // direction, we show the edge effect when scrolling reaches the edge.
+ if (mBoundTop != mBoundBottom) {
+ if (y < mBoundTop) {
+ mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
+ } else if (y > mBoundBottom) {
+ mListener.onPull(y - mBoundBottom, EdgeView.TOP);
+ }
+ }
+
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ if (!mHasPrev && x > mBoundRight) {
+ int pixels = x - mBoundRight;
+ mListener.onPull(pixels, EdgeView.LEFT);
+ x = mBoundRight;
+ } else if (!mHasNext && x < mBoundLeft) {
+ int pixels = mBoundLeft - x;
+ mListener.onPull(pixels, EdgeView.RIGHT);
+ x = mBoundLeft;
+ }
+
+ startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmX(int dx) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ if (b.mAnimationStartTime != NO_ANIMATION) {
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ break;
+ default:
+ return;
+ }
+ }
+
+ int x = p.mCurrentX + dx;
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ x -= mPlatform.mDefaultX;
+ if (!mHasPrev && x > 0) {
+ mListener.onPull(x, EdgeView.LEFT);
+ x = 0;
+ } else if (!mHasNext && x < 0) {
+ mListener.onPull(-x, EdgeView.RIGHT);
+ x = 0;
+ }
+ x += mPlatform.mDefaultX;
+ startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmY(int boxIndex, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(boxIndex);
+ int y = b.mCurrentY + dy;
+ b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ redraw();
+ }
+
+ public boolean flingPage(int velocityX, int velocityY) {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We only want to do fling when the picture is zoomed-in.
+ if (viewWiderThanScaledImage(b.mCurrentScale) &&
+ viewTallerThanScaledImage(b.mCurrentScale)) {
+ return false;
+ }
+
+ // We only allow flinging in the directions where it won't go over the
+ // picture.
+ int edges = getImageAtEdges();
+ if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
+ (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
+ velocityX = 0;
+ }
+ if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
+ (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
+ velocityY = 0;
+ }
+
+ if (velocityX == 0 && velocityY == 0) return false;
+
+ mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
+ mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+ int targetX = mPageScroller.getFinalX();
+ int targetY = mPageScroller.getFinalY();
+ ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+ return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
+ }
+
+ public boolean flingFilmX(int velocityX) {
+ if (velocityX == 0) return false;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // If we are already at the edge, don't start the fling.
+ int defaultX = p.mDefaultX;
+ if ((!mHasPrev && p.mCurrentX >= defaultX)
+ || (!mHasNext && p.mCurrentX <= defaultX)) {
+ return false;
+ }
+
+ mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+ int targetX = mFilmScroller.getFinalX();
+ return startAnimation(
+ targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
+ }
+
+ // Moves the specified box out of screen. If velocityY is 0, a default
+ // velocity is used. Returns the time for the duration, or -1 if we cannot
+ // not do the animation.
+ public int flingFilmY(int boxIndex, int velocityY) {
+ Box b = mBoxes.get(boxIndex);
+
+ // Calculate targetY
+ int h = heightOf(b);
+ int targetY;
+ int FUZZY = 3; // TODO: figure out why this is needed.
+ if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
+ targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
+ } else {
+ targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
+ }
+
+ // Calculate duration
+ int duration;
+ if (velocityY != 0) {
+ duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
+ / Math.abs(velocityY));
+ duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
+ } else {
+ duration = DEFAULT_DELETE_ANIMATION_DURATION;
+ }
+
+ // Start animation
+ ANIM_TIME[ANIM_KIND_DELETE] = duration;
+ if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
+ redraw();
+ return duration;
+ }
+ return -1;
+ }
+
+ // Returns the index of the box which contains the given point (x, y)
+ // Returns Integer.MAX_VALUE if there is no hit. There may be more than
+ // one box contains the given point, and we want to give priority to the
+ // one closer to the focused index (0).
+ public int hitTest(int x, int y) {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ int j = CENTER_OUT_INDEX[i];
+ Rect r = mRects.get(j);
+ if (r.contains(x, y)) {
+ return j;
+ }
+ }
+
+ return Integer.MAX_VALUE;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Redraw
+ //
+ // If a method changes box positions directly, redraw()
+ // should be called.
+ //
+ // If a method may also cause a snapback to happen, snapAndRedraw() should
+ // be called.
+ //
+ // If a method starts an animation to change the position of focused box,
+ // startAnimation() should be called.
+ //
+ // If time advances to change the box position, advanceAnimation() should
+ // be called.
+ ////////////////////////////////////////////////////////////////////////////
+ private void redraw() {
+ layoutAndSetPosition();
+ mListener.invalidate();
+ }
+
+ private void snapAndRedraw() {
+ mPlatform.startSnapback();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).startSnapback();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).startSnapback();
+ }
+ mFilmRatio.startSnapback();
+ redraw();
+ }
+
+ private boolean startAnimation(int targetX, int targetY, float targetScale,
+ int kind) {
+ boolean changed = false;
+ changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
+ changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+ if (changed) redraw();
+ return changed;
+ }
+
+ public void advanceAnimation() {
+ boolean changed = false;
+ changed |= mPlatform.advanceAnimation();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ changed |= mBoxes.get(i).advanceAnimation();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ changed |= mGaps.get(i).advanceAnimation();
+ }
+ changed |= mFilmRatio.advanceAnimation();
+ if (changed) redraw();
+ }
+
+ public boolean inOpeningAnimation() {
+ return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
+ mPlatform.mAnimationStartTime != NO_ANIMATION) ||
+ (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
+ mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Layout
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Returns the display width of this box.
+ private int widthOf(Box b) {
+ return (int) (b.mImageW * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display height of this box.
+ private int heightOf(Box b) {
+ return (int) (b.mImageH * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display width of this box, using the given scale.
+ private int widthOf(Box b, float scale) {
+ return (int) (b.mImageW * scale + 0.5f);
+ }
+
+ // Returns the display height of this box, using the given scale.
+ private int heightOf(Box b, float scale) {
+ return (int) (b.mImageH * scale + 0.5f);
+ }
+
+ // Convert the information in mPlatform and mBoxes to mRects, so the user
+ // can get the position of each box by getPosition().
+ //
+ // Note we go from center-out because each box's X coordinate
+ // is relative to its anchor box (except the focused box).
+ private void layoutAndSetPosition() {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ convertBoxToRect(CENTER_OUT_INDEX[i]);
+ }
+ //dumpState();
+ }
+
+ @SuppressWarnings("unused")
+ private void dumpState() {
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
+ }
+
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ dumpRect(CENTER_OUT_INDEX[i]);
+ }
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ for (int j = i + 1; j <= BOX_MAX; j++) {
+ if (Rect.intersects(mRects.get(i), mRects.get(j))) {
+ Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
+ }
+ }
+ }
+ }
+
+ private void dumpRect(int i) {
+ StringBuilder sb = new StringBuilder();
+ Rect r = mRects.get(i);
+ sb.append("Rect " + i + ":");
+ sb.append("(");
+ sb.append(r.centerX());
+ sb.append(",");
+ sb.append(r.centerY());
+ sb.append(") [");
+ sb.append(r.width());
+ sb.append("x");
+ sb.append(r.height());
+ sb.append("]");
+ Log.d(TAG, sb.toString());
+ }
+
+ private void convertBoxToRect(int i) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
+ int w = widthOf(b);
+ int h = heightOf(b);
+ if (i == 0) {
+ int x = mPlatform.mCurrentX + mViewW / 2;
+ r.left = x - w / 2;
+ r.right = r.left + w;
+ } else if (i > 0) {
+ Rect a = mRects.get(i - 1);
+ Gap g = mGaps.get(i - 1);
+ r.left = a.right + g.mCurrentGap;
+ r.right = r.left + w;
+ } else { // i < 0
+ Rect a = mRects.get(i + 1);
+ Gap g = mGaps.get(i);
+ r.right = a.left - g.mCurrentGap;
+ r.left = r.right - w;
+ }
+ r.top = y - h / 2;
+ r.bottom = r.top + h;
+ }
+
+ // Returns the position of a box.
+ public Rect getPosition(int index) {
+ return mRects.get(index);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box management
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Initialize the platform to be at the view center.
+ private void initPlatform() {
+ mPlatform.updateDefaultXY();
+ mPlatform.mCurrentX = mPlatform.mDefaultX;
+ mPlatform.mCurrentY = mPlatform.mDefaultY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ // Initialize a box to have the size of the view.
+ private void initBox(int index) {
+ Box b = mBoxes.get(index);
+ b.mImageW = mViewW;
+ b.mImageH = mViewH;
+ b.mUseViewSize = true;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a box to a given size.
+ private void initBox(int index, Size size) {
+ if (size.width == 0 || size.height == 0) {
+ initBox(index);
+ return;
+ }
+ Box b = mBoxes.get(index);
+ b.mImageW = size.width;
+ b.mImageH = size.height;
+ b.mUseViewSize = false;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a gap. This can only be called after the boxes around the gap
+ // has been initialized.
+ private void initGap(int index) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = g.mDefaultSize;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ private void initGap(int index, int size) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = size;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ @SuppressWarnings("unused")
+ private void debugMoveBox(int fromIndex[]) {
+ StringBuilder s = new StringBuilder("moveBox:");
+ for (int i = 0; i < fromIndex.length; i++) {
+ int j = fromIndex[i];
+ if (j == Integer.MAX_VALUE) {
+ s.append(" N");
+ } else {
+ s.append(" ");
+ s.append(fromIndex[i]);
+ }
+ }
+ Log.d(TAG, s.toString());
+ }
+
+ // Move the boxes: it may indicate focus change, box deleted, box appearing,
+ // box reordered, etc.
+ //
+ // Each element in the fromIndex array indicates where each box was in the
+ // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
+ // means the box is new.
+ //
+ // For example:
+ // N N N N N N N -- all new boxes
+ // -3 -2 -1 0 1 2 3 -- nothing changed
+ // -2 -1 0 1 2 3 N -- focus goes to the next box
+ // N -3 -2 -1 0 1 2 -- focus goes to the previous box
+ // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+ //
+ // hasPrev/hasNext indicates if there are previous/next boxes for the
+ // focused box. constrained indicates whether the focused box should be put
+ // into the constrained frame.
+ public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
+ boolean constrained, Size[] sizes) {
+ //debugMoveBox(fromIndex);
+ mHasPrev = hasPrev;
+ mHasNext = hasNext;
+
+ RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+ // 1. Get the absolute X coordinates for the boxes.
+ layoutAndSetPosition();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ b.mAbsoluteX = r.centerX() - mViewW / 2;
+ }
+
+ // 2. copy boxes and gaps to temporary storage.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mTempBoxes.put(i, mBoxes.get(i));
+ mBoxes.put(i, null);
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mTempGaps.put(i, mGaps.get(i));
+ mGaps.put(i, null);
+ }
+
+ // 3. move back boxes that are used in the new array.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ mBoxes.put(i, mTempBoxes.get(j));
+ mTempBoxes.put(j, null);
+ }
+
+ // 4. move back gaps if both boxes around it are kept together.
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ int k = from.get(i + 1);
+ if (k == Integer.MAX_VALUE) continue;
+ if (j + 1 == k) {
+ mGaps.put(i, mTempGaps.get(j));
+ mTempGaps.put(j, null);
+ }
+ }
+
+ // 5. recycle the boxes that are not used in the new array.
+ int k = -BOX_MAX;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i) != null) continue;
+ while (mTempBoxes.get(k) == null) {
+ k++;
+ }
+ mBoxes.put(i, mTempBoxes.get(k++));
+ initBox(i, sizes[i + BOX_MAX]);
+ }
+
+ // 6. Now give the recycled box a reasonable absolute X position.
+ //
+ // First try to find the first and the last box which the absolute X
+ // position is known.
+ int first, last;
+ for (first = -BOX_MAX; first <= BOX_MAX; first++) {
+ if (from.get(first) != Integer.MAX_VALUE) break;
+ }
+ for (last = BOX_MAX; last >= -BOX_MAX; last--) {
+ if (from.get(last) != Integer.MAX_VALUE) break;
+ }
+ // If there is no box has known X position at all, make the focused one
+ // as known.
+ if (first > BOX_MAX) {
+ mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
+ first = last = 0;
+ }
+ // Now for those boxes between first and last, assign their position to
+ // align to the previous box or the next box with known position. For
+ // the boxes before first or after last, we will use a new default gap
+ // size below.
+
+ // Align to the previous box
+ for (int i = Math.max(0, first + 1); i < last; i++) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
+ + getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // Align to the next box
+ for (int i = Math.min(-1, last - 1); i > first; i--) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
+ - getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // 7. recycle the gaps that are not used in the new array.
+ k = -BOX_MAX;
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ if (mGaps.get(i) != null) continue;
+ while (mTempGaps.get(k) == null) {
+ k++;
+ }
+ mGaps.put(i, mTempGaps.get(k++));
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ if (i >= first && i < last) {
+ int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
+ initGap(i, g);
+ } else {
+ initGap(i);
+ }
+ }
+
+ // 8. calculate the new absolute X coordinates for those box before
+ // first or after last.
+ for (int i = first - 1; i >= -BOX_MAX; i--) {
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
+ }
+
+ for (int i = last + 1; i <= BOX_MAX; i++) {
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i - 1);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
+ }
+
+ // 9. offset the Platform position
+ int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+ mPlatform.mCurrentX += dx;
+ mPlatform.mFromX += dx;
+ mPlatform.mToX += dx;
+ mPlatform.mFlingOffset += dx;
+
+ if (mConstrained != constrained) {
+ mConstrained = constrained;
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ }
+
+ snapAndRedraw();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Public utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ public boolean isAtMinimalScale() {
+ Box b = mBoxes.get(0);
+ return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+ }
+
+ public boolean isCenter() {
+ Box b = mBoxes.get(0);
+ return mPlatform.mCurrentX == mPlatform.mDefaultX
+ && b.mCurrentY == 0;
+ }
+
+ public int getImageWidth() {
+ Box b = mBoxes.get(0);
+ return b.mImageW;
+ }
+
+ public int getImageHeight() {
+ Box b = mBoxes.get(0);
+ return b.mImageH;
+ }
+
+ public float getImageScale() {
+ Box b = mBoxes.get(0);
+ return b.mCurrentScale;
+ }
+
+ public int getImageAtEdges() {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ calculateStableBound(b.mCurrentScale);
+ int edges = 0;
+ if (p.mCurrentX <= mBoundLeft) {
+ edges |= IMAGE_AT_RIGHT_EDGE;
+ }
+ if (p.mCurrentX >= mBoundRight) {
+ edges |= IMAGE_AT_LEFT_EDGE;
+ }
+ if (b.mCurrentY <= mBoundTop) {
+ edges |= IMAGE_AT_BOTTOM_EDGE;
+ }
+ if (b.mCurrentY >= mBoundBottom) {
+ edges |= IMAGE_AT_TOP_EDGE;
+ }
+ return edges;
+ }
+
+ public boolean isScrolling() {
+ return mPlatform.mAnimationStartTime != NO_ANIMATION
+ && mPlatform.mCurrentX != mPlatform.mToX;
+ }
+
+ public void stopScrolling() {
+ if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
+ if (mFilmMode) mFilmScroller.forceFinished(true);
+ mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
+ }
+
+ public float getFilmRatio() {
+ return mFilmRatio.mCurrentRatio;
+ }
+
+ public void setPopFromTop(boolean top) {
+ mPopFromTop = top;
+ }
+
+ public boolean hasDeletingBox() {
+ for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Private utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ private float getMinimalScale(Box b) {
+ float wFactor = 1.0f;
+ float hFactor = 1.0f;
+ int viewW, viewH;
+
+ if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
+ && b == mBoxes.get(0)) {
+ viewW = mConstrainedFrame.width();
+ viewH = mConstrainedFrame.height();
+ } else {
+ viewW = mViewW;
+ viewH = mViewH;
+ }
+
+ if (mFilmMode) {
+ if (mViewH > mViewW) { // portrait
+ wFactor = FILM_MODE_PORTRAIT_WIDTH;
+ hFactor = FILM_MODE_PORTRAIT_HEIGHT;
+ } else { // landscape
+ wFactor = FILM_MODE_LANDSCAPE_WIDTH;
+ hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
+ }
+ }
+
+ float s = Math.min(wFactor * viewW / b.mImageW,
+ hFactor * viewH / b.mImageH);
+ return Math.min(SCALE_LIMIT, s);
+ }
+
+ private float getMaximalScale(Box b) {
+ if (mFilmMode) return getMinimalScale(b);
+ if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
+ return SCALE_LIMIT;
+ }
+
+ private static boolean isAlmostEqual(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
+ }
+
+ // Calculates the stable region of mPlatform.mCurrentX and
+ // mBoxes.get(0).mCurrentY, 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}.
+ //
+ // An extra parameter "horizontalSlack" (which has the value of 0 usually)
+ // is used to extend the stable region by some pixels on each side
+ // horizontally.
+ private void calculateStableBound(float scale, int horizontalSlack) {
+ Box b = mBoxes.get(0);
+
+ // The width and height of the box in number of view pixels
+ int w = widthOf(b, scale);
+ int h = heightOf(b, scale);
+
+ // When the edge of the view is aligned with the edge of the box
+ mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
+ mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
+ mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
+ mBoundBottom = h / 2 - mViewH / 2;
+
+ // If the scaled height is smaller than the view height,
+ // force it to be in the center.
+ if (viewTallerThanScaledImage(scale)) {
+ mBoundTop = mBoundBottom = 0;
+ }
+
+ // Same for width
+ if (viewWiderThanScaledImage(scale)) {
+ mBoundLeft = mBoundRight = mPlatform.mDefaultX;
+ }
+ }
+
+ private void calculateStableBound(float scale) {
+ calculateStableBound(scale, 0);
+ }
+
+ private boolean viewTallerThanScaledImage(float scale) {
+ return mViewH >= heightOf(mBoxes.get(0), scale);
+ }
+
+ private boolean viewWiderThanScaledImage(float scale) {
+ return mViewW >= widthOf(mBoxes.get(0), scale);
+ }
+
+ private float getTargetScale(Box b) {
+ return b.mAnimationStartTime == NO_ANIMATION
+ ? b.mCurrentScale : b.mToScale;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Animatable: an thing which can do animation.
+ ////////////////////////////////////////////////////////////////////////////
+ private abstract static class Animatable {
+ public long mAnimationStartTime;
+ public int mAnimationKind;
+ public int mAnimationDuration;
+
+ // This should be overridden in subclass to change the animation values
+ // give the progress value in [0, 1].
+ protected abstract boolean interpolate(float progress);
+ public abstract boolean startSnapback();
+
+ // Returns true if the animation values changes, so things need to be
+ // redrawn.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ }
+ if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ return startSnapback();
+ }
+
+ float progress;
+ if (mAnimationDuration == 0) {
+ progress = 1;
+ } else {
+ long now = AnimationTime.get();
+ progress =
+ (float) (now - mAnimationStartTime) / mAnimationDuration;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ } else {
+ progress = applyInterpolationCurve(mAnimationKind, progress);
+ }
+
+ boolean done = interpolate(progress);
+
+ if (done) {
+ mAnimationStartTime = LAST_ANIMATION;
+ }
+
+ return true;
+ }
+
+ private static float applyInterpolationCurve(int kind, float progress) {
+ float f = 1 - progress;
+ switch (kind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ case ANIM_KIND_DELETE:
+ case ANIM_KIND_CAPTURE:
+ progress = 1 - f; // linear
+ break;
+ case ANIM_KIND_OPENING:
+ case ANIM_KIND_SCALE:
+ progress = 1 - f * f; // quadratic
+ break;
+ case ANIM_KIND_SNAPBACK:
+ case ANIM_KIND_ZOOM:
+ case ANIM_KIND_SLIDE:
+ progress = 1 - f * f * f * f * f; // x^5
+ break;
+ }
+ return progress;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Platform: captures the global X/Y movement.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Platform extends Animatable {
+ public int mCurrentX, mFromX, mToX, mDefaultX;
+ public int mCurrentY, mFromY, mToY, mDefaultY;
+ public int mFlingOffset;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mInScale) return false;
+
+ Box b = mBoxes.get(0);
+ float scaleMin = mExtraScalingRange ?
+ b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
+ float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
+ int x = mCurrentX;
+ int y = mDefaultY;
+ if (mFilmMode) {
+ x = mDefaultX;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus point
+ // stay in the same position on screen, so we need to adjust
+ // target mCurrentX (which is the center of the focused
+ // box). The position of the focus point on screen (relative the
+ // the center of the view) is:
+ //
+ // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
+ // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
+ //
+ if (!viewWiderThanScaledImage(scale)) {
+ float scaleDiff = b.mCurrentScale - scale;
+ x += (int) (mFocusX * scaleDiff + 0.5f);
+ }
+ x = Utils.clamp(x, mBoundLeft, mBoundRight);
+ }
+ if (mCurrentX != x || mCurrentY != y) {
+ return doAnimation(x, y, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ // The updateDefaultXY() should be called whenever these variables
+ // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
+ // mFilmMode
+ public void updateDefaultXY() {
+ // We don't check mFilmMode and return 0 for mDefaultX. Because
+ // otherwise if we decide to leave film mode because we are
+ // centered, we will immediately back into film mode because we find
+ // we are not centered.
+ if (mConstrained && !mConstrainedFrame.isEmpty()) {
+ mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
+ mDefaultY = mFilmMode ? 0 :
+ mConstrainedFrame.centerY() - mViewH / 2;
+ } else {
+ mDefaultX = 0;
+ mDefaultY = 0;
+ }
+ }
+
+ // Starts an animation for the platform.
+ private boolean doAnimation(int targetX, int targetY, int kind) {
+ if (mCurrentX == targetX && mCurrentY == targetY) return false;
+ mAnimationKind = kind;
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mToX = targetX;
+ mToY = targetY;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ mFlingOffset = 0;
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else if (mAnimationKind == ANIM_KIND_FLING_X) {
+ return interpolateFlingFilm(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingFilm(float progress) {
+ mFilmScroller.computeScrollOffset();
+ mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+ int dir = EdgeView.INVALID_DIRECTION;
+ if (mCurrentX < mDefaultX) {
+ if (!mHasNext) {
+ dir = EdgeView.RIGHT;
+ }
+ } else if (mCurrentX > mDefaultX) {
+ if (!mHasPrev) {
+ dir = EdgeView.LEFT;
+ }
+ }
+ if (dir != EdgeView.INVALID_DIRECTION) {
+ // TODO: restore this onAbsorb call
+ //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+ //mListener.onAbsorb(v, dir);
+ mFilmScroller.forceFinished(true);
+ mCurrentX = mDefaultX;
+ }
+ return mFilmScroller.isFinished();
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ Box b = mBoxes.get(0);
+ calculateStableBound(b.mCurrentScale);
+
+ int oldX = mCurrentX;
+ mCurrentX = mPageScroller.getCurrX();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
+ int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.RIGHT);
+ } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
+ int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.LEFT);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ // Other animations
+ if (progress >= 1) {
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ return true;
+ } else {
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ progress = CaptureAnimation.calculateSlide(progress);
+ }
+ mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ return false;
+ } else {
+ return (mCurrentX == mToX && mCurrentY == mToY);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box: represents a rectangular area which shows a picture.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Box extends Animatable {
+ // Size of the bitmap
+ public int mImageW, mImageH;
+
+ // This is true if we assume the image size is the same as view size
+ // until we know the actual size of image. This is also used to
+ // determine if there is an image ready to show.
+ public boolean mUseViewSize;
+
+ // The minimum and maximum scale we allow for this box.
+ public float mScaleMin, mScaleMax;
+
+ // The X/Y value indicates where the center of the box is on the view
+ // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
+ // actual values used currently. Note that the X values are implicitly
+ // defined by Platform and Gaps.
+ public int mCurrentY, mFromY, mToY;
+ public float mCurrentScale, mFromScale, mToScale;
+
+ // The absolute X coordinate of the center of the box. This is only used
+ // during moveBox().
+ public int mAbsoluteX;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mAnimationKind == ANIM_KIND_DELETE
+ && mListener.isHoldingDelete()) return false;
+ if (mInScale && this == mBoxes.get(0)) return false;
+
+ int y = mCurrentY;
+ float scale;
+
+ if (this == mBoxes.get(0)) {
+ float scaleMin = mExtraScalingRange ?
+ mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+ scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+ if (mFilmMode) {
+ y = 0;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus
+ // point stay in the same position on screen. See the
+ // comment in Platform.startSnapback for details.
+ if (!viewTallerThanScaledImage(scale)) {
+ float scaleDiff = mCurrentScale - scale;
+ y += (int) (mFocusY * scaleDiff + 0.5f);
+ }
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+ }
+ } else {
+ y = 0;
+ scale = mScaleMin;
+ }
+
+ if (mCurrentY != y || mCurrentScale != scale) {
+ return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ private boolean doAnimation(int targetY, float targetScale, int kind) {
+ targetScale = clampScale(targetScale);
+
+ if (mCurrentY == targetY && mCurrentScale == targetScale
+ && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+
+ // Now starts an animation for the box.
+ mAnimationKind = kind;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+ mToY = targetY;
+ mToScale = targetScale;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ advanceAnimation();
+ return true;
+ }
+
+ // Clamps the input scale to the range that doAnimation() can reach.
+ public float clampScale(float s) {
+ return Utils.clamp(s,
+ SCALE_MIN_EXTRA * mScaleMin,
+ SCALE_MAX_EXTRA * mScaleMax);
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ calculateStableBound(mCurrentScale);
+
+ int oldY = mCurrentY;
+ mCurrentY = mPageScroller.getCurrY();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldY > mBoundTop && mCurrentY == mBoundTop) {
+ int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.BOTTOM);
+ } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
+ int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.TOP);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ if (progress >= 1) {
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ return true;
+ } else {
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentScale *= f;
+ return false;
+ } else {
+ return (mCurrentY == mToY && mCurrentScale == mToScale);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Gap: represents a rectangular area which is between two boxes.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Gap extends Animatable {
+ // The default gap size between two boxes. The value may vary for
+ // different image size of the boxes and for different modes (page or
+ // film).
+ public int mDefaultSize;
+
+ // The gap size between the two boxes.
+ public int mCurrentGap, mFromGap, mToGap;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for a gap.
+ public boolean doAnimation(int targetSize, int kind) {
+ if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+ mAnimationKind = kind;
+ mFromGap = mCurrentGap;
+ mToGap = targetSize;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentGap = mToGap;
+ return true;
+ } else {
+ mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentGap = (int) (mCurrentGap * f);
+ return false;
+ } else {
+ return (mCurrentGap == mToGap);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // FilmRatio: represents the progress of film mode change.
+ ////////////////////////////////////////////////////////////////////////////
+ private class FilmRatio extends Animatable {
+ // The film ratio: 1 means switching to film mode is complete, 0 means
+ // switching to page mode is complete.
+ public float mCurrentRatio, mFromRatio, mToRatio;
+
+ @Override
+ public boolean startSnapback() {
+ float target = mFilmMode ? 1f : 0f;
+ if (target == mToRatio) return false;
+ return doAnimation(target, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for the film ratio.
+ private boolean doAnimation(float targetRatio, int kind) {
+ mAnimationKind = kind;
+ mFromRatio = mCurrentRatio;
+ mToRatio = targetRatio;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentRatio = mToRatio;
+ return true;
+ } else {
+ mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
+ return (mCurrentRatio == mToRatio);
+ }
+ }
+ }
+}