| /* |
| * 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); |
| } |
| } |
| } |
| } |