diff options
author | Chih-Chung Chang <chihchung@google.com> | 2011-10-12 17:10:33 +0800 |
---|---|---|
committer | Chih-Chung Chang <chihchung@google.com> | 2011-10-13 12:10:12 +0800 |
commit | be074856000e2b417425585da0b4ffb36fc45bd1 (patch) | |
tree | 9c0a9488a2f247bfc33fdef2034fa29316d413e5 /src/com | |
parent | 80481cb46180021abe0f3cf959dacca2519b9c53 (diff) | |
download | android_packages_apps_Snap-be074856000e2b417425585da0b4ffb36fc45bd1.tar.gz android_packages_apps_Snap-be074856000e2b417425585da0b4ffb36fc45bd1.tar.bz2 android_packages_apps_Snap-be074856000e2b417425585da0b4ffb36fc45bd1.zip |
Fix 5254974: Add EdgeEffect for PhotoView.
Change-Id: Ib9ea8fff14a932e8ec25c3f272fe0539776bb062
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/gallery3d/ui/EdgeEffect.java | 440 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/EdgeView.java | 128 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/FilmStripView.java | 2 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/FlingScroller.java | 16 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/GLCanvasImpl.java | 2 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PhotoView.java | 67 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PositionController.java | 73 |
7 files changed, 696 insertions, 32 deletions
diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java new file mode 100644 index 000000000..b2d83f5ba --- /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 000000000..db6a45c4f --- /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 eaf041ead..e6ed49b6e 100644 --- a/src/com/android/gallery3d/ui/FilmStripView.java +++ b/src/com/android/gallery3d/ui/FilmStripView.java @@ -79,7 +79,7 @@ public class FilmStripView extends GLView implements ScrollBarView.Listener, 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 0ba3d5d33..9aef07421 100644 --- a/src/com/android/gallery3d/ui/FlingScroller.java +++ b/src/com/android/gallery3d/ui/FlingScroller.java @@ -42,6 +42,7 @@ class FlingScroller { private int mFinalX, mFinalY; private int mCurrX, mCurrY; + private double mCurrV; public int getFinalX() { return mFinalX; @@ -64,6 +65,14 @@ class FlingScroller { 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 @@ class FlingScroller { 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 @@ class FlingScroller { 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 ab0d91b32..612c7c4f2 100644 --- a/src/com/android/gallery3d/ui/GLCanvasImpl.java +++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java @@ -231,7 +231,7 @@ public class GLCanvasImpl implements GLCanvas { 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 f0e5142f5..5062c0e8e 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -85,6 +85,7 @@ public class PhotoView extends GLView { 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 @@ public class PhotoView extends GLView { 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 @@ public class PhotoView extends GLView { 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 @@ public class PhotoView extends GLView { 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 @@ public class PhotoView extends GLView { 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 class PhotoView extends GLView { 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 @@ public class PhotoView extends GLView { @Override public void onScaleEnd(ScaleGestureDetector detector) { mPositionController.endScale(); - swipeImages(0); + snapToNeighborImage(); } } @@ -565,11 +582,13 @@ public class PhotoView extends GLView { } 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 5d66f70d4..abffbc58f 100644 --- a/src/com/android/gallery3d/ui/PositionController.java +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -63,6 +63,7 @@ class PositionController { 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 @@ class PositionController { 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 @@ class PositionController { 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 @@ class PositionController { 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 @@ class PositionController { public int getImageHeight() { return mImageH; } + + public boolean isAtLeftEdge() { + calculateStableBound(mCurrentScale); + return mCurrentX <= mBoundLeft; + } + + public boolean isAtRightEdge() { + calculateStableBound(mCurrentScale); + return mCurrentX >= mBoundRight; + } } |