import com.android.gallery3d.R;
import com.android.gallery3d.app.GalleryActivity;
import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeBoolArray;
+
+import java.util.Arrays;
public class PhotoView extends GLView {
@SuppressWarnings("unused")
private static final String TAG = "PhotoView";
public static final int INVALID_SIZE = -1;
+ public static final long INVALID_DATA_VERSION =
+ MediaObject.INVALID_DATA_VERSION;
- private static final int MSG_TRANSITION_COMPLETE = 1;
- private static final int MSG_SHOW_LOADING = 2;
- private static final int MSG_CANCEL_EXTRA_SCALING = 3;
+ public static interface Model extends TileImageView.Model {
+ public void next();
+ public void previous();
+ public int getImageRotation();
- private static final long DELAY_SHOW_LOADING = 250; // 250ms;
+ // This amends the getScreenNail() method of TileImageView.Model to get
+ // ScreenNail at previous (negative offset) or next (positive offset)
+ // positions. Returns null if the specified ScreenNail is unavailable.
+ public ScreenNail getScreenNail(int offset);
+ }
+
+ public interface PhotoTapListener {
+ public void onSingleTapUp(int x, int y);
+ }
- private static final int TRANS_NONE = 0;
- private static final int TRANS_SWITCH_NEXT = 3;
- private static final int TRANS_SWITCH_PREVIOUS = 4;
+ private static final int MSG_SHOW_LOADING = 1;
+ private static final int MSG_CANCEL_EXTRA_SCALING = 2;
+ private static final int MSG_SWITCH_FOCUS = 3;
- public static final int TRANS_SLIDE_IN_RIGHT = 1;
- public static final int TRANS_SLIDE_IN_LEFT = 2;
- public static final int TRANS_OPEN_ANIMATION = 5;
+ private static final long DELAY_SHOW_LOADING = 250; // 250ms;
private static final int LOADING_INIT = 0;
private static final int LOADING_TIMEOUT = 1;
private static final int LOADING_COMPLETE = 2;
private static final int LOADING_FAIL = 3;
- private static final int ENTRY_PREVIOUS = 0;
- private static final int ENTRY_NEXT = 1;
-
- private static final int IMAGE_GAP = 96;
- private static final int SWITCH_THRESHOLD = 256;
+ private static final int MOVE_THRESHOLD = 256;
private static final float SWIPE_THRESHOLD = 300f;
private static final float DEFAULT_TEXT_SIZE = 20;
private static float TRANSITION_SCALE_FACTOR = 0.74f;
+ private static final boolean CARD_EFFECT = true;
// Used to calculate the scaling factor for the fading animation.
private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
private AccelerateInterpolator mAlphaInterpolator =
new AccelerateInterpolator(0.9f);
- public interface PhotoTapListener {
- public void onSingleTapUp(int x, int y);
- }
+ // We keep this many previous ScreenNails. (also this many next ScreenNails)
+ public static final int SCREEN_NAIL_MAX = 3;
+
+ // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
+ // SCREEN_NAIL_MAX.
+ private final RangeArray<Picture> mPictures =
+ new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+ private final RangeBoolArray mReused =
+ new RangeBoolArray(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+ private final RangeArray<ScreenNail> mTempScreenNail =
+ new RangeArray<ScreenNail>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
- // the previous/next image entries
- private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
+ private final long mDataVersion[] = new long[2 * SCREEN_NAIL_MAX + 1];
+ private final int mFromIndex[] = new int[2 * SCREEN_NAIL_MAX + 1];
private final GestureRecognizer mGestureRecognizer;
private Model mModel;
private StringTexture mLoadingText;
private StringTexture mNoThumbnailText;
- private int mTransitionMode = TRANS_NONE;
- private final TileImageView mTileView;
+ private TileImageView mTileView;
private EdgeView mEdgeView;
private Texture mVideoPlayIcon;
private int mLoadingState = LOADING_COMPLETE;
- private int mImageRotation;
-
- private Rect mOpenAnimationRect;
private Point mImageCenter = new Point();
private boolean mCancelExtraScalingPending;
+ private boolean mFilmMode = false;
public PhotoView(GalleryActivity activity) {
mTileView = new TileImageView(activity);
context.getString(R.string.no_thumbnail),
DEFAULT_TEXT_SIZE, Color.WHITE);
- mHandler = new SynchronizedHandler(activity.getGLRoot()) {
- @Override
- public void handleMessage(Message message) {
- switch (message.what) {
- case MSG_TRANSITION_COMPLETE: {
- onTransitionComplete();
- break;
- }
- case MSG_SHOW_LOADING: {
- if (mLoadingState == LOADING_INIT) {
- // We don't need the opening animation
- mOpenAnimationRect = null;
-
- mLoadingSpinner.startAnimation();
- mLoadingState = LOADING_TIMEOUT;
- invalidate();
- }
- break;
- }
- case MSG_CANCEL_EXTRA_SCALING: {
- mGestureRecognizer.cancelScale();
- mPositionController.setExtraScalingRange(false);
- mCancelExtraScalingPending = false;
- break;
- }
- default: throw new AssertionError(message.what);
- }
- }
- };
+ mHandler = new MyHandler(activity.getGLRoot());
mGestureRecognizer = new GestureRecognizer(
context, new MyGestureListener());
- for (int i = 0, n = mScreenNails.length; i < n; ++i) {
- mScreenNails[i] = new ScreenNailEntry();
- }
-
- mPositionController = new PositionController(this, context, mEdgeView);
+ mPositionController = new PositionController(context,
+ new PositionController.Listener() {
+ public void invalidate() {
+ PhotoView.this.invalidate();
+ }
+ public boolean isDown() {
+ return mGestureRecognizer.isDown();
+ }
+ public void onPull(int offset, int direction) {
+ mEdgeView.onPull(offset, direction);
+ }
+ public void onRelease() {
+ mEdgeView.onRelease();
+ }
+ public void onAbsorb(int velocity, int direction) {
+ mEdgeView.onAbsorb(velocity, direction);
+ }
+ });
mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
- }
-
-
- public void setModel(Model model) {
- if (mModel == model) return;
- mModel = model;
- mTileView.setModel(model);
- if (model != null) notifyOnNewImage();
- }
-
- public void setPhotoTapListener(PhotoTapListener listener) {
- mPhotoTapListener = listener;
- }
-
- private void setTileViewPosition(int centerX, int centerY, float scale) {
- TileImageView t = mTileView;
-
- // Calculate the move-out progress value.
- RectF bounds = mPositionController.getImageBounds();
- int left = Math.round(bounds.left);
- int right = Math.round(bounds.right);
- int width = getWidth();
- float progress = calculateMoveOutProgress(left, right, width);
- progress = Utils.clamp(progress, -1f, 1f);
-
- // We only want to apply the fading animation if the scrolling movement
- // is to the right.
- if (progress < 0) {
- if (right - left < width) {
- // If the picture is narrower than the view, keep it at the center
- // of the view.
- centerX = mPositionController.getImageWidth() / 2;
+ Arrays.fill(mDataVersion, INVALID_DATA_VERSION);
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ if (i == 0) {
+ mPictures.put(i, new FullPicture());
} else {
- // If the picture is wider than the view (it's zoomed-in), keep
- // the left edge of the object align the the left edge of the view.
- centerX = Math.round(width / 2f / scale);
+ mPictures.put(i, new ScreenNailPicture(i));
}
- scale *= getScrollScale(progress);
- t.setAlpha(getScrollAlpha(progress));
- }
-
- // set the position of the tile view
- int inverseX = mPositionController.getImageWidth() - centerX;
- int inverseY = mPositionController.getImageHeight() - centerY;
- int rotation = mImageRotation;
- switch (rotation) {
- case 0: t.setPosition(centerX, centerY, scale, 0); break;
- case 90: t.setPosition(centerY, inverseX, scale, 90); break;
- case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
- case 270: t.setPosition(inverseY, centerX, scale, 270); break;
- default: throw new IllegalArgumentException(String.valueOf(rotation));
}
}
- public void setPosition(int centerX, int centerY, float scale) {
- setTileViewPosition(centerX, centerY, scale);
- layoutScreenNails();
+ public void setModel(Model model) {
+ mModel = model;
+ mTileView.setModel(mModel);
}
- private void updateScreenNailEntry(int which, ScreenNail screenNail) {
- if (mTransitionMode == TRANS_SWITCH_NEXT
- || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
- // ignore screen nail updating during switching
- return;
+ class MyHandler extends SynchronizedHandler {
+ public MyHandler(GLRoot root) {
+ super(root);
}
- ScreenNailEntry entry = mScreenNails[which];
- entry.updateScreenNail(screenNail);
- }
- // -1 previous, 0 current, 1 next
- public void notifyImageInvalidated(int which) {
- switch (which) {
- case -1: {
- updateScreenNailEntry(
- ENTRY_PREVIOUS, mModel.getPrevScreenNail());
- layoutScreenNails();
- invalidate();
- break;
- }
- case 1: {
- updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
- layoutScreenNails();
- invalidate();
- break;
- }
- case 0: {
- // mImageWidth and mImageHeight will get updated
- mTileView.notifyModelInvalidated();
- mTileView.setAlpha(1.0f);
-
- mImageRotation = mModel.getImageRotation();
- if (((mImageRotation / 90) & 1) == 0) {
- mPositionController.setImageSize(
- mTileView.mImageWidth, mTileView.mImageHeight);
- } else {
- mPositionController.setImageSize(
- mTileView.mImageHeight, mTileView.mImageWidth);
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SHOW_LOADING: {
+ if (mLoadingState == LOADING_INIT) {
+ // We don't need the opening animation
+ mPositionController.setOpenAnimationRect(null);
+
+ mLoadingSpinner.startAnimation();
+ mLoadingState = LOADING_TIMEOUT;
+ invalidate();
+ }
+ break;
}
- updateLoadingState();
- break;
+ case MSG_CANCEL_EXTRA_SCALING: {
+ mGestureRecognizer.cancelScale();
+ mPositionController.setExtraScalingRange(false);
+ mCancelExtraScalingPending = false;
+ break;
+ }
+ case MSG_SWITCH_FOCUS: {
+ switchFocus();
+ break;
+ }
+ default: throw new AssertionError(message.what);
}
}
- }
+ };
private void updateLoadingState() {
// Possible transitions of mLoadingState:
mHandler.removeMessages(MSG_SHOW_LOADING);
mLoadingState = LOADING_FAIL;
// We don't want the opening animation after loading failure
- mOpenAnimationRect = null;
+ mPositionController.setOpenAnimationRect(null);
} else if (mLoadingState != LOADING_INIT) {
mLoadingState = LOADING_INIT;
mHandler.removeMessages(MSG_SHOW_LOADING);
}
}
- public void notifyModelInvalidated() {
- if (mModel == null) {
- updateScreenNailEntry(ENTRY_PREVIOUS, null);
- updateScreenNailEntry(ENTRY_NEXT, null);
- } else {
- updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPrevScreenNail());
- updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
+ ////////////////////////////////////////////////////////////////////////////
+ // Data/Image change notifications
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void notifyDataChange(long[] versions) {
+ // Check if the data version actually changed.
+ boolean changed = false;
+ int N = 2 * SCREEN_NAIL_MAX + 1;
+ for (int i = 0; i < N; i++) {
+ if (versions[i] != mDataVersion[i]) {
+ changed = true;
+ break;
+ }
}
- layoutScreenNails();
+ if (!changed) return;
- if (mModel == null) {
- mTileView.notifyModelInvalidated();
- mTileView.setAlpha(1.0f);
- mImageRotation = 0;
- mPositionController.setImageSize(0, 0);
- updateLoadingState();
- } else {
- notifyImageInvalidated(0);
+ // Remembers those ScreenNail which are reused.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ mReused.put(i, false);
}
- }
- @Override
- protected boolean onTouch(MotionEvent event) {
- mGestureRecognizer.onTouchEvent(event);
- return true;
- }
+ // Create the mFromIndex array, which records the index where the picture
+ // come from. The value Integer.MAX_VALUE means it's a new picture.
+ for (int i = 0; i < N; i++) {
+ long v = versions[i];
+ if (v == INVALID_DATA_VERSION) {
+ mFromIndex[i] = Integer.MAX_VALUE;
+ continue;
+ }
- @Override
- protected void onLayout(
- boolean changeSize, int left, int top, int right, int bottom) {
- mTileView.layout(left, top, right, bottom);
- mEdgeView.layout(left, top, right, bottom);
- if (changeSize) {
- mPositionController.setViewSize(getWidth(), getHeight());
- for (ScreenNailEntry entry : mScreenNails) {
- entry.updateDrawingSize();
+ // Try to find the same version number in the old array
+ int j;
+ for (j = 0; j < N; j++) {
+ if (mDataVersion[j] == v) {
+ mReused.put(j - SCREEN_NAIL_MAX, true);
+ break;
+ }
}
+ mFromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
}
+
+ // Copy the new data version
+ for (int i = 0; i < N; i++) {
+ mDataVersion[i] = versions[i];
+ }
+
+ // Move the boxes
+ mPositionController.moveBox(mFromIndex);
+
+ // Free those ScreenNails that are not reused.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ if (!mReused.get(i)) mPictures.get(i).updateScreenNail(null);
+ }
+
+ // Collect the reused ScreenNails, so we don't need to re-upload the
+ // textures.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ mTempScreenNail.put(i, mPictures.get(i).releaseScreenNail());
+ }
+
+ // Put back the reused ScreenNails.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ int j = mFromIndex[i + SCREEN_NAIL_MAX];
+ if (j != Integer.MAX_VALUE) {
+ ScreenNail s = mTempScreenNail.get(j);
+ mTempScreenNail.put(j, null);
+ mPictures.get(i).updateScreenNail(s);
+ }
+ mPictures.get(i).reload();
+ }
+
+ invalidate();
}
- private static int gapToSide(int imageWidth, int viewWidth) {
- return Math.max(0, (viewWidth - imageWidth) / 2);
+ public void notifyImageChange(int index) {
+ mPictures.get(index).reload();
+ invalidate();
}
- /*
- * Here is how we layout the screen nails
- *
- * previous current next
- * ___________ ________________ __________
- * | _______ | | __________ | | ______ |
- * | | | | | | right->| | | | | |
- * | | |<-------->|<--left | | | | | |
- * | |_______| | | | |__________| | | |______| |
- * |___________| | |________________| |__________|
- * | <--> gapToSide()
- * |
- * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
- */
- private void layoutScreenNails() {
- int width = getWidth();
- int height = getHeight();
+ ////////////////////////////////////////////////////////////////////////////
+ // Pictures
+ ////////////////////////////////////////////////////////////////////////////
+
+ private interface Picture {
+ void reload();
+ void draw(GLCanvas canvas, Rect r);
- // Use the image width in AC, since we may fake the size if the
- // image is unavailable
- RectF bounds = mPositionController.getImageBounds();
- int left = Math.round(bounds.left);
- int right = Math.round(bounds.right);
- int gap = gapToSide(right - left, width);
+ void updateScreenNail(ScreenNail s);
+ // Release the ownership of the ScreenNail from this entry.
+ ScreenNail releaseScreenNail();
+
+ boolean isEnabled();
+ };
- // layout the previous image
- ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
+ class FullPicture implements Picture {
+ private int mRotation;
- if (entry.isEnabled()) {
- entry.layoutRightEdgeAt(left - (
- IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ // This is a temporary hack to switch mode when entering/leaving camera.
+ private volatile boolean mIsNonBitmap;
+
+ public void FullPicture(TileImageView tileView) {
+ mTileView = tileView;
}
- // layout the next image
- entry = mScreenNails[ENTRY_NEXT];
- if (entry.isEnabled()) {
- entry.layoutLeftEdgeAt(right + (
- IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ @Override
+ public void reload() {
+ // mImageWidth and mImageHeight will get updated
+ mTileView.notifyModelInvalidated();
+ if (CARD_EFFECT) mTileView.setAlpha(1.0f);
+
+ if (mModel == null) {
+ mRotation = 0;
+ mPositionController.setImageSize(0, 0, 0);
+ } else {
+ mRotation = mModel.getImageRotation();
+ int w = mTileView.mImageWidth;
+ int h = mTileView.mImageHeight;
+ mPositionController.setImageSize(0,
+ getRotated(mRotation, w, h),
+ getRotated(mRotation, h, w));
+ }
+ updateScreenNail(mModel == null
+ ? null : mModel.getScreenNail(0));
+ updateLoadingState();
}
- }
- @Override
- protected void render(GLCanvas canvas) {
- boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT
- && mTransitionMode != TRANS_SLIDE_IN_RIGHT
- && mTransitionMode != TRANS_OPEN_ANIMATION);
+ @Override
+ public void draw(GLCanvas canvas, Rect r) {
+ if (mLoadingState == LOADING_COMPLETE) {
+ setTileViewPosition(r);
+ PhotoView.super.render(canvas);
+ }
+ renderMessage(canvas, r.centerX(), r.centerY());
- // Draw the next photo
- if (drawScreenNail) {
- ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
- nextNail.draw(canvas, true);
+ boolean isCenter = r.centerX() == getWidth() / 2;
+ if (mIsNonBitmap && !isCenter && !mFilmMode) {
+ setFilmMode(true);
+ } else if (mIsNonBitmap && isCenter && mFilmMode) {
+ setFilmMode(false);
+ }
}
- // Draw the current photo
- if (mLoadingState == LOADING_COMPLETE) {
- super.render(canvas);
+ @Override
+ public void updateScreenNail(ScreenNail s) {
+ mIsNonBitmap = (s != null && !(s instanceof BitmapScreenNail));
+ mTileView.updateScreenNail(s);
}
- // If the photo is loaded, draw the message/icon at the center of it,
- // otherwise draw the message/icon at the center of the view.
- if (mLoadingState == LOADING_COMPLETE) {
- mTileView.getImageCenter(mImageCenter);
- renderMessage(canvas, mImageCenter.x, mImageCenter.y);
- } else {
- renderMessage(canvas, getWidth() / 2, getHeight() / 2);
+ @Override
+ public ScreenNail releaseScreenNail() {
+ return mTileView.releaseScreenNail();
}
- // Draw the previous photo
- if (drawScreenNail) {
- ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
- prevNail.draw(canvas, false);
+ @Override
+ public boolean isEnabled() {
+ return true;
}
- }
- private void renderMessage(GLCanvas canvas, int x, int y) {
- // Draw the progress spinner and the text below it
- //
- // (x, y) is where we put the center of the spinner.
- // s is the size of the video play icon, and we use s to layout text
- // because we want to keep the text at the same place when the video
- // play icon is shown instead of the spinner.
- int w = getWidth();
- int h = getHeight();
- int s = Math.min(getWidth(), getHeight()) / 6;
+ private void setTileViewPosition(Rect r) {
+ TileImageView t = mTileView;
+
+ // Find out the bitmap coordinates of the center of the view
+ int imageW = mPositionController.getImageWidth();
+ int imageH = mPositionController.getImageHeight();
+ float scale = mPositionController.getImageScale();
+ int viewW = getWidth();
+ int viewH = getHeight();
+ int centerX = (int) (imageW / 2f +
+ (viewW / 2f - r.exactCenterX()) / scale + 0.5f);
+ int centerY = (int) (imageH / 2f +
+ (viewH / 2f - r.exactCenterY()) / scale + 0.5f);
+
+ if (CARD_EFFECT && !mFilmMode) {
+ // Calculate the move-out progress value.
+ int left = r.left;
+ int right = r.right;
+ float progress = calculateMoveOutProgress(left, right, viewW);
+ progress = Utils.clamp(progress, -1f, 1f);
+
+ // We only want to apply the fading animation if the scrolling
+ // movement is to the right.
+ if (progress < 0) {
+ if (right - left < viewW) {
+ // If the picture is narrower than the view, keep it at
+ // the center of the view.
+ centerX = imageW / 2;
+ } else {
+ // If the picture is wider than the view (it's
+ // zoomed-in), keep the left edge of the object align
+ // the the left edge of the view.
+ centerX = Math.round(viewW / 2f / scale);
+ }
+ scale *= getScrollScale(progress);
+ t.setAlpha(getScrollAlpha(progress));
+ }
+ }
- if (mLoadingState == LOADING_TIMEOUT) {
- StringTexture m = mLoadingText;
- ProgressSpinner r = mLoadingSpinner;
- r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
- m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
- invalidate(); // we need to keep the spinner rotating
- } else if (mLoadingState == LOADING_FAIL) {
- StringTexture m = mNoThumbnailText;
- m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ // set the position of the tile view
+ int inverseX = imageW - centerX;
+ int inverseY = imageH - centerY;
+ int rotation = mRotation;
+ switch (rotation) {
+ case 0: t.setPosition(centerX, centerY, scale, 0); break;
+ case 90: t.setPosition(centerY, inverseX, scale, 90); break;
+ case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
+ case 270: t.setPosition(inverseY, centerX, scale, 270); break;
+ default:
+ throw new IllegalArgumentException(String.valueOf(rotation));
+ }
}
- // Draw the video play icon (in the place where the spinner was)
- if (mShowVideoPlayIcon
- && mLoadingState != LOADING_INIT
- && mLoadingState != LOADING_TIMEOUT) {
- mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
- }
+ private void renderMessage(GLCanvas canvas, int x, int y) {
+ // Draw the progress spinner and the text below it
+ //
+ // (x, y) is where we put the center of the spinner.
+ // s is the size of the video play icon, and we use s to layout text
+ // because we want to keep the text at the same place when the video
+ // play icon is shown instead of the spinner.
+ int w = getWidth();
+ int h = getHeight();
+ int s = Math.min(getWidth(), getHeight()) / 6;
+
+ if (mLoadingState == LOADING_TIMEOUT) {
+ StringTexture m = mLoadingText;
+ ProgressSpinner p = mLoadingSpinner;
+ p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2);
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ invalidate(); // we need to keep the spinner rotating
+ } else if (mLoadingState == LOADING_FAIL) {
+ StringTexture m = mNoThumbnailText;
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ }
- mPositionController.advanceAnimation();
- }
+ // Draw a debug indicator showing which picture has focus (index ==
+ // 0).
+ // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
- private void stopCurrentSwipingIfNeeded() {
- // Enable fast swiping
- if (mTransitionMode == TRANS_SWITCH_NEXT) {
- mTransitionMode = TRANS_NONE;
- mPositionController.stopAnimation();
- switchToNextImage();
- } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
- mTransitionMode = TRANS_NONE;
- mPositionController.stopAnimation();
- switchToPreviousImage();
+ // Draw the video play icon (in the place where the spinner was)
+ if (mShowVideoPlayIcon
+ && mLoadingState != LOADING_INIT
+ && mLoadingState != LOADING_TIMEOUT) {
+ mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
+ }
}
}
- private boolean swipeImages(float velocityX, float velocityY) {
- if (mTransitionMode != TRANS_NONE
- && mTransitionMode != TRANS_SWITCH_NEXT
- && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
+ private class ScreenNailPicture implements Picture {
+ private int mIndex;
+ private boolean mEnabled;
+ private int mRotation;
+ private ScreenNail mScreenNail;
- // Avoid swiping images if we're possibly flinging to view the
- // zoomed in picture vertically.
- PositionController controller = mPositionController;
- boolean isMinimal = controller.isAtMinimalScale();
- int edges = controller.getImageAtEdges();
- if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
- if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
- || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
- return false;
+ public ScreenNailPicture(int index) {
+ mIndex = index;
+ }
- // If we are at the edge of the current photo and the sweeping velocity
- // exceeds the threshold, switch to next / previous image.
- int halfWidth = getWidth() / 2;
- if (velocityX < -SWIPE_THRESHOLD && (isMinimal
- || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
- stopCurrentSwipingIfNeeded();
- ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
- if (next.isEnabled()) {
- mTransitionMode = TRANS_SWITCH_NEXT;
- controller.startHorizontalSlide(next.mOffsetX - halfWidth);
- return true;
+ @Override
+ public void reload() {
+ updateScreenNail(mModel == null ? null
+ : mModel.getScreenNail(mIndex));
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Rect r) {
+ if (mScreenNail == null) {
+ return;
}
- } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
- || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
- stopCurrentSwipingIfNeeded();
- ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
- if (prev.isEnabled()) {
- mTransitionMode = TRANS_SWITCH_PREVIOUS;
- controller.startHorizontalSlide(prev.mOffsetX - halfWidth);
- return true;
+ if (r.left >= getWidth() || r.right <= 0 ||
+ r.top >= getHeight() || r.bottom <= 0) {
+ mScreenNail.noDraw();
+ return;
}
- }
- return false;
- }
+ boolean applyFadingAnimation =
+ CARD_EFFECT && mIndex > 0 && !mFilmMode;
- private boolean snapToNeighborImage() {
- if (mTransitionMode != TRANS_NONE) return false;
+ int w = getWidth();
+ int drawW = getRotated(mRotation, r.width(), r.height());
+ int drawH = getRotated(mRotation, r.height(), r.width());
+ int cx = applyFadingAnimation ? w / 2 : r.centerX();
+ int cy = r.centerY();
+ int flags = GLCanvas.SAVE_FLAG_MATRIX;
- PositionController controller = mPositionController;
- RectF bounds = controller.getImageBounds();
- int left = Math.round(bounds.left);
- int right = Math.round(bounds.right);
- int width = getWidth();
- int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
+ if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
+ canvas.save(flags);
+ canvas.translate(cx, cy);
+ if (applyFadingAnimation) {
+ float progress = (float) (w / 2 - r.centerX()) / w;
+ progress = Utils.clamp(progress, -1, 1);
+ float alpha = getScrollAlpha(progress);
+ float scale = getScrollScale(progress);
+ canvas.multiplyAlpha(alpha);
+ canvas.scale(scale, scale, 1);
+ }
+ if (mRotation != 0) {
+ canvas.rotate(mRotation, 0, 0, 1);
+ }
+ mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+ canvas.restore();
+ }
- // If we have moved the picture a lot, switching.
- ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
- if (next.isEnabled() && threshold < width - right) {
- mTransitionMode = TRANS_SWITCH_NEXT;
- controller.startHorizontalSlide(next.mOffsetX - width / 2);
- return true;
+ @Override
+ public void updateScreenNail(ScreenNail s) {
+ mEnabled = (s != null);
+ if (mScreenNail == s) return;
+ if (mScreenNail != null) {
+ mScreenNail.pauseDraw();
+ }
+ mScreenNail = s;
+ if (mScreenNail != null) {
+ mRotation = mScreenNail.getRotation();
+ }
+ if (mScreenNail != null) {
+ int w = s.getWidth();
+ int h = s.getHeight();
+ mPositionController.setImageSize(mIndex,
+ getRotated(mRotation, w, h),
+ getRotated(mRotation, h, w));
+ }
}
- ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
- if (prev.isEnabled() && threshold < left) {
- mTransitionMode = TRANS_SWITCH_PREVIOUS;
- controller.startHorizontalSlide(prev.mOffsetX - width / 2);
- return true;
+
+ @Override
+ public ScreenNail releaseScreenNail() {
+ ScreenNail s = mScreenNail;
+ mScreenNail = null;
+ return s;
}
- return false;
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+ }
+
+ private static int getRotated(int degree, int original, int theother) {
+ return (degree % 180 == 0) ? original : theother;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Gestures Handling
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ mGestureRecognizer.onTouchEvent(event);
+ return true;
}
private class MyGestureListener implements GestureRecognizer.Listener {
private boolean mIgnoreUpEvent = false;
+ // If we have changed the mode in this scaling gesture.
+ private boolean mModeChanged;
+
@Override
public boolean onSingleTapUp(float x, float y) {
+ if (mFilmMode) {
+ setFilmMode(false);
+ return true;
+ }
+
if (mPhotoTapListener != null) {
mPhotoTapListener.onSingleTapUp((int) x, (int) y);
}
@Override
public boolean onDoubleTap(float x, float y) {
- if (mTransitionMode != TRANS_NONE) return true;
PositionController controller = mPositionController;
- float scale = controller.getCurrentScale();
+ float scale = controller.getImageScale();
// onDoubleTap happened on the second ACTION_DOWN.
// We need to ignore the next UP event.
mIgnoreUpEvent = true;
@Override
public boolean onScroll(float dx, float dy) {
- if (mTransitionMode != TRANS_NONE) return true;
-
- ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
- ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
-
- mPositionController.startScroll(dx, dy, next.isEnabled(),
- prev.isEnabled());
+ mPositionController.startScroll(-dx, -dy);
return true;
}
public boolean onFling(float velocityX, float velocityY) {
if (swipeImages(velocityX, velocityY)) {
mIgnoreUpEvent = true;
- } else if (mTransitionMode != TRANS_NONE) {
- // do nothing
} else if (mPositionController.fling(velocityX, velocityY)) {
mIgnoreUpEvent = true;
}
@Override
public boolean onScaleBegin(float focusX, float focusY) {
- if (mTransitionMode != TRANS_NONE) return false;
mPositionController.beginScale(focusX, focusY);
+ mModeChanged = false;
return true;
}
@Override
public boolean onScale(float focusX, float focusY, float scale) {
- if (Float.isNaN(scale) || Float.isInfinite(scale)
- || mTransitionMode != TRANS_NONE) return true;
- boolean outOfRange = mPositionController.scaleBy(
- scale, focusX, focusY);
- if (outOfRange) {
- if (!mCancelExtraScalingPending) {
- mHandler.sendEmptyMessageDelayed(
- MSG_CANCEL_EXTRA_SCALING, 700);
- mPositionController.setExtraScalingRange(true);
- mCancelExtraScalingPending = true;
+ if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
+ int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+ // We allow only one mode change in a scaling gesture.
+ if (!mModeChanged) {
+ if ((outOfRange < 0 && !mFilmMode) ||
+ (outOfRange > 0 && mFilmMode)) {
+ setFilmMode(!mFilmMode);
+ mModeChanged = true;
+ return true;
}
+ }
+
+ if (outOfRange != 0 && !mModeChanged) {
+ startExtraScalingIfNeeded();
} else {
- if (mCancelExtraScalingPending) {
- mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
- mPositionController.setExtraScalingRange(false);
- mCancelExtraScalingPending = false;
- }
+ stopExtraScalingIfNeeded();
}
return true;
}
+ private void startExtraScalingIfNeeded() {
+ if (!mCancelExtraScalingPending) {
+ mHandler.sendEmptyMessageDelayed(
+ MSG_CANCEL_EXTRA_SCALING, 700);
+ mPositionController.setExtraScalingRange(true);
+ mCancelExtraScalingPending = true;
+ }
+ }
+
+ private void stopExtraScalingIfNeeded() {
+ if (mCancelExtraScalingPending) {
+ mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
+ mPositionController.setExtraScalingRange(false);
+ mCancelExtraScalingPending = false;
+ }
+ }
+
@Override
public void onScaleEnd() {
mPositionController.endScale();
- snapToNeighborImage();
}
@Override
mIgnoreUpEvent = false;
return;
}
- if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) {
+
+ if (!snapToNeighborImage()) {
mPositionController.up();
}
}
}
- public void notifyOnNewImage() {
- mPositionController.setImageSize(0, 0);
+ private void setFilmMode(boolean enabled) {
+ if (mFilmMode == enabled) return;
+ mFilmMode = enabled;
+ mPositionController.setFilmMode(mFilmMode);
}
- public void startSlideInAnimation(int direction) {
- PositionController a = mPositionController;
- a.stopAnimation();
- switch (direction) {
- case TRANS_SLIDE_IN_LEFT:
- case TRANS_SLIDE_IN_RIGHT: {
- mTransitionMode = direction;
- a.startSlideInAnimation(direction);
- break;
- }
- default: throw new IllegalArgumentException(String.valueOf(direction));
+ ////////////////////////////////////////////////////////////////////////////
+ // Framework events
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ mTileView.layout(left, top, right, bottom);
+ mEdgeView.layout(left, top, right, bottom);
+ if (changeSize) {
+ mPositionController.setViewSize(getWidth(), getHeight());
}
}
- private void switchToNextImage() {
- // We update the texture here directly to prevent texture uploading.
- ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
- ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
- mTileView.invalidateTiles();
- prevNail.updateScreenNail(mTileView.releaseScreenNail());
- mTileView.updateScreenNail(nextNail.releaseScreenNail());
- mModel.next();
+ public void pause() {
+ mPositionController.skipAnimation();
+ mTileView.freeTextures();
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ mPictures.get(i).updateScreenNail(null);
+ }
}
- private void switchToPreviousImage() {
- // We update the texture here directly to prevent texture uploading.
- ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
- ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
- mTileView.invalidateTiles();
- nextNail.updateScreenNail(mTileView.releaseScreenNail());
- mTileView.updateScreenNail(prevNail.releaseScreenNail());
- mModel.previous();
+ public void resume() {
+ mTileView.prepareTextures();
}
- public void notifyTransitionComplete() {
- mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
+ ////////////////////////////////////////////////////////////////////////////
+ // Rendering
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ // Draw next photos
+ for (int i = 1; i <= SCREEN_NAIL_MAX; i++) {
+ Rect r = mPositionController.getPosition(i);
+ mPictures.get(i).draw(canvas, r);
+ // In page mode, we draw only one next photo.
+ if (!mFilmMode) break;
+ }
+
+ // Draw current photo
+ mPictures.get(0).draw(canvas, mPositionController.getPosition(0));
+
+ // Draw previous photos
+ for (int i = -1; i >= -SCREEN_NAIL_MAX; i--) {
+ Rect r = mPositionController.getPosition(i);
+ mPictures.get(i).draw(canvas, r);
+ // In page mode, we draw only one previous photo.
+ if (!mFilmMode) break;
+ }
+
+ mPositionController.advanceAnimation();
+ checkFocusSwitching();
}
- private void onTransitionComplete() {
- int mode = mTransitionMode;
- mTransitionMode = TRANS_NONE;
+ ////////////////////////////////////////////////////////////////////////////
+ // Film mode focus switching
+ ////////////////////////////////////////////////////////////////////////////
- if (mModel == null) return;
- if (mode == TRANS_SWITCH_NEXT) {
- switchToNextImage();
- } else if (mode == TRANS_SWITCH_PREVIOUS) {
- switchToPreviousImage();
+ // Runs in GL thread.
+ private void checkFocusSwitching() {
+ if (!mFilmMode) return;
+ if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
+ if (switchPosition() != 0) {
+ mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
}
}
- public boolean isDown() {
- return mGestureRecognizer.isDown();
+ // Runs in main thread.
+ private void switchFocus() {
+ if (mGestureRecognizer.isDown()) return;
+ switch (switchPosition()) {
+ case -1:
+ switchToPrevImage();
+ break;
+ case 1:
+ switchToNextImage();
+ break;
+ }
}
- public static interface Model extends TileImageView.Model {
- public void next();
- public void previous();
- public int getImageRotation();
+ // Returns -1 if we should switch focus to the previous picture, +1 if we
+ // should switch to the next, 0 otherwise.
+ private int switchPosition() {
+ Rect curr = mPositionController.getPosition(0);
+ int center = getWidth() / 2;
- // Return null if the specified image is unavailable.
- public ScreenNail getNextScreenNail();
- public ScreenNail getPrevScreenNail();
- }
+ if (curr.left > center && mPictures.get(-1).isEnabled()) {
+ Rect prev = mPositionController.getPosition(-1);
+ int currDist = curr.left - center;
+ int prevDist = center - prev.right;
+ if (prevDist < currDist) {
+ return -1;
+ }
+ } else if (curr.right < center && mPictures.get(1).isEnabled()) {
+ Rect next = mPositionController.getPosition(1);
+ int currDist = center - curr.right;
+ int nextDist = next.left - center;
+ if (nextDist < currDist) {
+ return 1;
+ }
+ }
- private static int getRotated(int degree, int original, int theother) {
- return ((degree / 90) & 1) == 0 ? original : theother;
+ return 0;
}
- private class ScreenNailEntry {
- private boolean mVisible;
- private boolean mEnabled;
+ ////////////////////////////////////////////////////////////////////////////
+ // Page mode focus switching
+ //
+ // We slide image to the next one or the previous one in two cases: 1: If
+ // the user did a fling gesture with enough velocity. 2 If the user has
+ // moved the picture a lot.
+ ////////////////////////////////////////////////////////////////////////////
- private int mDrawWidth;
- private int mDrawHeight;
- private int mOffsetX;
- private int mRotation;
+ private boolean swipeImages(float velocityX, float velocityY) {
+ if (mFilmMode) return false;
- private ScreenNail mScreenNail;
+ // Avoid swiping images if we're possibly flinging to view the
+ // zoomed in picture vertically.
+ PositionController controller = mPositionController;
+ boolean isMinimal = controller.isAtMinimalScale();
+ int edges = controller.getImageAtEdges();
+ if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
+ if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
+ || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
+ return false;
- public void updateScreenNail(ScreenNail screenNail) {
- mEnabled = (screenNail != null);
- if (mScreenNail == screenNail) return;
- if (mScreenNail != null) mScreenNail.pauseDraw();
- mScreenNail = screenNail;
- if (mScreenNail != null) {
- mRotation = mScreenNail.getRotation();
- updateDrawingSize();
- }
+ // If we are at the edge of the current photo and the sweeping velocity
+ // exceeds the threshold, slide to the next / previous image.
+ if (velocityX < -SWIPE_THRESHOLD && (isMinimal
+ || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
+ return slideToNextPicture();
+ } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
+ || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
+ return slideToPrevPicture();
}
- // Release the ownership of the ScreenNail from this entry.
- public ScreenNail releaseScreenNail() {
- ScreenNail s = mScreenNail;
- mScreenNail = null;
- return s;
- }
+ return false;
+ }
- public void layoutRightEdgeAt(int x) {
- mVisible = x > 0;
- mOffsetX = x - getRotated(
- mRotation, mDrawWidth, mDrawHeight) / 2;
- }
+ private boolean snapToNeighborImage() {
+ if (mFilmMode) return false;
- public void layoutLeftEdgeAt(int x) {
- mVisible = x < getWidth();
- mOffsetX = x + getRotated(
- mRotation, mDrawWidth, mDrawHeight) / 2;
- }
+ Rect r = mPositionController.getPosition(0);
+ int viewW = getWidth();
+ int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW);
- public int gapToSide() {
- return ((mRotation / 90) & 1) != 0
- ? PhotoView.gapToSide(mDrawHeight, getWidth())
- : PhotoView.gapToSide(mDrawWidth, getWidth());
+ // If we have moved the picture a lot, switching.
+ if (viewW - r.right > threshold) {
+ return slideToNextPicture();
+ } else if (r.left > threshold) {
+ return slideToPrevPicture();
}
- public void updateDrawingSize() {
- if (mScreenNail == null) return;
+ return false;
+ }
- int width = mScreenNail.getWidth();
- int height = mScreenNail.getHeight();
+ private boolean slideToNextPicture() {
+ Picture next = mPictures.get(1);
+ if (!next.isEnabled()) return false;
+ int currentX = mPositionController.getPosition(1).centerX();
+ int targetX = getWidth() / 2;
+ mPositionController.startHorizontalSlide(targetX - currentX);
+ switchToNextImage();
+ return true;
+ }
- // Calculate the initial scale that will used by PositionController
- // (usually fit-to-screen)
- float s = ((mRotation / 90) & 0x01) == 0
- ? mPositionController.getMinimalScale(width, height)
- : mPositionController.getMinimalScale(height, width);
+ private boolean slideToPrevPicture() {
+ Picture prev = mPictures.get(-1);
+ if (!prev.isEnabled()) return false;
+ int currentX = mPositionController.getPosition(-1).centerX();
+ int targetX = getWidth() / 2;
+ mPositionController.startHorizontalSlide(targetX - currentX);
+ switchToPrevImage();
+ return true;
+ }
- mDrawWidth = Math.round(width * s);
- mDrawHeight = Math.round(height * s);
- }
+ private static int gapToSide(int imageWidth, int viewWidth) {
+ return Math.max(0, (viewWidth - imageWidth) / 2);
+ }
- public boolean isEnabled() {
- return mEnabled;
- }
+ ////////////////////////////////////////////////////////////////////////////
+ // Focus switching
+ ////////////////////////////////////////////////////////////////////////////
- public void draw(GLCanvas canvas, boolean applyFadingAnimation) {
- if (mScreenNail == null) return;
- if (!mVisible) {
- mScreenNail.noDraw();
- return;
- }
+ private void switchToNextImage() {
+ mModel.next();
+ }
- int w = getWidth();
- int x = applyFadingAnimation ? w / 2 : mOffsetX;
- int y = getHeight() / 2;
- int flags = GLCanvas.SAVE_FLAG_MATRIX;
+ private void switchToPrevImage() {
+ mModel.previous();
+ }
- if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
- canvas.save(flags);
- canvas.translate(x, y);
- if (applyFadingAnimation) {
- float progress = (float) (x - mOffsetX) / w;
- float alpha = getScrollAlpha(progress);
- float scale = getScrollScale(progress);
- canvas.multiplyAlpha(alpha);
- canvas.scale(scale, scale, 1);
- }
- if (mRotation != 0) {
- canvas.rotate(mRotation, 0, 0, 1);
- }
- canvas.translate(-x, -y);
- mScreenNail.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
- mDrawWidth, mDrawHeight);
- canvas.restore();
- }
+ ////////////////////////////////////////////////////////////////////////////
+ // Opening Animation
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void setOpenAnimationRect(Rect rect) {
+ mPositionController.setOpenAnimationRect(rect);
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Card deck effect calculation
+ ////////////////////////////////////////////////////////////////////////////
+
// Returns the scrolling progress value for an object moving out of a
// view. The progress value measures how much the object has moving out of
// the view. The object currently displays in [left, right), and the view is
}
}
- public void pause() {
- mPositionController.skipAnimation();
- mTransitionMode = TRANS_NONE;
- mTileView.freeTextures();
- for (ScreenNailEntry entry : mScreenNails) {
- entry.updateScreenNail(null);
- }
- }
-
- public void resume() {
- mTileView.prepareTextures();
- }
+ ////////////////////////////////////////////////////////////////////////////
+ // Simple public utilities
+ ////////////////////////////////////////////////////////////////////////////
- public void setOpenAnimationRect(Rect rect) {
- mOpenAnimationRect = rect;
+ public void setPhotoTapListener(PhotoTapListener listener) {
+ mPhotoTapListener = listener;
}
public void showVideoPlayIcon(boolean show) {
mShowVideoPlayIcon = show;
}
-
- // Returns the opening animation rectangle saved by the previous page.
- public Rect retrieveOpenAnimationRect() {
- Rect r = mOpenAnimationRect;
- mOpenAnimationRect = null;
- return r;
- }
-
- public void openAnimationStarted() {
- mTransitionMode = TRANS_OPEN_ANIMATION;
- }
-
- public boolean isInTransition() {
- return mTransitionMode != TRANS_NONE;
- }
}
import android.content.Context;
import android.graphics.Rect;
-import android.graphics.RectF;
-import android.util.FloatMath;
+import android.util.Log;
+import android.widget.Scroller;
import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.MediaItem;
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;
- private long mAnimationStartTime = NO_ANIMATION;
+ // Special values for animation time.
private static final long NO_ANIMATION = -1;
private static final long LAST_ANIMATION = -2;
- private int mAnimationKind;
- private float mAnimationDuration;
- private final static int ANIM_KIND_SCROLL = 0;
- private final static int ANIM_KIND_SCALE = 1;
- private final static int ANIM_KIND_SNAPBACK = 2;
- private final static int ANIM_KIND_SLIDE = 3;
- private final static int ANIM_KIND_ZOOM = 4;
- private final static int ANIM_KIND_FLING = 5;
+ 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.
- private final static int ANIM_TIME[] = {
+ private static final int ANIM_TIME[] = {
0, // ANIM_KIND_SCROLL
50, // ANIM_KIND_SCALE
600, // ANIM_KIND_SNAPBACK
400, // ANIM_KIND_SLIDE
300, // ANIM_KIND_ZOOM
+ 600, // ANIM_KIND_OPENING
0, // ANIM_KIND_FLING (the duration is calculated dynamically)
};
// 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;
- private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12);
- private static final float SCALE_MIN_EXTRA = 0.6f;
+ // 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;
- private PhotoView mViewer;
- private EdgeView mEdgeView;
- private int mImageW, mImageH;
- private int mViewW, mViewH;
-
- // The X, Y are the coordinate on bitmap which shows on the center of
- // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
- // values used currently.
- private int mCurrentX, mFromX, mToX;
- private int mCurrentY, mFromY, mToY;
- private float mCurrentScale, mFromScale, mToScale;
-
- // The focus point of the scaling gesture (in bitmap coordinates).
- private int mFocusBitmapX;
- private int mFocusBitmapY;
- private boolean mInScale;
-
- // The minimum and maximum scale we allow.
- private float mScaleMin, mScaleMax = SCALE_LIMIT;
+ // Setting this true makes the extra scaling range permanent (until this is
+ // set to false again).
private boolean mExtraScalingRange = false;
- // This is used by the fling animation
- private FlingScroller mScroller;
+ // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+ private boolean mFilmMode = false;
+ private static final float FILM_MODE_SCALE_FACTOR = 0.7f;
- // The bound of the stable region, see the comments above
- // calculateStableBound() for details.
- private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+ // The scaling factor in current mode.
+ private float mScaleFactor = mFilmMode ? FILM_MODE_SCALE_FACTOR : 1.0f;
- // Assume the image size is the same as view size before we know the actual
- // size of image.
- private boolean mUseViewSize = true;
+ // 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 RectF mTempRect = new RectF();
- private float[] mTempPoints = new float[8];
+ public static final int IMAGE_GAP = 96;
+ private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
- public PositionController(PhotoView viewer, Context context,
- EdgeView edgeView) {
- mViewer = viewer;
- mEdgeView = edgeView;
- mScroller = new FlingScroller();
- }
+ private Listener mListener;
+ private volatile Rect mOpenAnimationRect;
+ private int mViewW = 640;
+ private int mViewH = 480;;
- public void setImageSize(int width, int height) {
+ // A scaling guesture 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;
- // If no image available, use view size.
- if (width == 0 || height == 0) {
- mUseViewSize = true;
- mImageW = mViewW;
- mImageH = mViewH;
- mCurrentX = mImageW / 2;
- mCurrentY = mImageH / 2;
- mCurrentScale = 1;
- mScaleMin = 1;
- mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
- return;
- }
+ // This is used by the fling animation (page mode).
+ private FlingScroller mPageScroller;
- mUseViewSize = false;
+ // This is used by the fling animation (film mode).
+ private Scroller mFilmScroller;
- float ratio = Math.min(
- (float) mImageW / width, (float) mImageH / height);
+ // 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;
- // See the comment above translate() for details.
- mCurrentX = translate(mCurrentX, mImageW, width, ratio);
- mCurrentY = translate(mCurrentY, mImageH, height, ratio);
- mCurrentScale = mCurrentScale * ratio;
+ //
+ // ___________________________________________________________
+ // | _____ _____ _____ _____ _____ |
+ // | | | | | | | | | | | |
+ // | | Box | | Box | | Box*| | Box | | Box | |
+ // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
+ // | Gap Gap Gap Gap |
+ // |___________________________________________________________|
+ //
+ // <-- Platform -->
+ //
+ // The focused box (Box*) centers at mPlatform.mCurrentX
- mFromX = translate(mFromX, mImageW, width, ratio);
- mFromY = translate(mFromY, mImageH, height, ratio);
- mFromScale = mFromScale * ratio;
+ 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);
- mToX = translate(mToX, mImageW, width, ratio);
- mToY = translate(mToY, mImageH, height, ratio);
- mToScale = mToScale * ratio;
+ // 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);
- mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
- mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
+ // The output of the PositionController. Available throught getPosition().
+ private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
- mImageW = width;
- mImageH = height;
+ public interface Listener {
+ void invalidate();
+ boolean isDown();
- mScaleMin = getMinimalScale(mImageW, mImageH);
+ // EdgeView
+ void onPull(int offset, int direction);
+ void onRelease();
+ void onAbsorb(int velocity, int direction);
+ }
- // Start animation from the saved rectangle if we have one.
- Rect r = mViewer.retrieveOpenAnimationRect();
- if (r != null) {
- // The animation starts from the specified rectangle; the image
- // should be scaled and centered as the thumbnail shown in the
- // rectangle to minimize janky opening animation. Note: The below
- // implementation depends on how thumbnails are drawn and placed.
- float size = MediaItem.getTargetSize(
- MediaItem.TYPE_MICROTHUMBNAIL);
- float scale = (size / Math.min(width, height)) * Math.min(
- r.width() / size, r.height() / size);
-
- mCurrentX = Math.round((mViewW / 2f - r.centerX()) / scale) + mImageW / 2;
- mCurrentY = Math.round((mViewH / 2f - r.centerY()) / scale) + mImageH / 2;
- mCurrentScale = scale;
- mViewer.openAnimationStarted();
- startSnapback();
- } else if (mAnimationStartTime == NO_ANIMATION) {
- mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
- }
- mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ public PositionController(Context context, Listener listener) {
+ mListener = listener;
+ mPageScroller = new FlingScroller();
+ mFilmScroller = new Scroller(context);
+
+ // 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 zoomIn(float tapX, float tapY, float targetScale) {
- if (targetScale > mScaleMax) targetScale = mScaleMax;
+ public void setOpenAnimationRect(Rect r) {
+ mOpenAnimationRect = r;
+ }
- // Convert the tap position to image coordinate
- int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
- int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
+ public void setViewSize(int viewW, int viewH) {
+ if (viewW == mViewW && viewH == mViewH) return;
- calculateStableBound(targetScale);
- int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
- int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
+ mViewW = viewW;
+ mViewH = viewH;
+ initPlatform();
- startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
- }
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ setBoxSize(i, viewW, viewH, true);
+ }
- public void resetToFullView() {
- startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
+ updateScaleAndGapLimit();
+ snapAndRedraw();
}
- public float getMinimalScale(int w, int h) {
- return Math.min(SCALE_LIMIT,
- Math.min((float) mViewW / w, (float) mViewH / h));
- }
+ public void setImageSize(int index, int width, int height) {
+ if (width == 0 || height == 0) {
+ initBox(index);
+ } else {
+ setBoxSize(index, width, height, false);
+ }
- // Translate a coordinate on bitmap if the bitmap size changes.
- // If the aspect ratio doesn't change, it's easy:
- //
- // r = w / w' (= h / h')
- // x' = x / r
- // y' = y / r
- //
- // However the aspect ratio may change. That happens when the user slides
- // a image before it's loaded, we don't know the actual aspect ratio, so
- // we will assume one. When we receive the actual bitmap size, we need to
- // translate the coordinate from the old bitmap into the new bitmap.
- //
- // What we want to do is center the bitmap at the original position.
- //
- // ...+--+...
- // . | | .
- // . | | .
- // ...+--+...
- //
- // First we scale down the new bitmap by a factor r = min(w/w', h/h').
- // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
- // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
- // the old bitmap maps to (x', y') in the new bitmap, where
- // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
- // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
- private static int translate(int value, int size, int newSize, float ratio) {
- return Math.round(newSize / 2f + (value - size / 2f) / ratio);
+ updateScaleAndGapLimit();
+ startOpeningAnimationIfNeeded();
+ snapAndRedraw();
}
- public void setViewSize(int viewW, int viewH) {
- boolean needLayout = mViewW == 0 || mViewH == 0;
+ private void setBoxSize(int i, int width, int height, boolean isViewSize) {
+ Box b = mBoxes.get(i);
- mViewW = viewW;
- mViewH = viewH;
+ // If we already have image size, we don't want to use the view size.
+ if (isViewSize && !b.mUseViewSize) return;
+ b.mUseViewSize = isViewSize;
- if (mUseViewSize) {
- mImageW = viewW;
- mImageH = viewH;
- mCurrentX = mImageW / 2;
- mCurrentY = mImageH / 2;
- mCurrentScale = 1;
- mScaleMin = 1;
- mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ if (width == b.mImageW && height == b.mImageH) {
return;
}
- // In most cases we want to keep the scaling factor intact when the
- // view size changes. The cases we want to reset the scaling factor
- // (to fit the view if possible) are (1) the scaling factor is too
- // small for the new view size (2) the scaling factor has not been
- // changed by the user.
- boolean wasMinScale = (mCurrentScale == mScaleMin);
- mScaleMin = getMinimalScale(mImageW, mImageH);
+ // The ratio of the old size and the new size.
+ float ratio = Math.min(
+ (float) b.mImageW / width, (float) b.mImageH / height);
+
+ b.mCurrentScale *= ratio;
+ b.mFromScale *= ratio;
+ b.mToScale *= ratio;
- if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
- mCurrentX = mImageW / 2;
- mCurrentY = mImageH / 2;
- mCurrentScale = mScaleMin;
- mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ b.mImageW = width;
+ b.mImageH = height;
+
+ if (i == 0) {
+ mFocusX /= ratio;
+ mFocusY /= ratio;
}
}
- public void stopAnimation() {
- mAnimationStartTime = NO_ANIMATION;
+ private void startOpeningAnimationIfNeeded() {
+ if (mOpenAnimationRect == null) return;
+ Box b = mBoxes.get(0);
+ if (b.mUseViewSize) return;
+
+ // Start animation from the saved rectangle if we have one.
+ Rect r = mOpenAnimationRect;
+ mOpenAnimationRect = null;
+ mPlatform.mCurrentX = r.centerX();
+ b.mCurrentY = r.centerY();
+ b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+ r.height() / (float) b.mImageH);
+ startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
}
- public void skipAnimation() {
- if (mAnimationStartTime == NO_ANIMATION) return;
- mAnimationStartTime = NO_ANIMATION;
- mCurrentX = mToX;
- mCurrentY = mToY;
- mCurrentScale = mToScale;
+ public void setFilmMode(boolean enabled) {
+ if (enabled == mFilmMode) return;
+ mFilmMode = enabled;
+ mScaleFactor = enabled ? FILM_MODE_SCALE_FACTOR : 1.0f;
+
+ updateScaleAndGapLimit();
+ stopAnimation();
+ snapAndRedraw();
}
- public void beginScale(float focusX, float focusY) {
- mInScale = true;
- mFocusBitmapX = Math.round(mCurrentX +
- (focusX - mViewW / 2f) / mCurrentScale);
- mFocusBitmapY = Math.round(mCurrentY +
- (focusY - mViewH / 2f) / mCurrentScale);
+ public void setExtraScalingRange(boolean enabled) {
+ if (mExtraScalingRange == enabled) return;
+ mExtraScalingRange = enabled;
+ if (!enabled) {
+ snapAndRedraw();
+ }
}
- // Returns true if the result scale is outside the stable range.
- public boolean scaleBy(float s, float focusX, float focusY) {
+ // 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, and mode.
+ private void updateScaleAndGapLimit() {
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
+ b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
+ }
- // We want to keep the focus point (on the bitmap) the same as when
- // we begin the scale guesture, that is,
- //
- // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
- //
- s *= getTargetScale();
- int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
- int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ g.mDefaultSize = getDefaultGapSize(i);
+ }
+ }
- startAnimation(x, y, s, ANIM_KIND_SCALE);
- return (s < mScaleMin || s > mScaleMax);
+ // 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));
}
- public void endScale() {
- mInScale = false;
- startSnapbackIfNeeded();
+ // 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);
}
- public void setExtraScalingRange(boolean enabled) {
- mExtraScalingRange = enabled;
- if (!enabled) {
- startSnapbackIfNeeded();
+ // 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 float getCurrentScale() {
- return mCurrentScale;
+ public void skipAnimation() {
+ if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+ mPlatform.mCurrentX = mPlatform.mToX;
+ 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 boolean isAtMinimalScale() {
- return isAlmostEquals(mCurrentScale, mScaleMin);
+ public void up() {
+ snapAndRedraw();
}
- private static boolean isAlmostEquals(float a, float b) {
- float diff = a - b;
- return (diff < 0 ? -diff : diff) < 0.02f;
+ ////////////////////////////////////////////////////////////////////////////
+ // Start an animations for the focused box
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ 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) (mViewW / 2 - tempX * targetScale + 0.5f);
+ int y = (int) (mViewH / 2 - 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 up() {
- startSnapback();
- }
-
- // |<--| (1/2) * mImageW
- // +-------+-------+-------+
- // | | | |
- // | | o | |
- // | | | |
- // +-------+-------+-------+
- // |<----------| (3/2) * mImageW
- // Slide in the image from left or right.
- // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
- // Sliding from left: mCurrentX = (1/2) * mImageW
- // right: mCurrentX = (3/2) * mImageW
- public void startSlideInAnimation(int direction) {
- int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
- mImageW / 2 : 3 * mImageW / 2;
- mFromX = Math.round(fromX);
- mFromY = Math.round(mImageH / 2f);
- mCurrentX = mFromX;
- mCurrentY = mFromY;
- startAnimation(
- mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
+ public void resetToFullView() {
+ Box b = mBoxes.get(0);
+ startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ 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) {
+ 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 guesture, that is,
+ //
+ // (focusX' - currentX') / scale' = (focusX - currentX) / scale
+ //
+ 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();
}
public void startHorizontalSlide(int distance) {
- scrollBy(distance, 0, ANIM_KIND_SLIDE);
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ startAnimation(getTargetX(p) + distance, getTargetY(b),
+ b.mCurrentScale, ANIM_KIND_SLIDE);
}
- private void scrollBy(float dx, float dy, int type) {
- startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
- getTargetY() + Math.round(dy / mCurrentScale),
- mCurrentScale, type);
+ public void startScroll(float dx, float dy) {
+ boolean hasPrev = hasPrevImages();
+ boolean hasNext = hasNextImages();
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ int x = getTargetX(p) + (int) (dx + 0.5f);
+ int y = getTargetY(b) + (int) (dy + 0.5f);
+
+ if (mFilmMode) {
+ scrollToFilm(x, y, hasPrev, hasNext);
+ } else {
+ scrollToPage(x, y, hasPrev, hasNext);
+ }
}
- public void startScroll(float dx, float dy, boolean hasNext,
- boolean hasPrev) {
- int x = getTargetX() + Math.round(dx / mCurrentScale);
- int y = getTargetY() + Math.round(dy / mCurrentScale);
+ private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) {
+ Box b = mBoxes.get(0);
- calculateStableBound(mCurrentScale);
+ calculateStableBound(b.mCurrentScale);
// 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) {
- mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
+ mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
} else if (y > mBoundBottom) {
- mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
+ mListener.onPull(y - mBoundBottom, EdgeView.TOP);
}
}
// 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 (!hasPrev && x < mBoundLeft) {
- int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
- mEdgeView.onPull(pixels, EdgeView.LEFT);
- x = mBoundLeft;
- } else if (!hasNext && x > mBoundRight) {
- int pixels = Math.round((x - mBoundRight) * mCurrentScale);
- mEdgeView.onPull(pixels, EdgeView.RIGHT);
+ if (!hasPrev && x > mBoundRight) {
+ int pixels = x - mBoundRight;
+ mListener.onPull(pixels, EdgeView.LEFT);
x = mBoundRight;
+ } else if (!hasNext && x < mBoundLeft) {
+ int pixels = mBoundLeft - x;
+ mListener.onPull(pixels, EdgeView.RIGHT);
+ x = mBoundLeft;
+ }
+
+ startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) {
+ Box b = mBoxes.get(0);
+
+ // 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.
+ int cx = mViewW / 2;
+ if (!hasPrev && x > cx) {
+ int pixels = x - cx;
+ mListener.onPull(pixels, EdgeView.LEFT);
+ x = cx;
+ } else if (!hasNext && x < cx) {
+ int pixels = cx - x;
+ mListener.onPull(pixels, EdgeView.RIGHT);
+ x = cx;
}
- startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
+ startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
}
public boolean fling(float velocityX, float velocityY) {
+ int vx = (int) (velocityX + 0.5f);
+ int vy = (int) (velocityY + 0.5f);
+ return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
+ }
+
+ private 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(mCurrentScale) &&
- viewHigherThanScaledImage(mCurrentScale)) {
+ if (viewWiderThanScaledImage(b.mCurrentScale) &&
+ viewTallerThanScaledImage(b.mCurrentScale)) {
return false;
}
(velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
velocityY = 0;
}
- if (isAlmostEquals(velocityX, 0) && isAlmostEquals(velocityY, 0)) {
- return false;
- }
- mScroller.fling(mCurrentX, mCurrentY,
- Math.round(-velocityX / mCurrentScale),
- Math.round(-velocityY / mCurrentScale),
+ if (velocityX == 0 && velocityY == 0) return false;
+
+ mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
- int targetX = mScroller.getFinalX();
- int targetY = mScroller.getFinalY();
- mAnimationDuration = mScroller.getDuration();
- startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
+ int targetX = mPageScroller.getFinalX();
+ int targetY = mPageScroller.getFinalY();
+ ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+ startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
return true;
}
- private void startAnimation(
- int targetX, int targetY, float scale, int kind) {
- mAnimationKind = kind;
- if (targetX == mCurrentX && targetY == mCurrentY
- && scale == mCurrentScale) {
- onAnimationComplete();
- return;
+ private boolean flingFilm(int velocityX, int velocityY) {
+ boolean hasPrev = hasPrevImages();
+ boolean hasNext = hasNextImages();
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // If we are already at the edge, don't start the fling.
+ int cx = mViewW / 2;
+ if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) {
+ return false;
}
- mFromX = mCurrentX;
- mFromY = mCurrentY;
- mFromScale = mCurrentScale;
+ if (velocityX == 0) return false;
- mToX = targetX;
- mToY = targetY;
- mToScale = Utils.clamp(scale, SCALE_MIN_EXTRA * mScaleMin,
- SCALE_MAX_EXTRA * mScaleMax);
+ mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+ int targetX = mFilmScroller.getFinalX();
+ ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration();
+ startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
+ return true;
+ }
- // If the scaled height is smaller than the view height,
- // force it to be in the center.
- // (We do for height only, not width, because the user may
- // want to scroll to the previous/next image.)
- if (!mInScale && viewHigherThanScaledImage(mToScale)) {
- mToY = mImageH / 2;
+ ////////////////////////////////////////////////////////////////////////////
+ // 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();
}
+ redraw();
+ }
- mAnimationStartTime = AnimationTime.get();
- if (mAnimationKind != ANIM_KIND_FLING) {
- mAnimationDuration = ANIM_TIME[mAnimationKind];
+ private void startAnimation(int targetX, int targetY, float targetScale,
+ int kind) {
+ boolean changed = false;
+ changed |= mPlatform.doAnimation(targetX, kind);
+ changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+ if (changed) redraw();
+ }
+
+ public boolean advanceAnimation() {
+ boolean changed = false;
+ changed |= mPlatform.advanceAnimation();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ changed |= mBoxes.get(i).advanceAnimation();
}
- advanceAnimation();
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ changed |= mGaps.get(i).advanceAnimation();
+ }
+ if (changed) redraw();
+ return changed;
}
- public void advanceAnimation() {
- if (mAnimationStartTime == NO_ANIMATION) {
- return;
- } else if (mAnimationStartTime == LAST_ANIMATION) {
- onAnimationComplete();
- return;
+ ////////////////////////////////////////////////////////////////////////////
+ // 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 the loop index goes from inside-out because each box's X coordinate
+ // is relative to its anchor box (except the focused box).
+ private void layoutAndSetPosition() {
+ // layout box 0 (focused box)
+ convertBoxToRect(0);
+ for (int i = 1; i <= BOX_MAX; i++) {
+ // layout box i and -i
+ convertBoxToRect(i);
+ convertBoxToRect(-i);
}
+ //dumpState();
+ }
- long now = AnimationTime.get();
- float progress;
- if (mAnimationDuration == 0) {
- progress = 1;
- } else {
- progress = (now - mAnimationStartTime) / mAnimationDuration;
+ private void dumpState() {
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
}
- if (progress >= 1) {
- progress = 1;
- mCurrentX = mToX;
- mCurrentY = mToY;
- mCurrentScale = mToScale;
- mAnimationStartTime = LAST_ANIMATION;
- } else {
- float f = 1 - progress;
- switch (mAnimationKind) {
- case ANIM_KIND_SCROLL:
- case ANIM_KIND_FLING:
- progress = 1 - f; // linear
- break;
- 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;
+ dumpRect(0);
+ for (int i = 1; i <= BOX_MAX; i++) {
+ dumpRect(i);
+ dumpRect(-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!");
+ }
}
- if (mAnimationKind == ANIM_KIND_FLING) {
- flingInterpolate(progress);
+ }
+ }
+
+ 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;
+ int w = widthOf(b);
+ int h = heightOf(b);
+ if (i == 0) {
+ int x = mPlatform.mCurrentX;
+ 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.mCurrentX = mViewW / 2;
+ 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.mImageW, b.mImageH);
+ b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
+ b.mCurrentY = mViewH / 2;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ // 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;
+ }
+
+ 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 {
- linearInterpolate(progress);
+ s.append(" ");
+ s.append(fromIndex[i]);
}
}
- mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
- mViewer.invalidate();
+ Log.d(TAG, s.toString());
}
- private void onAnimationComplete() {
- mAnimationStartTime = NO_ANIMATION;
- if (mViewer.isInTransition()) {
- mViewer.notifyTransitionComplete();
- } else {
- if (startSnapbackIfNeeded()) mViewer.invalidate();
- }
- }
-
- private void flingInterpolate(float progress) {
- mScroller.computeScrollOffset(progress);
- int oldX = mCurrentX;
- int oldY = mCurrentY;
- mCurrentX = mScroller.getCurrX();
- mCurrentY = mScroller.getCurrY();
-
- // Check if we hit the edges; show edge effects if we do.
- if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
- int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
- mEdgeView.onAbsorb(v, EdgeView.LEFT);
- } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
- int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
- mEdgeView.onAbsorb(v, EdgeView.RIGHT);
- }
-
- if (oldY > mBoundTop && mCurrentY == mBoundTop) {
- int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
- mEdgeView.onAbsorb(v, EdgeView.TOP);
- } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
- int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
- mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
- }
- }
-
- // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
- private void linearInterpolate(float progress) {
- // To linearly interpolate the position on view coordinates, we do the
- // following steps:
- // (1) convert a bitmap position (x, y) to view coordinates:
- // from: (x - mFromX) * mFromScale + mViewW / 2
- // to: (x - mToX) * mToScale + mViewW / 2
- // (2) interpolate between the "from" and "to" coordinates:
- // (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
- // + mViewW / 2
- // should be equal to
- // (x - mCurrentX) * mCurrentScale + mViewW / 2
- // (3) The x-related terms in the above equation can be removed because
- // mFromScale * (1 - p) + ToScale * p = mCurrentScale
- // (4) Solve for mCurrentX, we have mCurrentX =
- // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
- float fromX = mFromX * mFromScale;
- float toX = mToX * mToScale;
- float currentX = fromX + progress * (toX - fromX);
-
- float fromY = mFromY * mFromScale;
- float toY = mToY * mToScale;
- float currentY = fromY + progress * (toY - fromY);
-
- mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
- mCurrentX = Math.round(currentX / mCurrentScale);
- mCurrentY = Math.round(currentY / mCurrentScale);
- }
-
- // Returns true if redraw is needed.
- private boolean startSnapbackIfNeeded() {
- if (mAnimationStartTime != NO_ANIMATION) return false;
- if (mInScale) return false;
- if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
- return false;
+ // 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 -- focuse goes to the previous box
+ // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+ public void moveBox(int fromIndex[]) {
+ //debugMoveBox(fromIndex);
+ RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+ // 1. Get the absolute X coordiates 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();
}
- return startSnapback();
- }
- private boolean startSnapback() {
- boolean needAnimation = false;
- float scale = mCurrentScale;
+ // 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);
+ }
- float scaleMin = mExtraScalingRange ?
- mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
- float scaleMax = mExtraScalingRange ?
- mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+ // 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);
+ }
+ }
- if (mCurrentScale < scaleMin || mCurrentScale > scaleMax) {
- needAnimation = true;
- scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+ // 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);
}
- calculateStableBound(scale, sHorizontalSlack);
- int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
- int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
+ // 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, just assign the same
+ // position as the previous box. (We can do better, but this should be
+ // rare). For the boxes before first or after last, we will use a new
+ // default gap size below.
+ for (int i = first + 1; i < last; i++) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX;
+ }
- if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
- needAnimation = true;
+ // 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);
+ }
}
- if (needAnimation) {
- startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
+ // 8. offset the Platform position
+ int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+ mPlatform.mCurrentX += dx;
+ mPlatform.mFromX += dx;
+ mPlatform.mToX += dx;
+ mPlatform.mFlingOffset += dx;
+
+ snapAndRedraw();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Public utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ public float getMinimalScale(int imageW, int imageH) {
+ float s = Math.min(mScaleFactor * mViewW / imageW,
+ mScaleFactor * mViewH / imageH);
+ return Math.min(SCALE_LIMIT, s);
+ }
+
+ public float getMaximalScale(int imageW, int imageH) {
+ return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
+ }
+
+ public boolean isAtMinimalScale() {
+ Box b = mBoxes.get(0);
+ return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+ }
+
+ 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;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Private utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ private float getMinimalScale(Box b) {
+ return getMinimalScale(b.mImageW, b.mImageH);
+ }
- return needAnimation;
+ private float getMaxmimalScale(Box b) {
+ return getMaximalScale(b.mImageW, b.mImageH);
+ }
+
+ private static boolean isAlmostEqual(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
}
// Calculates the stable region of mCurrent{X/Y}, where "stable" means
// 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) {
- calculateStableBound(scale, 0f);
- }
+ private void calculateStableBound(float scale, int horizontalSlack) {
+ Box b = mBoxes.get(0);
- private void calculateStableBound(float scale, float horizontalSlack) {
- // The number of pixels between the center of the view
- // and the edge when the edge is aligned.
- mBoundLeft = (int) FloatMath.ceil((mViewW - horizontalSlack) / (2 * scale));
- mBoundRight = mImageW - mBoundLeft;
- mBoundTop = (int) FloatMath.ceil(mViewH / (2 * scale));
- mBoundBottom = mImageH - mBoundTop;
+ // 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 - horizontalSlack) - w / 2;
+ mBoundRight = mViewW - mBoundLeft;
+ mBoundTop = mViewH - h / 2;
+ mBoundBottom = mViewH - mBoundTop;
// If the scaled height is smaller than the view height,
// force it to be in the center.
- if (viewHigherThanScaledImage(scale)) {
- mBoundTop = mBoundBottom = mImageH / 2;
+ if (viewTallerThanScaledImage(scale)) {
+ mBoundTop = mBoundBottom = mViewH / 2;
}
// Same for width
if (viewWiderThanScaledImage(scale)) {
- mBoundLeft = mBoundRight = mImageW / 2;
+ mBoundLeft = mBoundRight = mViewW / 2;
+ }
+ }
+
+ private void calculateStableBound(float scale) {
+ calculateStableBound(scale, 0);
+ }
+
+ private boolean hasNextImages() {
+ for (int i = 1; i <= BOX_MAX; i++) {
+ if (!mBoxes.get(i).mUseViewSize) return true;
+ }
+ return false;
+ }
+
+ private boolean hasPrevImages() {
+ for (int i = -1; i >= -BOX_MAX; i--) {
+ if (!mBoxes.get(i).mUseViewSize) return true;
}
+ return false;
}
- private boolean viewHigherThanScaledImage(float scale) {
- return FloatMath.floor(mImageH * scale) <= mViewH;
+ private boolean viewTallerThanScaledImage(float scale) {
+ return mViewH >= heightOf(mBoxes.get(0), scale);
}
private boolean viewWiderThanScaledImage(float scale) {
- return FloatMath.floor(mImageW * scale) <= mViewW;
+ return mViewW >= widthOf(mBoxes.get(0), scale);
}
- private boolean useCurrentValueAsTarget() {
- return mAnimationStartTime == NO_ANIMATION ||
- mAnimationKind == ANIM_KIND_SNAPBACK ||
- mAnimationKind == ANIM_KIND_FLING;
+ private float getTargetScale(Box b) {
+ return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
}
- private float getTargetScale() {
- return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
+ private int getTargetX(Platform p) {
+ return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
}
- private int getTargetX() {
- return useCurrentValueAsTarget() ? mCurrentX : mToX;
+ private int getTargetY(Box b) {
+ return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
}
- private int getTargetY() {
- return useCurrentValueAsTarget() ? mCurrentY : mToY;
+ private boolean useCurrentValueAsTarget(Animatable a) {
+ return a.mAnimationStartTime == NO_ANIMATION ||
+ a.mAnimationKind == ANIM_KIND_SNAPBACK ||
+ a.mAnimationKind == ANIM_KIND_FLING;
}
- public RectF getImageBounds() {
- float points[] = mTempPoints;
+ // Returns the index of the anchor box.
+ private int anchorIndex(int i) {
+ if (i > 0) return i - 1;
+ if (i < 0) return i + 1;
+ throw new IllegalArgumentException();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Animatable: an thing which can do animation.
+ ////////////////////////////////////////////////////////////////////////////
+ private abstract static class Animatable {
+ public long mAnimationStartTime;
+ public int mAnimationKind;
+ public int mAnimationDuration;
+
+ // This should be overidden 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);
+ }
- /*
- * (p0,p1)----------(p2,p3)
- * | |
- * | |
- * (p4,p5)----------(p6,p7)
- */
- points[0] = points[4] = -mCurrentX;
- points[1] = points[3] = -mCurrentY;
- points[2] = points[6] = mImageW - mCurrentX;
- points[5] = points[7] = mImageH - mCurrentY;
+ boolean done = interpolate(progress);
- RectF rect = mTempRect;
- rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
- Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+ if (done) {
+ mAnimationStartTime = LAST_ANIMATION;
+ }
- float scale = mCurrentScale;
- float offsetX = mViewW / 2;
- float offsetY = mViewH / 2;
- for (int i = 0; i < 4; ++i) {
- float x = points[i + i] * scale + offsetX;
- float y = points[i + i + 1] * scale + offsetY;
- if (x < rect.left) rect.left = x;
- if (x > rect.right) rect.right = x;
- if (y < rect.top) rect.top = y;
- if (y > rect.bottom) rect.bottom = y;
+ return true;
}
- return rect;
- }
- public int getImageWidth() {
- return mImageW;
+ private static float applyInterpolationCurve(int kind, float progress) {
+ float f = 1 - progress;
+ switch (kind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ progress = 1 - f; // linear
+ break;
+ case ANIM_KIND_SCALE:
+ progress = 1 - f * f; // quadratic
+ break;
+ case ANIM_KIND_SNAPBACK:
+ case ANIM_KIND_ZOOM:
+ case ANIM_KIND_SLIDE:
+ case ANIM_KIND_OPENING:
+ progress = 1 - f * f * f * f * f; // x^5
+ break;
+ }
+ return progress;
+ }
}
- public int getImageHeight() {
- return mImageH;
+ ////////////////////////////////////////////////////////////////////////////
+ // Platform: captures the global X movement.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Platform extends Animatable {
+ public int mCurrentX, mFromX, mToX;
+ public int mFlingOffset;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isDown()) 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;
+ if (mFilmMode) {
+ if (!hasNextImages()) x = Math.max(x, mViewW / 2);
+ if (!hasPrevImages()) x = Math.min(x, mViewW / 2);
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ x = Utils.clamp(x, mBoundLeft, mBoundRight);
+ }
+ if (mCurrentX != x) {
+ return doAnimation(x, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ // Starts an animation for the platform.
+ public boolean doAnimation(int targetX, int kind) {
+ if (mCurrentX == targetX) return false;
+ mAnimationKind = kind;
+ mFromX = mCurrentX;
+ mToX = targetX;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ mFlingOffset = 0;
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return mFilmMode
+ ? interpolateFlingFilm(progress)
+ : interpolateFlingPage(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingFilm(float progress) {
+ mFilmScroller.computeScrollOffset();
+ mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+ int dir = EdgeView.INVALID_DIRECTION;
+ if (mCurrentX < mViewW / 2) {
+ if (!hasNextImages()) {
+ dir = EdgeView.RIGHT;
+ }
+ } else if (mCurrentX > mViewW / 2) {
+ if (!hasPrevImages()) {
+ dir = EdgeView.LEFT;
+ }
+ }
+ if (dir != EdgeView.INVALID_DIRECTION) {
+ int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+ mListener.onAbsorb(v, dir);
+ mFilmScroller.forceFinished(true);
+ mCurrentX = mViewW / 2;
+ }
+ 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;
+ return true;
+ } else {
+ mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+ return (mCurrentX == mToX);
+ }
+ }
}
- public int getImageAtEdges() {
- calculateStableBound(mCurrentScale);
- int edges = 0;
- if (mCurrentX <= mBoundLeft) {
- edges |= IMAGE_AT_LEFT_EDGE;
+ ////////////////////////////////////////////////////////////////////////////
+ // 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.isDown()) return false;
+ if (mInScale && this == mBoxes.get(0)) return false;
+
+ int y;
+ 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 = mViewH / 2;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
+ }
+ } else {
+ y = mViewH / 2;
+ scale = mScaleMin;
+ }
+
+ if (mCurrentY != y || mCurrentScale != scale) {
+ return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+ }
+ return false;
}
- if (mCurrentX >= mBoundRight) {
- edges |= IMAGE_AT_RIGHT_EDGE;
+
+ private boolean doAnimation(int targetY, float targetScale, int kind) {
+ targetScale = Utils.clamp(targetScale,
+ SCALE_MIN_EXTRA * mScaleMin,
+ SCALE_MAX_EXTRA * mScaleMax);
+
+ // If the scaled height is smaller than the view height, force it to be
+ // in the center. (We do this for height only, not width, because the
+ // user may want to scroll to the previous/next image.)
+ if (!mInScale && viewTallerThanScaledImage(targetScale)) {
+ targetY = mViewH / 2;
+ }
+
+ 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;
}
- if (mCurrentY <= mBoundTop) {
- edges |= IMAGE_AT_TOP_EDGE;
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ // Currently a Box can only be flung in page mode.
+ return interpolateFlingPage(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
}
- if (mCurrentY >= mBoundBottom) {
- edges |= IMAGE_AT_BOTTOM_EDGE;
+
+ 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);
+ 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);
+ }
+
+ // Starts an animation for a gap.
+ public boolean doAnimation(int targetSize) {
+ if (mCurrentGap == targetSize) return false;
+ mAnimationKind = ANIM_KIND_SNAPBACK;
+ 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));
+ return (mCurrentGap == mToGap);
+ }
}
- return edges;
}
}