/*
* 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.dwdesign.gallery3d.ui;
import com.dwdesign.gallery3d.ui.PhotoView.Size;
import com.dwdesign.gallery3d.util.GalleryUtils;
import android.graphics.Rect;
class 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;
// 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;
private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
private final 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;
// This is used by the fling animation (page mode).
private final FlingScroller mPageScroller;
// 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 final 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 final boolean mConstrained = true;
private final Platform mPlatform = new Platform();
private final Box mBox;
// The output of the PositionController. Available through getPosition().
private final Rect mRect;
public PositionController(final Listener listener) {
mListener = listener;
mPageScroller = new FlingScroller();
// Initialize the areas.
initPlatform();
mBox = new Box();
initBox();
mRect = new Rect();
}
public void advanceAnimation() {
boolean changed = false;
changed |= mPlatform.advanceAnimation();
changed |= mBox.advanceAnimation();
if (changed) {
redraw();
}
}
public void beginScale(float focusX, float focusY) {
focusX -= mViewW / 2;
focusY -= mViewH / 2;
final Platform p = mPlatform;
mInScale = true;
mFocusX = (int) ((focusX - p.mCurrentX) / mBox.mCurrentScale + 0.5f);
mFocusY = (int) ((focusY - mBox.mCurrentY) / mBox.mCurrentScale + 0.5f);
}
public void endScale() {
mInScale = false;
snapAndRedraw();
}
public boolean flingPage(int velocityX, int velocityY) {
final Platform p = mPlatform;
// We only want to do fling when the picture is zoomed-in.
if (viewWiderThanScaledImage(mBox.mCurrentScale) && viewTallerThanScaledImage(mBox.mCurrentScale))
return false;
// We only allow flinging in the directions where it won't go over the
// picture.
final 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, mBox.mCurrentY, velocityX, velocityY, mBoundLeft, mBoundRight, mBoundTop,
mBoundBottom);
final int targetX = mPageScroller.getFinalX();
final int targetY = mPageScroller.getFinalY();
ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
return startAnimation(targetX, targetY, mBox.mCurrentScale, ANIM_KIND_FLING);
}
public void forceImageSize(final Size s) {
if (s.width == 0 || s.height == 0) return;
mBox.mImageW = s.width;
mBox.mImageH = s.height;
return;
}
public int getImageAtEdges() {
final Platform p = mPlatform;
calculateStableBound(mBox.mCurrentScale);
int edges = 0;
if (p.mCurrentX <= mBoundLeft) {
edges |= IMAGE_AT_RIGHT_EDGE;
}
if (p.mCurrentX >= mBoundRight) {
edges |= IMAGE_AT_LEFT_EDGE;
}
if (mBox.mCurrentY <= mBoundTop) {
edges |= IMAGE_AT_BOTTOM_EDGE;
}
if (mBox.mCurrentY >= mBoundBottom) {
edges |= IMAGE_AT_TOP_EDGE;
}
return edges;
}
public int getImageHeight() {
return mBox.mImageH;
}
public float getImageScale() {
return mBox.mCurrentScale;
}
public int getImageWidth() {
return mBox.mImageW;
}
// Returns the position of a box.
public Rect getPosition() {
return mRect;
}
// 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(final int x, final int y) {
final Rect r = mRect;
if (r.contains(x, y)) return 0;
return Integer.MAX_VALUE;
}
public boolean isAtMinimalScale() {
return isAlmostEqual(mBox.mCurrentScale, mBox.mScaleMin);
}
// //////////////////////////////////////////////////////////////////////////
// Start an animations for the focused box
// //////////////////////////////////////////////////////////////////////////
public boolean isCenter() {
return mPlatform.mCurrentX == mPlatform.mDefaultX && mBox.mCurrentY == 0;
}
public boolean isScrolling() {
return mPlatform.mAnimationStartTime != NO_ANIMATION && mPlatform.mCurrentX != mPlatform.mToX;
}
public void resetToFullView() {
startAnimation(mPlatform.mDefaultX, 0, mBox.mScaleMin, ANIM_KIND_ZOOM);
}
// 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;
// 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 = mBox.clampScale(s * getTargetScale(mBox));
final int x = (int) (focusX - s * mFocusX + 0.5f);
final int y = (int) (focusY - s * mFocusY + 0.5f);
startAnimation(x, y, s, ANIM_KIND_SCALE);
if (s < mBox.mScaleMin) return -1;
if (s > mBox.mScaleMax) return 1;
return 0;
}
public void scrollPage(final int dx, final int dy) {
if (!canScroll()) return;
calculateStableBound(mBox.mCurrentScale);
int x = mPlatform.mCurrentX + dx;
int y = mBox.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 = GalleryUtils.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 (mBoundLeft != mBoundRight) {
if (x > mBoundRight) {
mListener.onPull(x - mBoundRight, EdgeView.LEFT);
} else if (x < mBoundLeft) {
mListener.onPull(mBoundLeft - x, EdgeView.RIGHT);
}
}
x = GalleryUtils.clamp(x, mBoundLeft, mBoundRight);
startAnimation(x, y, mBox.mCurrentScale, ANIM_KIND_SCROLL);
}
public void setConstrainedFrame(final Rect cFrame) {
if (mConstrainedFrame.equals(cFrame)) return;
mConstrainedFrame.set(cFrame);
mPlatform.updateDefaultXY();
updateScaleAndGapLimit();
snapAndRedraw();
}
public void setExtraScalingRange(final boolean enabled) {
if (mExtraScalingRange == enabled) return;
mExtraScalingRange = enabled;
if (!enabled) {
snapAndRedraw();
}
}
public void setFilmMode(final boolean enabled) {
mPlatform.updateDefaultXY();
updateScaleAndGapLimit();
stopAnimation();
snapAndRedraw();
}
public void setImageSize(final Size s, final 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(s.width, s.height, false);
if (!needUpdate) return;
updateScaleAndGapLimit();
snapAndRedraw();
}
public void setOpenAnimationRect(final Rect r) {
mOpenAnimationRect = r;
}
public void setViewSize(final int viewW, final int viewH) {
if (viewW == mViewW && viewH == mViewH) return;
final boolean wasMinimal = isAtMinimalScale();
mViewW = viewW;
mViewH = viewH;
initPlatform();
setBoxSize(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) {
mBox.mCurrentScale = mBox.mScaleMin;
}
// If we have the opening animation, do it. Otherwise go directly to the
// right position.
if (!startOpeningAnimationIfNeeded()) {
skipToFinalPosition();
}
}
public void skipAnimation() {
if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
mPlatform.mCurrentX = mPlatform.mToX;
mPlatform.mCurrentY = mPlatform.mToY;
mPlatform.mAnimationStartTime = NO_ANIMATION;
}
if (mBox.mAnimationStartTime != NO_ANIMATION) {
mBox.mCurrentY = mBox.mToY;
mBox.mCurrentScale = mBox.mToScale;
mBox.mAnimationStartTime = NO_ANIMATION;
}
redraw();
}
public void skipToFinalPosition() {
stopAnimation();
snapAndRedraw();
skipAnimation();
}
public void snapback() {
snapAndRedraw();
}
// //////////////////////////////////////////////////////////////////////////
// Layout
// //////////////////////////////////////////////////////////////////////////
public void zoomIn(float tapX, float tapY, float targetScale) {
tapX -= mViewW / 2;
tapY -= mViewH / 2;
// Convert the tap position to distance to center in bitmap coordinates
final float tempX = (tapX - mPlatform.mCurrentX) / mBox.mCurrentScale;
final float tempY = (tapY - mBox.mCurrentY) / mBox.mCurrentScale;
final int x = (int) (-tempX * targetScale + 0.5f);
final int y = (int) (-tempY * targetScale + 0.5f);
calculateStableBound(targetScale);
final int targetX = GalleryUtils.clamp(x, mBoundLeft, mBoundRight);
final int targetY = GalleryUtils.clamp(y, mBoundTop, mBoundBottom);
targetScale = GalleryUtils.clamp(targetScale, mBox.mScaleMin, mBox.mScaleMax);
startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
}
private void calculateStableBound(final float scale) {
calculateStableBound(scale, 0);
}
// Calculates the stable region of mPlatform.mCurrentX and
// mBox.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(final float scale, final int horizontalSlack) {
// The width and height of the box in number of view pixels
final int w = widthOf(mBox, scale);
final int h = heightOf(mBox, 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;
}
}
// Only allow scrolling when we are not currently in an animation or we
// are in some animation with can be interrupted.
private boolean canScroll() {
if (mBox.mAnimationStartTime == NO_ANIMATION) return true;
switch (mBox.mAnimationKind) {
case ANIM_KIND_SCROLL:
case ANIM_KIND_FLING:
return true;
}
return false;
}
private void convertBoxToRect() {
final Rect r = mRect;
final int y = mBox.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
final int w = widthOf(mBox);
final int h = heightOf(mBox);
final int x = mPlatform.mCurrentX + mViewW / 2;
r.left = x - w / 2;
r.right = r.left + w;
r.top = y - h / 2;
r.bottom = r.top + h;
}
private float getMaximalScale(final Box b) {
if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
return SCALE_LIMIT;
}
private float getMinimalScale(final Box b) {
final float wFactor = 1.0f;
final float hFactor = 1.0f;
int viewW, viewH;
if (mConstrained && !mConstrainedFrame.isEmpty() && b == mBox) {
viewW = mConstrainedFrame.width();
viewH = mConstrainedFrame.height();
} else {
viewW = mViewW;
viewH = mViewH;
}
final float s = Math.min(wFactor * viewW / b.mImageW, hFactor * viewH / b.mImageH);
return Math.min(SCALE_LIMIT, s);
}
private float getTargetScale(final Box b) {
return b.mAnimationStartTime == NO_ANIMATION ? b.mCurrentScale : b.mToScale;
}
// Returns the display height of this box.
private int heightOf(final Box b) {
return (int) (b.mImageH * b.mCurrentScale + 0.5f);
}
// Returns the display height of this box, using the given scale.
private int heightOf(final Box b, final float scale) {
return (int) (b.mImageH * scale + 0.5f);
}
// //////////////////////////////////////////////////////////////////////////
// Public utilities
// //////////////////////////////////////////////////////////////////////////
// Initialize a box to have the size of the view.
private void initBox() {
mBox.mImageW = mViewW;
mBox.mImageH = mViewH;
mBox.mUseViewSize = true;
mBox.mScaleMin = getMinimalScale(mBox);
mBox.mScaleMax = getMaximalScale(mBox);
mBox.mCurrentY = 0;
mBox.mCurrentScale = mBox.mScaleMin;
mBox.mAnimationStartTime = NO_ANIMATION;
mBox.mAnimationKind = ANIM_KIND_NONE;
}
// 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;
}
// 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 * 0 + 1; i++) {
convertBoxToRect();
}
}
// //////////////////////////////////////////////////////////////////////////
// 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.onInvalidate();
}
// Returns false if the box size doesn't change.
private boolean setBoxSize(final int width, final int height, final boolean isViewSize) {
final boolean wasViewSize = mBox.mUseViewSize;
// If we already have an image size, we don't want to use the view size.
if (!wasViewSize && isViewSize) return false;
mBox.mUseViewSize = isViewSize;
if (width == mBox.mImageW && height == mBox.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.
final float ratio = width > height ? (float) mBox.mImageW / width : (float) mBox.mImageH / height;
mBox.mImageW = width;
mBox.mImageH = height;
mBox.mCurrentScale = getMinimalScale(mBox);
mBox.mAnimationStartTime = NO_ANIMATION;
mFocusX /= ratio;
mFocusY /= ratio;
return true;
}
private void snapAndRedraw() {
mPlatform.startSnapback();
mBox.startSnapback();
redraw();
}
private boolean startAnimation(final int targetX, final int targetY, final float targetScale, final int kind) {
boolean changed = false;
changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
changed |= mBox.doAnimation(targetY, targetScale, kind);
if (changed) {
redraw();
}
return changed;
}
private boolean startOpeningAnimationIfNeeded() {
if (mOpenAnimationRect == null) return false;
if (mBox.mUseViewSize) return false;
// Start animation from the saved rectangle if we have one.
final Rect r = mOpenAnimationRect;
mOpenAnimationRect = null;
mPlatform.mCurrentX = r.centerX() - mViewW / 2;
mBox.mCurrentY = r.centerY() - mViewH / 2;
mBox.mCurrentScale = Math.max(r.width() / (float) mBox.mImageW, r.height() / (float) mBox.mImageH);
startAnimation(mPlatform.mDefaultX, 0, mBox.mScaleMin, ANIM_KIND_OPENING);
return true;
}
// //////////////////////////////////////////////////////////////////////////
// Private utilities
// //////////////////////////////////////////////////////////////////////////
// Stop all animations at where they are now.
private void stopAnimation() {
mPlatform.mAnimationStartTime = NO_ANIMATION;
mBox.mAnimationStartTime = NO_ANIMATION;
}
// 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() {
mBox.mScaleMin = getMinimalScale(mBox);
mBox.mScaleMax = getMaximalScale(mBox);
}
private boolean viewTallerThanScaledImage(final float scale) {
return mViewH >= heightOf(mBox, scale);
}
private boolean viewWiderThanScaledImage(final float scale) {
return mViewW >= widthOf(mBox, scale);
}
// Returns the display width of this box.
private int widthOf(final Box b) {
return (int) (b.mImageW * b.mCurrentScale + 0.5f);
}
// Returns the display width of this box, using the given scale.
private int widthOf(final Box b, final float scale) {
return (int) (b.mImageW * scale + 0.5f);
}
private static boolean isAlmostEqual(final float a, final float b) {
final float diff = a - b;
return (diff < 0 ? -diff : diff) < 0.02f;
}
public interface Listener {
boolean isHoldingDown();
void onAbsorb(int velocity, int direction);
void onInvalidate();
// EdgeView
void onPull(int offset, int direction);
}
// //////////////////////////////////////////////////////////////////////////
// Animatable: an thing which can do animation.
// //////////////////////////////////////////////////////////////////////////
private abstract static class Animatable {
public long mAnimationStartTime;
public int mAnimationKind;
public int mAnimationDuration;
// 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 {
final long now = AnimationTime.get();
progress = (float) (now - mAnimationStartTime) / mAnimationDuration;
}
if (progress >= 1) {
progress = 1;
} else {
progress = applyInterpolationCurve(mAnimationKind, progress);
}
final boolean done = interpolate(progress);
if (done) {
mAnimationStartTime = LAST_ANIMATION;
}
return true;
}
public abstract boolean startSnapback();
// This should be overridden in subclass to change the animation values
// give the progress value in [0, 1].
protected abstract boolean interpolate(float progress);
private static float applyInterpolationCurve(final int kind, float progress) {
final float f = 1 - progress;
switch (kind) {
case ANIM_KIND_SCROLL:
case ANIM_KIND_FLING:
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;
}
}
// //////////////////////////////////////////////////////////////////////////
// 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;
// Clamps the input scale to the range that doAnimation() can reach.
public float clampScale(final float s) {
return GalleryUtils.clamp(s, SCALE_MIN_EXTRA * mScaleMin, SCALE_MAX_EXTRA * mScaleMax);
}
@Override
public boolean startSnapback() {
if (mAnimationStartTime != NO_ANIMATION) return false;
if (mAnimationKind == ANIM_KIND_SCROLL && mListener.isHoldingDown()) return false;
if (mInScale && this == mBox) return false;
int y = mCurrentY;
float scale;
if (this == mBox) {
final float scaleMin = mExtraScalingRange ? mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
final float scaleMax = mExtraScalingRange ? mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
scale = GalleryUtils.clamp(mCurrentScale, scaleMin, scaleMax);
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)) {
final float scaleDiff = mCurrentScale - scale;
y += (int) (mFocusY * scaleDiff + 0.5f);
}
y = GalleryUtils.clamp(y, mBoundTop, mBoundBottom);
} else {
y = 0;
scale = mScaleMin;
}
if (mCurrentY != y || mCurrentScale != scale) return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
return false;
}
@Override
protected boolean interpolate(final float progress) {
if (mAnimationKind == ANIM_KIND_FLING)
return interpolateFlingPage(progress);
else
return interpolateLinear(progress);
}
private boolean doAnimation(final int targetY, float targetScale, final int kind) {
targetScale = clampScale(targetScale);
if (mCurrentY == targetY && mCurrentScale == targetScale) 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;
}
private boolean interpolateFlingPage(final float progress) {
mPageScroller.computeScrollOffset(progress);
calculateStableBound(mCurrentScale);
final int oldY = mCurrentY;
mCurrentY = mPageScroller.getCurrY();
// Check if we hit the edges; show edge effects if we do.
if (oldY > mBoundTop && mCurrentY == mBoundTop) {
final int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
mListener.onAbsorb(v, EdgeView.BOTTOM);
} else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
final int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
mListener.onAbsorb(v, EdgeView.TOP);
}
return progress >= 1;
}
private boolean interpolateLinear(final float progress) {
if (progress >= 1) {
mCurrentY = mToY;
mCurrentScale = mToScale;
return true;
} else {
mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
return mCurrentY == mToY && mCurrentScale == mToScale;
}
}
}
// //////////////////////////////////////////////////////////////////////////
// Platform: captures the global X/Y movement.
// //////////////////////////////////////////////////////////////////////////
private class Platform extends Animatable {
public int mCurrentX, mFromX, mToX, mDefaultX;
public int mCurrentY, mFromY, mToY, mDefaultY;
@Override
public boolean startSnapback() {
if (mAnimationStartTime != NO_ANIMATION) return false;
if (mAnimationKind == ANIM_KIND_SCROLL && mListener.isHoldingDown()) return false;
if (mInScale) return false;
final float scaleMin = mExtraScalingRange ? mBox.mScaleMin * SCALE_MIN_EXTRA : mBox.mScaleMin;
final float scaleMax = mExtraScalingRange ? mBox.mScaleMax * SCALE_MAX_EXTRA : mBox.mScaleMax;
final float scale = GalleryUtils.clamp(mBox.mCurrentScale, scaleMin, scaleMax);
int x = mCurrentX;
final int y = mDefaultY;
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)) {
final float scaleDiff = mBox.mCurrentScale - scale;
x += (int) (mFocusX * scaleDiff + 0.5f);
}
x = GalleryUtils.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 = mConstrainedFrame.centerY() - mViewH / 2;
} else {
mDefaultX = 0;
mDefaultY = 0;
}
}
@Override
protected boolean interpolate(final float progress) {
if (mAnimationKind == ANIM_KIND_FLING)
return interpolateFlingPage(progress);
else
return interpolateLinear(progress);
}
// Starts an animation for the platform.
private boolean doAnimation(final int targetX, final int targetY, final 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];
advanceAnimation();
return true;
}
private boolean interpolateFlingPage(final float progress) {
mPageScroller.computeScrollOffset(progress);
calculateStableBound(mBox.mCurrentScale);
final int oldX = mCurrentX;
mCurrentX = mPageScroller.getCurrX();
// Check if we hit the edges; show edge effects if we do.
if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
final int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
mListener.onAbsorb(v, EdgeView.RIGHT);
} else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
final int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
mListener.onAbsorb(v, EdgeView.LEFT);
}
return progress >= 1;
}
private boolean interpolateLinear(final float progress) {
// Other animations
if (progress >= 1) {
mCurrentX = mToX;
mCurrentY = mToY;
return true;
} else {
mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
return mCurrentX == mToX && mCurrentY == mToY;
}
}
}
}