Bug 918079 - Show highlight when overscrolling. r=kats
authorWes Johnston <wjohnston@mozilla.com>
Thu, 26 Sep 2013 22:57:57 -0700
changeset 149077 9433c19bf735b27e4dbcdea5aed5d08982915033
parent 148901 e4cd2242cc7d85e240c585f68730753348fd1a9e
child 149078 72a469699ec39b75f8557e7862745a83fe8ab6b0
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerskats
bugs918079
milestone27.0a1
Bug 918079 - Show highlight when overscrolling. r=kats
mobile/android/base/Makefile.in
mobile/android/base/gfx/Axis.java
mobile/android/base/gfx/GeckoLayerClient.java
mobile/android/base/gfx/JavaPanZoomController.java
mobile/android/base/gfx/LayerView.java
mobile/android/base/gfx/NativePanZoomController.java
mobile/android/base/gfx/Overscroll.java
mobile/android/base/gfx/PanZoomController.java
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -189,16 +189,17 @@ FENNEC_JAVA_FILES = \
   gfx/IntSize.java \
   gfx/JavaPanZoomController.java \
   gfx/Layer.java \
   gfx/LayerMarginsAnimator.java \
   gfx/LayerRenderer.java \
   gfx/LayerView.java \
   gfx/NativePanZoomController.java \
   gfx/NinePatchTileLayer.java \
+  gfx/Overscroll.java \
   gfx/PanningPerfAPI.java \
   gfx/PanZoomController.java \
   gfx/PanZoomTarget.java \
   gfx/PluginLayer.java \
   gfx/PointUtils.java \
   gfx/ProgressiveUpdateData.java \
   gfx/RectUtils.java \
   gfx/RenderTask.java \
--- a/mobile/android/base/gfx/Axis.java
+++ b/mobile/android/base/gfx/Axis.java
@@ -149,16 +149,20 @@ abstract class Axis {
     protected abstract boolean marginsHidden();
 
     Axis(SubdocumentScrollHelper subscroller) {
         mSubscroller = subscroller;
         mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
         mRecentVelocities = new float[FLING_VELOCITY_POINTS];
     }
 
+    // Implementors can override these to show effects when the axis overscrolls
+    protected void overscrollFling(float velocity) { }
+    protected void overscrollPan(float displacement) { }
+
     public void setOverScrollMode(int overscrollMode) {
         mOverscrollMode = overscrollMode;
     }
 
     public int getOverScrollMode() {
         return mOverscrollMode;
     }
 
@@ -374,22 +378,32 @@ abstract class Axis {
         else
             mDisplacement += mVelocity * getEdgeResistance(false);
 
         // if overscroll is disabled and we're trying to overscroll, reset the displacement
         // to remove any excess. Using getExcess alone isn't enough here since it relies on
         // getOverscroll which doesn't take into account any new displacment being applied.
         // If we using a subscroller, we don't want to alter the scrolling being done
         if (getOverScrollMode() == View.OVER_SCROLL_NEVER && !mSubscroller.scrolling()) {
+            float originalDisplacement = mDisplacement;
+
             if (mDisplacement + getOrigin() < getPageStart() - getMarginStart()) {
                 mDisplacement = getPageStart() - getMarginStart() - getOrigin();
-                stopFling();
             } else if (mDisplacement + getViewportEnd() > getPageEnd() + getMarginEnd()) {
                 mDisplacement = getPageEnd() - getMarginEnd() - getViewportEnd();
-                stopFling();
+            }
+
+            // Return the amount of overscroll so that the overscroll controller can draw it for us
+            if (originalDisplacement != mDisplacement) {
+                if (mFlingState == FlingStates.FLINGING) {
+                    overscrollFling(mVelocity / MS_PER_FRAME * 1000);
+                    stopFling();
+                } else if (mFlingState == FlingStates.PANNING) {
+                    overscrollPan(originalDisplacement - mDisplacement);
+                }
             }
         }
     }
 
     float resetDisplacement() {
         float d = mDisplacement;
         mDisplacement = 0.0f;
         return d;
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -127,16 +127,20 @@ public class GeckoLayerClient implements
 
         mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
         mMarginsAnimator = new LayerMarginsAnimator(this, view);
         mView = view;
         mView.setListener(this);
         mContentDocumentIsDisplayed = true;
     }
 
+    public void setOverscrollHandler(final Overscroll listener) {
+        mPanZoomController.setOverscrollHandler(listener);
+    }
+
     /** Attaches to root layer so that Gecko appears. */
     public void notifyGeckoReady() {
         mGeckoIsReady = true;
 
         mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
         mLayerRenderer = mView.getRenderer();
 
         sendResizeEventIfNecessary(true);
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -124,16 +124,19 @@ class JavaPanZoomController
     private float mAutonavZoomDelta;
     /* The user selected panning mode */
     private AxisLockMode mMode;
     /* A medium-length tap/press is happening */
     private boolean mMediumPress;
     /* Used to change the scrollY direction */
     private boolean mNegateWheelScrollY;
 
+    // Handler to be notified when overscroll occurs
+    private Overscroll mOverscroll;
+
     public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) {
         mTarget = target;
         mSubscroller = new SubdocumentScrollHelper(eventDispatcher);
         mX = new AxisX(mSubscroller);
         mY = new AxisY(mSubscroller);
         mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this);
 
         checkMainThread();
@@ -1108,16 +1111,28 @@ class JavaPanZoomController
         @Override
         protected float getPageLength() { return getMetrics().getPageWidthWithMargins(); }
         @Override
         protected boolean marginsHidden() {
             ImmutableViewportMetrics metrics = getMetrics();
             RectF maxMargins = mTarget.getMaxMargins();
             return (metrics.marginLeft < maxMargins.left || metrics.marginRight < maxMargins.right);
         }
+        @Override
+        protected void overscrollFling(final float velocity) {
+            if (mOverscroll != null) {
+                mOverscroll.setVelocity(velocity, Overscroll.Axis.X);
+            }
+        }
+        @Override
+        protected void overscrollPan(final float distance) {
+            if (mOverscroll != null) {
+                mOverscroll.setDistance(distance, Overscroll.Axis.X);
+            }
+        }
     }
 
     private class AxisY extends Axis {
         AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
         @Override
         public float getOrigin() { return getMetrics().viewportRectTop; }
         @Override
         protected float getViewportLength() { return getMetrics().getHeight(); }
@@ -1130,16 +1145,28 @@ class JavaPanZoomController
         @Override
         protected float getMarginEnd() { return mTarget.getMaxMargins().bottom - getMetrics().marginBottom; }
         @Override
         protected boolean marginsHidden() {
             ImmutableViewportMetrics metrics = getMetrics();
             RectF maxMargins = mTarget.getMaxMargins();
             return (metrics.marginTop < maxMargins.top || metrics.marginBottom < maxMargins.bottom);
         }
+        @Override
+        protected void overscrollFling(final float velocity) {
+            if (mOverscroll != null) {
+                mOverscroll.setVelocity(velocity, Overscroll.Axis.Y);
+            }
+        }
+        @Override
+        protected void overscrollPan(final float distance) {
+            if (mOverscroll != null) {
+                mOverscroll.setDistance(distance, Overscroll.Axis.Y);
+            }
+        }
     }
 
     /*
      * Zooming
      */
     @Override
     public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
         if (mState == PanZoomState.ANIMATED_ZOOM)
@@ -1429,9 +1456,14 @@ class JavaPanZoomController
     public int getOverScrollMode() {
         return mX.getOverScrollMode();
     }
 
     @Override
     public void updateScrollOffset(float cssX, float cssY) {
         // Nothing to update, this class doesn't store the scroll offset locally.
     }
+
+    @Override
+    public void setOverscrollHandler(final Overscroll handler) {
+        mOverscroll = handler;
+    }
 }
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -13,16 +13,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.TouchEventInterceptor;
 import org.mozilla.gecko.ZoomConstraints;
 import org.mozilla.gecko.mozglue.GeneratableAndroidBridgeTarget;
 import org.mozilla.gecko.util.EventDispatcher;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.os.Build;
 import android.os.Handler;
 import android.util.AttributeSet;
@@ -62,16 +63,17 @@ public class LayerView extends FrameLayo
 
     private SurfaceView mSurfaceView;
     private TextureView mTextureView;
 
     private Listener mListener;
 
     /* This should only be modified on the Java UI thread. */
     private final ArrayList<TouchEventInterceptor> mTouchInterceptors;
+    private final Overscroll mOverscroll;
 
     /* Flags used to determine when to show the painted surface. */
     public static final int PAINT_START = 0;
     public static final int PAINT_BEFORE_FIRST = 1;
     public static final int PAINT_AFTER_FIRST = 2;
 
     public boolean shouldUseTextureView() {
         // Disable TextureView support for now as it causes panning/zooming
@@ -99,20 +101,23 @@ public class LayerView extends FrameLayo
     public LayerView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         mGLController = GLController.getInstance(this);
         mPaintState = PAINT_START;
         mBackgroundColor = Color.WHITE;
 
         mTouchInterceptors = new ArrayList<TouchEventInterceptor>();
+        mOverscroll = new Overscroll(this);
     }
 
     public void initializeView(EventDispatcher eventDispatcher) {
         mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
+        mLayerClient.setOverscrollHandler(mOverscroll);
+
         mPanZoomController = mLayerClient.getPanZoomController();
         mMarginsAnimator = mLayerClient.getLayerMarginsAnimator();
 
         mRenderer = new LayerRenderer(this);
         mInputConnectionHandler = null;
 
         setFocusable(true);
         setFocusableInTouchMode(true);
@@ -214,16 +219,26 @@ public class LayerView extends FrameLayo
                 result |= i.onInterceptTouchEvent(this, event);
             }
         }
 
         return result;
     }
 
     @Override
+    public void dispatchDraw(final Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        // We must have a layer client to get valid viewport metrics
+        if (mLayerClient != null) {
+            mOverscroll.draw(canvas, getViewportMetrics());
+        }
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             requestFocus();
         }
 
         if (runTouchInterceptors(event, false)) {
             return true;
         }
@@ -482,24 +497,28 @@ public class LayerView extends FrameLayo
         if (!mGLController.hasValidSurface() || mSurfaceView == null) {
             surfaceChanged(width, height);
             return;
         }
 
         if (mListener != null) {
             mListener.sizeChanged(width, height);
         }
+
+        mOverscroll.setSize(width, height);
     }
 
     private void surfaceChanged(int width, int height) {
         mGLController.surfaceChanged(width, height);
 
         if (mListener != null) {
             mListener.surfaceChanged(width, height);
         }
+
+        mOverscroll.setSize(width, height);
     }
 
     private void onDestroyed() {
         mGLController.surfaceDestroyed();
     }
 
     public Object getNativeWindow() {
         if (mSurfaceView != null)
--- a/mobile/android/base/gfx/NativePanZoomController.java
+++ b/mobile/android/base/gfx/NativePanZoomController.java
@@ -99,9 +99,12 @@ class NativePanZoomController implements
             long nextDelay = runDelayedCallback();
             if (nextDelay >= 0) {
                 mTarget.postDelayed(this, nextDelay);
             }
         }
     }
 
     public native void updateScrollOffset(float cssX, float cssY);
+
+    public void setOverscrollHandler(final Overscroll listener) {
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/gfx/Overscroll.java
@@ -0,0 +1,127 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.support.v4.view.ViewCompat;
+import android.view.View;
+
+public class Overscroll {
+    // Used to index particular edges in the edges array
+    private static final int TOP = 0;
+    private static final int BOTTOM = 1;
+    private static final int LEFT = 2;
+    private static final int RIGHT = 3;
+
+    // All four edges of the screen
+    private final EdgeEffectCompat[] mEdges = new EdgeEffectCompat[4];
+
+    // The view we're showing this overscroll on.
+    private final View mView;
+
+    // The axis to show overscroll on.
+    public enum Axis {
+        X,
+        Y,
+    };
+
+    public Overscroll(final View v) {
+        mView = v;
+        Context context = v.getContext();
+        for (int i = 0; i < 4; i++) {
+            mEdges[i] = new EdgeEffectCompat(context);
+        }
+    }
+
+    public void setSize(final int width, final int height) {
+        mEdges[LEFT].setSize(height, width);
+        mEdges[RIGHT].setSize(height, width);
+        mEdges[TOP].setSize(width, height);
+        mEdges[BOTTOM].setSize(width, height);
+    }
+
+    private EdgeEffectCompat getEdgeForAxisAndSide(final Axis axis, final float side) {
+        if (axis == Axis.Y) {
+            if (side < 0) {
+                return mEdges[TOP];
+            } else {
+                return mEdges[BOTTOM];
+            }
+        } else {
+            if (side < 0) {
+                return mEdges[LEFT];
+            } else {
+                return mEdges[RIGHT];
+            }
+        }
+    }
+
+    public void setVelocity(final float velocity, final Axis axis) {
+        final EdgeEffectCompat edge = getEdgeForAxisAndSide(axis, velocity);
+
+        // If we're showing overscroll already, start fading it out.
+        if (!edge.isFinished()) {
+            edge.onRelease();
+        } else {
+            // Otherwise, show an absorb effect
+            edge.onAbsorb((int)velocity);
+        }
+
+        ViewCompat.postInvalidateOnAnimation(mView);
+    }
+
+    public void setDistance(final float distance, final Axis axis) {
+        // The first overscroll event often has zero distance. Throw it out
+        if (distance == 0.0f) {
+            return;
+        }
+
+        final EdgeEffectCompat edge = getEdgeForAxisAndSide(axis, (int)distance);
+        edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
+        ViewCompat.postInvalidateOnAnimation(mView);
+    }
+
+    public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
+        if (metrics == null) {
+            return;
+        }
+
+        // If we're pulling an edge, or fading it out, draw!
+        boolean invalidate = false;
+        if (!mEdges[TOP].isFinished()) {
+            invalidate |= draw(mEdges[TOP], canvas, metrics.marginLeft, metrics.marginTop, 0);
+        }
+
+        if (!mEdges[BOTTOM].isFinished()) {
+            invalidate |= draw(mEdges[BOTTOM], canvas, mView.getWidth(), mView.getHeight(), 180);
+        }
+
+        if (!mEdges[LEFT].isFinished()) {
+            invalidate |= draw(mEdges[LEFT], canvas, metrics.marginLeft, mView.getHeight(), 270);
+        }
+
+        if (!mEdges[RIGHT].isFinished()) {
+            invalidate |= draw(mEdges[RIGHT], canvas, mView.getWidth(), metrics.marginTop, 90);
+        }
+
+        // If the edge effect is animating off screen, invalidate.
+        if (invalidate) {
+            ViewCompat.postInvalidateOnAnimation(mView);
+        }
+    }
+
+    public boolean draw(final EdgeEffectCompat edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
+        final int state = canvas.save();
+        canvas.translate(translateX, translateY);
+        canvas.rotate(rotation);
+        boolean invalidate = edge.draw(canvas);
+        canvas.restoreToCount(state);
+
+        return invalidate;
+    }
+}
--- a/mobile/android/base/gfx/PanZoomController.java
+++ b/mobile/android/base/gfx/PanZoomController.java
@@ -37,9 +37,11 @@ public interface PanZoomController {
     public void pageRectUpdated();
     public void abortPanning();
     public void abortAnimation();
 
     public void setOverScrollMode(int overscrollMode);
     public int getOverScrollMode();
 
     public void updateScrollOffset(float cssX, float cssY);
+
+    public void setOverscrollHandler(final Overscroll controller);
 }