Bug 918079 - Show highlight when overscrolling. r=kats
authorWes Johnston <wjohnston@mozilla.com>
Thu, 26 Sep 2013 22:57:57 -0700
changeset 149089 9433c19bf735b27e4dbcdea5aed5d08982915033
parent 148919 e4cd2242cc7d85e240c585f68730753348fd1a9e
child 149090 72a469699ec39b75f8557e7862745a83fe8ab6b0
push id34439
push userkwierso@gmail.com
push dateSat, 28 Sep 2013 03:39:12 +0000
treeherdermozilla-inbound@caec8c0c4963 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs918079
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);
 }