diff options
author | Chih-Chung Chang <chihchung@google.com> | 2012-04-03 12:21:16 +0800 |
---|---|---|
committer | Chih-Chung Chang <chihchung@google.com> | 2012-04-10 19:24:53 +0800 |
commit | 8bb545968ebb45b25c7ed88632a1f82f8295baae (patch) | |
tree | d9a107dbaa36220d8e64f2b0ae47778800582fb8 | |
parent | 359dadda41154bd1836312a70280b42d86777be1 (diff) | |
download | android_packages_apps_Snap-8bb545968ebb45b25c7ed88632a1f82f8295baae.tar.gz android_packages_apps_Snap-8bb545968ebb45b25c7ed88632a1f82f8295baae.tar.bz2 android_packages_apps_Snap-8bb545968ebb45b25c7ed88632a1f82f8295baae.zip |
Add new filmstrip mode for PhotoView.
Change-Id: I9da9896303ced8d63a3557d5e6e9bc06fb366cf5
-rw-r--r-- | src/com/android/gallery3d/app/PhotoDataAdapter.java | 57 | ||||
-rw-r--r-- | src/com/android/gallery3d/app/SinglePhotoDataAdapter.java | 11 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/AnimationTime.java | 6 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/BitmapScreenNail.java | 1 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/EdgeView.java | 1 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/GLRootView.java | 2 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PhotoView.java | 1141 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PositionController.java | 1622 | ||||
-rw-r--r-- | src/com/android/gallery3d/util/RangeArray.java | 52 | ||||
-rw-r--r-- | src/com/android/gallery3d/util/RangeBoolArray.java | 49 | ||||
-rw-r--r-- | src/com/android/gallery3d/util/RangeIntArray.java | 49 |
11 files changed, 1940 insertions, 1051 deletions
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java index f4bf5a57c..10ed8f3d0 100644 --- a/src/com/android/gallery3d/app/PhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -58,13 +58,12 @@ public class PhotoDataAdapter implements PhotoPage.Model { private static final int MIN_LOAD_COUNT = 8; private static final int DATA_CACHE_SIZE = 32; - private static final int IMAGE_CACHE_SIZE = 5; + private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1; private static final int BIT_SCREEN_NAIL = 1; private static final int BIT_FULL_IMAGE = 2; - private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber(); - // sImageFetchSeq is the fetching sequence for images. // We want to fetch the current screennail first (offset = 0), the next // screennail (offset = +1), then the previous screennail (offset = -1) etc. @@ -129,9 +128,9 @@ public class PhotoDataAdapter implements PhotoPage.Model { private int mCurrentIndex; // mChanges keeps the version number (of MediaItem) about the previous, - // current, and next image. If the version number changes, we invalidate - // the model. This is used after a database reload or mCurrentIndex changes. - private final long mChanges[] = new long[3]; + // current, and next image. If the version number changes, we notify the + // view. This is used after a database reload or mCurrentIndex changes. + private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; private final Handler mMainHandler; private final ThreadPool mThreadPool; @@ -193,7 +192,7 @@ public class PhotoDataAdapter implements PhotoPage.Model { } private long getVersion(int index) { - if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE; + if (index < 0 || index >= mSize) return MediaObject.INVALID_DATA_VERSION; if (index >= mContentStart && index < mContentEnd) { MediaItem item = mData[index % DATA_CACHE_SIZE]; if (item != null) return item.getDataVersion(); @@ -201,15 +200,11 @@ public class PhotoDataAdapter implements PhotoPage.Model { return MediaObject.INVALID_DATA_VERSION; } - private void fireModelInvalidated() { - for (int i = -1; i <= 1; ++i) { - long current = getVersion(mCurrentIndex + i); - long change = mChanges[i + 1]; - if (current != change) { - mPhotoView.notifyImageInvalidated(i); - mChanges[i + 1] = current; - } + private void fireDataChange() { + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { + mChanges[i + SCREEN_NAIL_MAX] = getVersion(mCurrentIndex + i); } + mPhotoView.notifyDataChange(mChanges); } public void setDataListener(DataListener listener) { @@ -235,10 +230,12 @@ public class PhotoDataAdapter implements PhotoPage.Model { if (mDataListener != null) { mDataListener.onPhotoAvailable(version, false); } - for (int i = -1; i <= 1; ++i) { + + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { if (version == getVersion(mCurrentIndex + i)) { if (i == 0) updateTileProvider(entry); - mPhotoView.notifyImageInvalidated(i); + mPhotoView.notifyImageChange(i); + break; } } updateImageRequests(); @@ -260,7 +257,7 @@ public class PhotoDataAdapter implements PhotoPage.Model { } if (version == getVersion(mCurrentIndex)) { updateTileProvider(entry); - mPhotoView.notifyImageInvalidated(0); + mPhotoView.notifyImageChange(0); } } updateImageRequests(); @@ -275,7 +272,7 @@ public class PhotoDataAdapter implements PhotoPage.Model { mReloadTask = new ReloadTask(); mReloadTask.start(); - mPhotoView.notifyModelInvalidated(); + fireDataChange(); } public void pause() { @@ -302,12 +299,8 @@ public class PhotoDataAdapter implements PhotoPage.Model { return entry == null ? null : entry.screenNail; } - public ScreenNail getPrevScreenNail() { - return getImage(mCurrentIndex - 1); - } - - public ScreenNail getNextScreenNail() { - return getImage(mCurrentIndex + 1); + public ScreenNail getScreenNail(int offset) { + return getImage(mCurrentIndex + offset); } private void updateCurrentIndex(int index) { @@ -320,12 +313,12 @@ public class PhotoDataAdapter implements PhotoPage.Model { updateImageCache(); updateImageRequests(); updateTileProvider(); - mPhotoView.notifyOnNewImage(); if (mDataListener != null) { mDataListener.onPhotoChanged(index, mItemPath); } - fireModelInvalidated(); + + fireDataChange(); } public void next() { @@ -384,7 +377,7 @@ public class PhotoDataAdapter implements PhotoPage.Model { mCurrentIndex = indexHint; updateSlidingWindow(); updateImageCache(); - fireModelInvalidated(); + fireDataChange(); // We need to reload content if the path doesn't match. MediaItem item = getCurrentMediaItem(); @@ -735,7 +728,7 @@ public class PhotoDataAdapter implements PhotoPage.Model { updateImageCache(); updateTileProvider(); updateImageRequests(); - fireModelInvalidated(); + fireDataChange(); return null; } @@ -743,12 +736,8 @@ public class PhotoDataAdapter implements PhotoPage.Model { if (mSize == 0) return; if (mCurrentIndex >= mSize) { mCurrentIndex = mSize - 1; - mPhotoView.notifyOnNewImage(); - mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT); - } else { - mPhotoView.notifyOnNewImage(); - mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT); } + fireDataChange(); } } diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java index ad0d31a7d..6ef5040f3 100644 --- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -118,7 +118,7 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter setScreenNail(bundle.backupImage, bundle.decoder.getWidth(), bundle.decoder.getHeight()); setRegionDecoder(bundle.decoder); - mPhotoView.notifyImageInvalidated(0); + mPhotoView.notifyImageChange(0); } catch (Throwable t) { Log.w(TAG, "fail to decode large", t); } @@ -129,8 +129,7 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter Bitmap backup = future.get(); if (backup == null) return; setScreenNail(backup, backup.getWidth(), backup.getHeight()); - mPhotoView.notifyOnNewImage(); - mPhotoView.notifyImageInvalidated(0); // the current image + mPhotoView.notifyImageChange(0); } catch (Throwable t) { Log.w(TAG, "fail to decode thumb", t); } @@ -158,11 +157,7 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter } } - public ScreenNail getNextScreenNail() { - return null; - } - - public ScreenNail getPrevScreenNail() { + public ScreenNail getScreenNail(int offset) { return null; } diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java index 64ee27c7b..063677423 100644 --- a/src/com/android/gallery3d/ui/AnimationTime.java +++ b/src/com/android/gallery3d/ui/AnimationTime.java @@ -1,3 +1,4 @@ + /* * Copyright (C) 2012 The Android Open Source Project * @@ -36,4 +37,9 @@ public class AnimationTime { public static long get() { return sTime; } + + public static long startTime() { + sTime = SystemClock.uptimeMillis(); + return sTime; + } } diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java index 5a1006845..3481aa18e 100644 --- a/src/com/android/gallery3d/ui/BitmapScreenNail.java +++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java @@ -63,6 +63,7 @@ public class BitmapScreenNail implements ScreenNail { public void pauseDraw() { if (mTexture != null) { mTexture.recycle(); + mTexture = null; } } diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java index db6a45c4f..bf97108a8 100644 --- a/src/com/android/gallery3d/ui/EdgeView.java +++ b/src/com/android/gallery3d/ui/EdgeView.java @@ -23,6 +23,7 @@ import android.opengl.Matrix; public class EdgeView extends GLView { private static final String TAG = "EdgeView"; + public static final int INVALID_DIRECTION = -1; public static final int TOP = 0; public static final int LEFT = 1; public static final int BOTTOM = 2; diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java index bed2908c4..0268ef95d 100644 --- a/src/com/android/gallery3d/ui/GLRootView.java +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -103,7 +103,6 @@ public class GLRootView extends GLSurfaceView setEGLConfigChooser(mEglConfigChooser); setRenderer(this); getHolder().setFormat(PixelFormat.RGB_565); - AnimationTime.update(); // Uncomment this to enable gl error check. //setDebugFlags(DEBUG_CHECK_GL_ERROR); @@ -338,6 +337,7 @@ public class GLRootView extends GLSurfaceView @Override public boolean dispatchTouchEvent(MotionEvent event) { + AnimationTime.update(); int action = event.getAction(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java index 4bd4f3a75..b2a4be49d 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -29,41 +29,52 @@ import android.view.animation.AccelerateInterpolator; 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); @@ -72,12 +83,20 @@ public class PhotoView extends GLView { 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; @@ -88,8 +107,7 @@ public class PhotoView extends GLView { 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; @@ -100,11 +118,9 @@ public class PhotoView extends GLView { 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); @@ -120,148 +136,78 @@ public class PhotoView extends GLView { 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: @@ -276,7 +222,7 @@ public class PhotoView extends GLView { 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); @@ -285,242 +231,367 @@ public class PhotoView extends GLView { } } - 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); } @@ -529,9 +600,8 @@ public class PhotoView extends GLView { @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; @@ -545,13 +615,7 @@ public class PhotoView extends GLView { @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; } @@ -559,8 +623,6 @@ public class PhotoView extends GLView { 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; } @@ -569,38 +631,54 @@ public class PhotoView extends GLView { @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 @@ -615,183 +693,224 @@ public class PhotoView extends GLView { 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 @@ -868,39 +987,15 @@ public class PhotoView extends GLView { } } - 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; - } } diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java index 625505f49..d190e04ec 100644 --- a/src/com/android/gallery3d/ui/PositionController.java +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -18,352 +18,415 @@ package com.android.gallery3d.ui; 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); } } @@ -371,23 +434,51 @@ class PositionController { // 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; } @@ -402,202 +493,443 @@ class PositionController { (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 @@ -615,111 +947,431 @@ class PositionController { // 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; } } diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java new file mode 100644 index 000000000..8e61348a3 --- /dev/null +++ b/src/com/android/gallery3d/util/RangeArray.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeArray<T> { + private T[] mData; + private int mOffset; + + public RangeArray(int min, int max) { + mData = (T[]) new Object[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeArray(T[] src, int min, int max) { + if (max - min + 1 != src.length) { + throw new AssertionError(); + } + mData = src; + mOffset = min; + } + + public void put(int i, T object) { + mData[i - mOffset] = object; + } + + public T get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(T object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java new file mode 100644 index 000000000..035fc40a4 --- /dev/null +++ b/src/com/android/gallery3d/util/RangeBoolArray.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeBoolArray { + private boolean[] mData; + private int mOffset; + + public RangeBoolArray(int min, int max) { + mData = new boolean[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeBoolArray(boolean[] src, int min, int max) { + mData = src; + mOffset = min; + } + + public void put(int i, boolean object) { + mData[i - mOffset] = object; + } + + public boolean get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(boolean object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java new file mode 100644 index 000000000..9dbb99fac --- /dev/null +++ b/src/com/android/gallery3d/util/RangeIntArray.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeIntArray { + private int[] mData; + private int mOffset; + + public RangeIntArray(int min, int max) { + mData = new int[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeIntArray(int[] src, int min, int max) { + mData = src; + mOffset = min; + } + + public void put(int i, int object) { + mData[i - mOffset] = object; + } + + public int get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(int object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} |