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;
+ }
}