diff options
Diffstat (limited to 'src/com/android/gallery3d/ui/PositionController.java')
-rw-r--r-- | src/com/android/gallery3d/ui/PositionController.java | 1821 |
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); + } + } + } +} |