New effect in photo browsing.

Change-Id: I12ca70f56f5c874292a7a6f38f0518bbc855ed70
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 3ec245f..c2c750b 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -19,13 +19,16 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Message;
 import android.view.MotionEvent;
+import android.view.animation.AccelerateInterpolator;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
 
 public class PhotoView extends GLView {
     @SuppressWarnings("unused")
@@ -59,6 +62,14 @@
     private static final float SWIPE_THRESHOLD = 300f;
 
     private static final float DEFAULT_TEXT_SIZE = 20;
+    private static float TRANSITION_SCALE_FACTOR = 0.74f;
+
+    // Used to calculate the scaling factor for the fading animation.
+    private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
+
+    // Used to calculate the alpha factor for the fading animation.
+    private AccelerateInterpolator mAlphaInterpolator =
+            new AccelerateInterpolator(0.9f);
 
     public interface PhotoTapListener {
         public void onSingleTapUp(int x, int y);
@@ -91,6 +102,7 @@
     private int mImageRotation;
 
     private Rect mOpenAnimationRect;
+    private Point mImageCenter = new Point();
 
     public PhotoView(GalleryActivity activity) {
         mTileView = new TileImageView(activity);
@@ -153,24 +165,49 @@
         mPhotoTapListener = listener;
     }
 
-    private boolean setTileViewPosition(int centerX, int centerY, float scale) {
+    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;
+            } 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);
+            }
+            scale *= getScrollScale(progress);
+            t.setAlpha(getScrollAlpha(progress));
+        }
+
+        // set the position of the tile view
         int inverseX = mPositionController.getImageWidth() - centerX;
         int inverseY = mPositionController.getImageHeight() - centerY;
-        TileImageView t = mTileView;
         int rotation = mImageRotation;
         switch (rotation) {
-            case 0: return t.setPosition(centerX, centerY, scale, 0);
-            case 90: return t.setPosition(centerY, inverseX, scale, 90);
-            case 180: return t.setPosition(inverseX, inverseY, scale, 180);
-            case 270: return t.setPosition(inverseY, centerX, scale, 270);
+            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) {
-        if (setTileViewPosition(centerX, centerY, scale)) {
-            layoutScreenNails();
-        }
+        setTileViewPosition(centerX, centerY, scale);
+        layoutScreenNails();
     }
 
     private void updateScreenNailEntry(int which, ScreenNail screenNail) {
@@ -202,6 +239,7 @@
             case 0: {
                 // mImageWidth and mImageHeight will get updated
                 mTileView.notifyModelInvalidated();
+                mTileView.setAlpha(1.0f);
 
                 mImageRotation = mModel.getImageRotation();
                 if (((mImageRotation / 90) & 1) == 0) {
@@ -251,6 +289,7 @@
 
         if (mModel == null) {
             mTileView.notifyModelInvalidated();
+            mTileView.setAlpha(1.0f);
             mImageRotation = 0;
             mPositionController.setImageSize(0, 0);
             updateLoadingState();
@@ -325,22 +364,38 @@
 
     @Override
     protected void render(GLCanvas canvas) {
+        boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT
+                && mTransitionMode != TRANS_SLIDE_IN_RIGHT
+                && mTransitionMode != TRANS_OPEN_ANIMATION);
+
+        // Draw the next photo
+        if (drawScreenNail) {
+            ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+            nextNail.draw(canvas, true);
+        }
+
         // Draw the current photo
         if (mLoadingState == LOADING_COMPLETE) {
             super.render(canvas);
         }
 
-        // Draw the previous and the next photo
-        if (mTransitionMode != TRANS_SLIDE_IN_LEFT
-                && mTransitionMode != TRANS_SLIDE_IN_RIGHT
-                && mTransitionMode != TRANS_OPEN_ANIMATION) {
-            ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
-            ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
-
-            prevNail.draw(canvas);
-            nextNail.draw(canvas);
+        // 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);
         }
 
+        // Draw the previous photo
+        if (drawScreenNail) {
+            ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+            prevNail.draw(canvas, false);
+        }
+    }
+
+    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.
@@ -349,8 +404,6 @@
         // play icon is shown instead of the spinner.
         int w = getWidth();
         int h = getHeight();
-        int x = Math.round(mPositionController.getImageBounds().centerX());
-        int y = h / 2;
         int s = Math.min(getWidth(), getHeight()) / 6;
 
         if (mLoadingState == LOADING_TIMEOUT) {
@@ -688,29 +741,111 @@
             return mEnabled;
         }
 
-        public void draw(GLCanvas canvas) {
+        public void draw(GLCanvas canvas, boolean applyFadingAnimation) {
             if (mScreenNail == null) return;
             if (!mVisible) {
                 mScreenNail.disableDraw();
                 return;
             }
 
-            int x = mOffsetX;
+            int w = getWidth();
+            int x = applyFadingAnimation ? w / 2 : mOffsetX;
             int y = getHeight() / 2;
+            int flags = GLCanvas.SAVE_FLAG_MATRIX;
 
-            if (mRotation != 0) {
-                canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
-                canvas.translate(x, y);
-                canvas.rotate(mRotation, 0, 0, 1);
-                canvas.translate(-x, -y);
+            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();
+        }
+    }
 
-            if (mRotation != 0) {
-                canvas.restore();
-            }
+    // 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
+    // at [0, viewWidth].
+    //
+    // The returned value is negative when the object is moving right, and
+    // positive when the object is moving left. The value goes to -1 or 1 when
+    // the object just moves out of the view completely. The value is 0 if the
+    // object currently fills the view.
+    private static float calculateMoveOutProgress(int left, int right,
+            int viewWidth) {
+        // w = object width
+        // viewWidth = view width
+        int w = right - left;
+
+        // If the object width is smaller than the view width,
+        //      |....view....|
+        //                   |<-->|      progress = -1 when left = viewWidth
+        // |<-->|                        progress = 1 when left = -w
+        // So progress = 1 - 2 * (left + w) / (viewWidth + w)
+        if (w < viewWidth) {
+            return 1f - 2f * (left + w) / (viewWidth + w);
+        }
+
+        // If the object width is larger than the view width,
+        //             |..view..|
+        //                      |<--------->| progress = -1 when left = viewWidth
+        //             |<--------->|          progress = 0 between left = 0
+        //          |<--------->|                          and right = viewWidth
+        // |<--------->|                      progress = 1 when right = 0
+        if (left > 0) {
+            return -left / (float) viewWidth;
+        }
+
+        if (right < viewWidth) {
+            return (viewWidth - right) / (float) viewWidth;
+        }
+
+        return 0;
+    }
+
+    // Maps a scrolling progress value to the alpha factor in the fading
+    // animation.
+    private float getScrollAlpha(float scrollProgress) {
+        return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
+                     1 - Math.abs(scrollProgress)) : 1.0f;
+    }
+
+    // Maps a scrolling progress value to the scaling factor in the fading
+    // animation.
+    private float getScrollScale(float scrollProgress) {
+        float interpolatedProgress = mScaleInterpolator.getInterpolation(
+                Math.abs(scrollProgress));
+        float scale = (1 - interpolatedProgress) +
+                interpolatedProgress * TRANSITION_SCALE_FACTOR;
+        return scale;
+    }
+
+
+    // This interpolator emulates the rate at which the perceived scale of an
+    // object changes as its distance from a camera increases. When this
+    // interpolator is applied to a scale animation on a view, it evokes the
+    // sense that the object is shrinking due to moving away from the camera.
+    private static class ZInterpolator {
+        private float focalLength;
+
+        public ZInterpolator(float foc) {
+            focalLength = foc;
+        }
+
+        public float getInterpolation(float input) {
+            return (1.0f - focalLength / (focalLength + input)) /
+                (1.0f - focalLength / (focalLength + 1.0f));
         }
     }