/* * 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 mBoxes = new RangeArray(-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 mGaps = new RangeArray(-BOX_MAX, BOX_MAX - 1); private FilmRatio mFilmRatio = new FilmRatio(); // These are only used during moveBox(). private RangeArray mTempBoxes = new RangeArray(-BOX_MAX, BOX_MAX); private RangeArray mTempGaps = new RangeArray(-BOX_MAX, BOX_MAX - 1); // The output of the PositionController. Available through getPosition(). private RangeArray mRects = new RangeArray(-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); } } } }