Fix 5254974: Add EdgeEffect for PhotoView.

Change-Id: Ib9ea8fff14a932e8ec25c3f272fe0539776bb062
diff --git a/res/drawable-hdpi/overscroll_edge.png b/res/drawable-hdpi/overscroll_edge.png
new file mode 100644
index 0000000..08fc022
--- /dev/null
+++ b/res/drawable-hdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-hdpi/overscroll_glow.png b/res/drawable-hdpi/overscroll_glow.png
new file mode 100644
index 0000000..8f0c2cb
--- /dev/null
+++ b/res/drawable-hdpi/overscroll_glow.png
Binary files differ
diff --git a/res/drawable-mdpi/overscroll_edge.png b/res/drawable-mdpi/overscroll_edge.png
new file mode 100644
index 0000000..4c87a8b
--- /dev/null
+++ b/res/drawable-mdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-mdpi/overscroll_glow.png b/res/drawable-mdpi/overscroll_glow.png
new file mode 100644
index 0000000..8389ef4
--- /dev/null
+++ b/res/drawable-mdpi/overscroll_glow.png
Binary files differ
diff --git a/res/drawable-xhdpi/overscroll_edge.png b/res/drawable-xhdpi/overscroll_edge.png
new file mode 100644
index 0000000..4fe6c27
--- /dev/null
+++ b/res/drawable-xhdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-xhdpi/overscroll_glow.png b/res/drawable-xhdpi/overscroll_glow.png
new file mode 100644
index 0000000..75c3eb4
--- /dev/null
+++ b/res/drawable-xhdpi/overscroll_glow.png
Binary files differ
diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java
new file mode 100644
index 0000000..b2d83f5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeEffect.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+// This is copied from android.widget.EdgeEffect with some small modifications:
+// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
+// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
+// (3) Use a private Drawable class (which inherits from ResourceTexture)
+//     instead of android.graphics.drawable.Drawable to hold the images.
+//     The private Drawable class is used to translate original Canvas calls to
+//     corresponding GLCanvas calls.
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+    private static final String TAG = "EdgeEffect";
+
+    // Time it will take the effect to fully recede in ms
+    private static final int RECEDE_TIME = 1000;
+
+    // Time it will take before a pulled glow begins receding in ms
+    private static final int PULL_TIME = 167;
+
+    // Time it will take in ms for a pulled glow to decay to partial strength before release
+    private static final int PULL_DECAY_TIME = 1000;
+
+    private static final float MAX_ALPHA = 0.8f;
+    private static final float HELD_EDGE_ALPHA = 0.7f;
+    private static final float HELD_EDGE_SCALE_Y = 0.5f;
+    private static final float HELD_GLOW_ALPHA = 0.5f;
+    private static final float HELD_GLOW_SCALE_Y = 0.5f;
+
+    private static final float MAX_GLOW_HEIGHT = 4.f;
+
+    private static final float PULL_GLOW_BEGIN = 1.f;
+    private static final float PULL_EDGE_BEGIN = 0.6f;
+
+    // Minimum velocity that will be absorbed
+    private static final int MIN_VELOCITY = 100;
+
+    private static final float EPSILON = 0.001f;
+
+    private final Drawable mEdge;
+    private final Drawable mGlow;
+    private int mWidth;
+    private int mHeight;
+    private final int MIN_WIDTH = 300;
+    private final int mMinWidth;
+
+    private float mEdgeAlpha;
+    private float mEdgeScaleY;
+    private float mGlowAlpha;
+    private float mGlowScaleY;
+
+    private float mEdgeAlphaStart;
+    private float mEdgeAlphaFinish;
+    private float mEdgeScaleYStart;
+    private float mEdgeScaleYFinish;
+    private float mGlowAlphaStart;
+    private float mGlowAlphaFinish;
+    private float mGlowScaleYStart;
+    private float mGlowScaleYFinish;
+
+    private long mStartTime;
+    private float mDuration;
+
+    private final Interpolator mInterpolator;
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RECEDE = 3;
+    private static final int STATE_PULL_DECAY = 4;
+
+    // How much dragging should effect the height of the edge image.
+    // Number determined by user testing.
+    private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
+
+    // How much dragging should effect the height of the glow image.
+    // Number determined by user testing.
+    private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
+    private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
+
+    private static final int VELOCITY_EDGE_FACTOR = 8;
+    private static final int VELOCITY_GLOW_FACTOR = 16;
+
+    private int mState = STATE_IDLE;
+
+    private float mPullDistance;
+
+    /**
+     * Construct a new EdgeEffect with a theme appropriate for the provided context.
+     * @param context Context used to provide theming and resource information for the EdgeEffect
+     */
+    public EdgeEffect(Context context) {
+        mEdge = new Drawable(context, R.drawable.overscroll_edge);
+        mGlow = new Drawable(context, R.drawable.overscroll_glow);
+
+        mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
+        mInterpolator = new DecelerateInterpolator();
+    }
+
+    /**
+     * Set the size of this edge effect in pixels.
+     *
+     * @param width Effect width in pixels
+     * @param height Effect height in pixels
+     */
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    /**
+     * Reports if this EdgeEffect's animation is finished. If this method returns false
+     * after a call to {@link #draw(Canvas)} the host widget should schedule another
+     * drawing pass to continue the animation.
+     *
+     * @return true if animation is finished, false if drawing should continue on the next frame.
+     */
+    public boolean isFinished() {
+        return mState == STATE_IDLE;
+    }
+
+    /**
+     * Immediately finish the current animation.
+     * After this call {@link #isFinished()} will return true.
+     */
+    public void finish() {
+        mState = STATE_IDLE;
+    }
+
+    /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly.
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     */
+    public void onPull(float deltaDistance) {
+        final long now = AnimationUtils.currentAnimationTimeMillis();
+        if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+            return;
+        }
+        if (mState != STATE_PULL) {
+            mGlowScaleY = PULL_GLOW_BEGIN;
+        }
+        mState = STATE_PULL;
+
+        mStartTime = now;
+        mDuration = PULL_TIME;
+
+        mPullDistance += deltaDistance;
+        float distance = Math.abs(mPullDistance);
+
+        mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
+        mEdgeScaleY = mEdgeScaleYStart = Math.max(
+                HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
+
+        mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+                mGlowAlpha +
+                (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+        float glowChange = Math.abs(deltaDistance);
+        if (deltaDistance > 0 && mPullDistance < 0) {
+            glowChange = -glowChange;
+        }
+        if (mPullDistance == 0) {
+            mGlowScaleY = 0;
+        }
+
+        // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
+        mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
+                0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
+
+        mEdgeAlphaFinish = mEdgeAlpha;
+        mEdgeScaleYFinish = mEdgeScaleY;
+        mGlowAlphaFinish = mGlowAlpha;
+        mGlowScaleYFinish = mGlowScaleY;
+    }
+
+    /**
+     * Call when the object is released after being pulled.
+     * This will begin the "decay" phase of the effect. After calling this method
+     * the host view should {@link android.view.View#invalidate()} and thereby
+     * draw the results accordingly.
+     */
+    public void onRelease() {
+        mPullDistance = 0;
+
+        if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+            return;
+        }
+
+        mState = STATE_RECEDE;
+        mEdgeAlphaStart = mEdgeAlpha;
+        mEdgeScaleYStart = mEdgeScaleY;
+        mGlowAlphaStart = mGlowAlpha;
+        mGlowScaleYStart = mGlowScaleY;
+
+        mEdgeAlphaFinish = 0.f;
+        mEdgeScaleYFinish = 0.f;
+        mGlowAlphaFinish = 0.f;
+        mGlowScaleYFinish = 0.f;
+
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mDuration = RECEDE_TIME;
+    }
+
+    /**
+     * Call when the effect absorbs an impact at the given velocity.
+     * Used when a fling reaches the scroll boundary.
+     *
+     * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+     * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+     * to use here.</p>
+     *
+     * @param velocity Velocity at impact in pixels per second.
+     */
+    public void onAbsorb(int velocity) {
+        mState = STATE_ABSORB;
+        velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
+
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mDuration = 0.1f + (velocity * 0.03f);
+
+        // The edge should always be at least partially visible, regardless
+        // of velocity.
+        mEdgeAlphaStart = 0.f;
+        mEdgeScaleY = mEdgeScaleYStart = 0.f;
+        // The glow depends more on the velocity, and therefore starts out
+        // nearly invisible.
+        mGlowAlphaStart = 0.5f;
+        mGlowScaleYStart = 0.f;
+
+        // Factor the velocity by 8. Testing on device shows this works best to
+        // reflect the strength of the user's scrolling.
+        mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
+        // Edge should never get larger than the size of its asset.
+        mEdgeScaleYFinish = Math.max(
+                HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
+
+        // Growth for the size of the glow should be quadratic to properly
+        // respond
+        // to a user's scrolling speed. The faster the scrolling speed, the more
+        // intense the effect should be for both the size and the saturation.
+        mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
+        // Alpha should change for the glow as well as size.
+        mGlowAlphaFinish = Math.max(
+                mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+    }
+
+
+    /**
+     * Draw into the provided canvas. Assumes that the canvas has been rotated
+     * accordingly and the size has been set. The effect will be drawn the full
+     * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+     * 1.f of height.
+     *
+     * @param canvas Canvas to draw into
+     * @return true if drawing should continue beyond this frame to continue the
+     *         animation
+     */
+    public boolean draw(GLCanvas canvas) {
+        update();
+
+        final int edgeHeight = mEdge.getIntrinsicHeight();
+        final int edgeWidth = mEdge.getIntrinsicWidth();
+        final int glowHeight = mGlow.getIntrinsicHeight();
+        final int glowWidth = mGlow.getIntrinsicWidth();
+
+        mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
+
+        int glowBottom = (int) Math.min(
+                glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
+                glowHeight * MAX_GLOW_HEIGHT);
+        if (mWidth < mMinWidth) {
+            // Center the glow and clip it.
+            int glowLeft = (mWidth - mMinWidth)/2;
+            mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
+        } else {
+            // Stretch the glow to fit.
+            mGlow.setBounds(0, 0, mWidth, glowBottom);
+        }
+
+        mGlow.draw(canvas);
+
+        mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
+
+        int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
+        if (mWidth < mMinWidth) {
+            // Center the edge and clip it.
+            int edgeLeft = (mWidth - mMinWidth)/2;
+            mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
+        } else {
+            // Stretch the edge to fit.
+            mEdge.setBounds(0, 0, mWidth, edgeBottom);
+        }
+        mEdge.draw(canvas);
+
+        return mState != STATE_IDLE;
+    }
+
+    private void update() {
+        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+        final float interp = mInterpolator.getInterpolation(t);
+
+        mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
+        mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
+        mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+        mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+
+        if (t >= 1.f - EPSILON) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    mState = STATE_RECEDE;
+                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mDuration = RECEDE_TIME;
+
+                    mEdgeAlphaStart = mEdgeAlpha;
+                    mEdgeScaleYStart = mEdgeScaleY;
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After absorb, the glow and edge should fade to nothing.
+                    mEdgeAlphaFinish = 0.f;
+                    mEdgeScaleYFinish = 0.f;
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL:
+                    mState = STATE_PULL_DECAY;
+                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mDuration = PULL_DECAY_TIME;
+
+                    mEdgeAlphaStart = mEdgeAlpha;
+                    mEdgeScaleYStart = mEdgeScaleY;
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After pull, the glow and edge should fade to nothing.
+                    mEdgeAlphaFinish = 0.f;
+                    mEdgeScaleYFinish = 0.f;
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL_DECAY:
+                    // When receding, we want edge to decrease more slowly
+                    // than the glow.
+                    float factor = mGlowScaleYFinish != 0 ? 1
+                            / (mGlowScaleYFinish * mGlowScaleYFinish)
+                            : Float.MAX_VALUE;
+                    mEdgeScaleY = mEdgeScaleYStart +
+                        (mEdgeScaleYFinish - mEdgeScaleYStart) *
+                            interp * factor;
+                    mState = STATE_RECEDE;
+                    break;
+                case STATE_RECEDE:
+                    mState = STATE_IDLE;
+                    break;
+            }
+        }
+    }
+
+    private static class Drawable extends ResourceTexture {
+        private Rect mBounds = new Rect();
+        private int mAlpha = 255;
+
+        public Drawable(Context context, int resId) {
+            super(context, resId);
+        }
+
+        public int getIntrinsicWidth() {
+            return getWidth();
+        }
+
+        public int getIntrinsicHeight() {
+            return getHeight();
+        }
+
+        public void setBounds(int left, int top, int right, int bottom) {
+            mBounds.set(left, top, right, bottom);
+        }
+
+        public void setAlpha(int alpha) {
+            mAlpha = alpha;
+        }
+
+        public void draw(GLCanvas canvas) {
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(mAlpha / 255.0f);
+            Rect b = mBounds;
+            draw(canvas, b.left, b.top, b.width(), b.height());
+            canvas.restore();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java
new file mode 100644
index 0000000..db6a45c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeView.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.opengl.Matrix;
+
+// EdgeView draws EdgeEffect (blue glow) at four sides of the view.
+public class EdgeView extends GLView {
+    private static final String TAG = "EdgeView";
+
+    public static final int TOP = 0;
+    public static final int LEFT = 1;
+    public static final int BOTTOM = 2;
+    public static final int RIGHT = 3;
+
+    // Each edge effect has a transform matrix, and each matrix has 16 elements.
+    // We put all the elements in one array. These constants specify the
+    // starting index of each matrix.
+    private static final int TOP_M = TOP * 16;
+    private static final int LEFT_M = LEFT * 16;
+    private static final int BOTTOM_M = BOTTOM * 16;
+    private static final int RIGHT_M = RIGHT * 16;
+
+    private EdgeEffect[] mEffect = new EdgeEffect[4];
+    private float[] mMatrix = new float[4 * 16];
+
+    public EdgeView(Context context) {
+        for (int i = 0; i < 4; i++) {
+            mEffect[i] = new EdgeEffect(context);
+        }
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changeSize, int left, int top, int right, int bottom) {
+        if (!changeSize) return;
+
+        int w = right - left;
+        int h = bottom - top;
+        for (int i = 0; i < 4; i++) {
+            if ((i & 1) == 0) {  // top or bottom
+                mEffect[i].setSize(w, h);
+            } else {  // left or right
+                mEffect[i].setSize(h, w);
+            }
+        }
+
+        // Set up transforms for the four edges. Without transforms an
+        // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y
+        // is some factor < 1. For other edges we need to move, rotate, and
+        // flip the effects into proper places.
+        Matrix.setIdentityM(mMatrix, TOP_M);
+        Matrix.setIdentityM(mMatrix, LEFT_M);
+        Matrix.setIdentityM(mMatrix, BOTTOM_M);
+        Matrix.setIdentityM(mMatrix, RIGHT_M);
+
+        Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1);
+        Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1);
+
+        Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0);
+        Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1);
+
+        Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0);
+        Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1);
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        super.render(canvas);
+        boolean more = false;
+        for (int i = 0; i < 4; i++) {
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            canvas.multiplyMatrix(mMatrix, i * 16);
+            more |= mEffect[i].draw(canvas);
+            canvas.restore();
+        }
+        if (more) {
+            invalidate();
+        }
+    }
+
+    // Called when the content is pulled away from the edge.
+    // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+    public void onPull(int offset, int direction) {
+        int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight();
+        mEffect[direction].onPull((float)offset / fullLength);
+        if (!mEffect[direction].isFinished()) {
+            invalidate();
+        }
+    }
+
+    // Call when the object is released after being pulled.
+    public void onRelease() {
+        boolean more = false;
+        for (int i = 0; i < 4; i++) {
+            mEffect[i].onRelease();
+            more |= !mEffect[i].isFinished();
+        }
+        if (more) {
+            invalidate();
+        }
+    }
+
+    // Call when the effect absorbs an impact at the given velocity.
+    // Used when a fling reaches the scroll boundary. velocity is in pixels
+    // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+    public void onAbsorb(int velocity, int direction) {
+        mEffect[direction].onAbsorb(velocity);
+        if (!mEffect[direction].isFinished()) {
+            invalidate();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
index eaf041e..e6ed49b 100644
--- a/src/com/android/gallery3d/ui/FilmStripView.java
+++ b/src/com/android/gallery3d/ui/FilmStripView.java
@@ -79,7 +79,7 @@
         spec.slotWidth = thumbSize;
         spec.slotHeight = thumbSize;
         mAlbumView = new AlbumView(activity, spec, thumbSize);
-        mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM);
+        mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_NONE);
         mAlbumView.setSelectionDrawer(mStripDrawer);
         mAlbumView.setListener(new SlotView.SimpleListener() {
             @Override
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
index 0ba3d5d..9aef074 100644
--- a/src/com/android/gallery3d/ui/FlingScroller.java
+++ b/src/com/android/gallery3d/ui/FlingScroller.java
@@ -42,6 +42,7 @@
     private int mFinalX, mFinalY;
 
     private int mCurrX, mCurrY;
+    private double mCurrV;
 
     public int getFinalX() {
         return mFinalX;
@@ -64,6 +65,14 @@
         return mCurrY;
     }
 
+    public int getCurrVelocityX() {
+        return (int)Math.round(mCurrV * mCosAngle);
+    }
+
+    public int getCurrVelocityY() {
+        return (int)Math.round(mCurrV * mSinAngle);
+    }
+
     public void fling(int startX, int startY, int velocityX, int velocityY,
             int minX, int maxX, int minY, int maxY) {
         mStartX = startX;
@@ -101,6 +110,7 @@
         f = 1 - (float) Math.pow(f, DECELERATED_FACTOR);
         mCurrX = getX(f);
         mCurrY = getY(f);
+        mCurrV = getV(progress);
     }
 
     private int getX(float f) {
@@ -112,4 +122,10 @@
         return (int) Utils.clamp(
                 Math.round(mStartY + f * mDistance * mSinAngle), mMinY, mMaxY);
     }
+
+    private double getV(float progress) {
+        // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+        return DECELERATED_FACTOR * mDistance * 1000 *
+                Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration;
+    }
 }
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index ab0d91b..612c7c4 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -231,7 +231,7 @@
 
     public void multiplyMatrix(float matrix[], int offset) {
         float[] temp = mTempMatrix;
-        Matrix.multiplyMM(temp, 0, mMatrixValues , 0, matrix, 0);
+        Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
         System.arraycopy(temp, 0, mMatrixValues, 0, 16);
     }
 
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index f0e5142..5062c0e 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -85,6 +85,7 @@
     private StringTexture mNoThumbnailText;
     private int mTransitionMode = TRANS_NONE;
     private final TileImageView mTileView;
+    private EdgeView mEdgeView;
     private Texture mVideoPlayIcon;
 
     private boolean mShowVideoPlayIcon;
@@ -104,6 +105,8 @@
         mTileView = new TileImageView(activity);
         addComponent(mTileView);
         Context context = activity.getAndroidContext();
+        mEdgeView = new EdgeView(context);
+        addComponent(mEdgeView);
         mLoadingSpinner = new ProgressSpinner(context);
         mLoadingText = StringTexture.newInstance(
                 context.getString(R.string.loading),
@@ -145,7 +148,7 @@
             mScreenNails[i] = new ScreenNailEntry();
         }
 
-        mPositionController = new PositionController(this, context);
+        mPositionController = new PositionController(this, context, mEdgeView);
         mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
     }
 
@@ -281,6 +284,7 @@
     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) {
@@ -410,33 +414,41 @@
 
         int width = getWidth();
 
-        // If the edge of the current photo is visible and the sweeping velocity
-        // exceed the threshold, switch to next / previous image
+        // If we are at the edge of the current photo and the sweeping velocity
+        // exceeds the threshold, switch to next / previous image.
         PositionController controller = mPositionController;
-        if (controller.isAtMinimalScale()) {
-            if (velocity < -SWIPE_THRESHOLD) {
-                stopCurrentSwipingIfNeeded();
-                if (next.isEnabled()) {
-                    mTransitionMode = TRANS_SWITCH_NEXT;
-                    controller.startHorizontalSlide(next.mOffsetX - width / 2);
-                    return true;
-                }
-                return false;
+        boolean isMinimal = controller.isAtMinimalScale();
+
+        if (velocity < -SWIPE_THRESHOLD &&
+                (isMinimal || controller.isAtRightEdge())) {
+            stopCurrentSwipingIfNeeded();
+            if (next.isEnabled()) {
+                mTransitionMode = TRANS_SWITCH_NEXT;
+                controller.startHorizontalSlide(next.mOffsetX - width / 2);
+                return true;
             }
-            if (velocity > SWIPE_THRESHOLD) {
-                stopCurrentSwipingIfNeeded();
-                if (prev.isEnabled()) {
-                    mTransitionMode = TRANS_SWITCH_PREVIOUS;
-                    controller.startHorizontalSlide(prev.mOffsetX - width / 2);
-                    return true;
-                }
-                return false;
+        } else if (velocity > SWIPE_THRESHOLD &&
+                (isMinimal || controller.isAtLeftEdge())) {
+            stopCurrentSwipingIfNeeded();
+            if (prev.isEnabled()) {
+                mTransitionMode = TRANS_SWITCH_PREVIOUS;
+                controller.startHorizontalSlide(prev.mOffsetX - width / 2);
+                return true;
             }
         }
 
+        return false;
+    }
+
+    public boolean snapToNeighborImage() {
         if (mTransitionMode != TRANS_NONE) return false;
 
-        // Decide whether to swiping to the next/prev image in the zoom-in case
+        ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
+        ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
+
+        int width = getWidth();
+        PositionController controller = mPositionController;
+
         RectF bounds = controller.getImageBounds();
         int left = Math.round(bounds.left);
         int right = Math.round(bounds.right);
@@ -465,7 +477,12 @@
         public boolean onScroll(
                 MotionEvent e1, MotionEvent e2, float dx, float dy) {
             if (mTransitionMode != TRANS_NONE) return true;
-            mPositionController.startScroll(dx, dy);
+
+            ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
+            ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
+
+            mPositionController.startScroll(dx, dy, next.isEnabled(),
+                    prev.isEnabled());
             return true;
         }
 
@@ -532,7 +549,7 @@
         @Override
         public void onScaleEnd(ScaleGestureDetector detector) {
             mPositionController.endScale();
-            swipeImages(0);
+            snapToNeighborImage();
         }
     }
 
@@ -565,11 +582,13 @@
         }
 
         public void onUp(MotionEvent e) {
+            mEdgeView.onRelease();
+
             if (mIgnoreUpEvent) {
                 mIgnoreUpEvent = false;
                 return;
             }
-            if (!swipeImages(0) && mTransitionMode == TRANS_NONE) {
+            if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) {
                 mPositionController.up();
             }
         }
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 5d66f70..abffbc5 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -63,6 +63,7 @@
     private static final float SCALE_LIMIT = 4;
 
     private PhotoView mViewer;
+    private EdgeView mEdgeView;
     private int mImageW, mImageH;
     private int mViewW, mViewH;
 
@@ -95,8 +96,10 @@
     private RectF mTempRect = new RectF();
     private float[] mTempPoints = new float[8];
 
-    public PositionController(PhotoView viewer, Context context) {
+    public PositionController(PhotoView viewer, Context context,
+            EdgeView edgeView) {
         mViewer = viewer;
+        mEdgeView = edgeView;
         mScroller = new FlingScroller();
     }
 
@@ -325,16 +328,46 @@
         scrollBy(distance, 0, ANIM_KIND_SLIDE);
     }
 
-    public void startScroll(float dx, float dy) {
-        scrollBy(dx, dy, ANIM_KIND_SCROLL);
-    }
-
     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 hasNext,
+            boolean hasPrev) {
+        int x = getTargetX() + Math.round(dx / mCurrentScale);
+        int y = getTargetY() + Math.round(dy / mCurrentScale);
+
+        calculateStableBound(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);
+            } else if (y > mBoundBottom) {
+                mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
+            }
+        }
+
+        y = Utils.clamp(y, mBoundTop, mBoundBottom);
+
+        // Horizontal direction: we show the edge effect when the scrolling
+        // tries to go left of the first image or go right of the last image.
+        if (!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);
+            x = mBoundRight;
+        }
+
+        startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
+    }
+
     public boolean fling(float velocityX, float velocityY) {
         // We only want to do fling when the picture is zoomed-in.
         if (mImageW * mCurrentScale <= mViewW &&
@@ -439,9 +472,27 @@
 
     private void flingInterpolate(float progress) {
         mScroller.computeScrollOffset(progress);
+        int oldX = mCurrentX;
+        int oldY = mCurrentY;
         mCurrentX = mScroller.getCurrX();
         mCurrentY = mScroller.getCurrY();
-        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+
+        // 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].
@@ -596,4 +647,14 @@
     public int getImageHeight() {
         return mImageH;
     }
+
+    public boolean isAtLeftEdge() {
+        calculateStableBound(mCurrentScale);
+        return mCurrentX <= mBoundLeft;
+    }
+
+    public boolean isAtRightEdge() {
+        calculateStableBound(mCurrentScale);
+        return mCurrentX >= mBoundRight;
+    }
 }