[Libreoffice-commits] core.git: Branch 'feature/droid_calcimpress3' - 9 commits - android/experimental

Tomaž Vajngerl tomaz.vajngerl at collabora.com
Mon Dec 1 09:51:17 PST 2014


 android/experimental/LOAndroid3/src/java/org/libreoffice/LOEvent.java                          |   29 
 android/experimental/LOAndroid3/src/java/org/libreoffice/LOEventFactory.java                   |    4 
 android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitShell.java                       |    9 
 android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java                      |   42 
 android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitTileProvider.java                |   32 
 android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java          |   17 
 android/experimental/LOAndroid3/src/java/org/libreoffice/MockTileProvider.java                 |   14 
 android/experimental/LOAndroid3/src/java/org/libreoffice/TileIdentifier.java                   |   35 
 android/experimental/LOAndroid3/src/java/org/libreoffice/TileProviderFactory.java              |    8 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/OnInterceptTouchListener.java       |   14 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Axis.java                       |  337 +++
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java           |   20 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java           |  475 +++-
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java      |  972 ++++++++++
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java            |  265 --
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerRenderer.java              |   18 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java                  |   14 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java          |   34 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java              |   24 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java |  323 +++
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SubTile.java                    |   40 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java    |   78 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java          |   41 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java                        |  337 ---
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java           |  953 ---------
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java               |   24 
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java  |  303 ---
 android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java     |   78 
 28 files changed, 2311 insertions(+), 2229 deletions(-)

New commits:
commit 365c82688f6cb6ff7fa89010d3da4e7188f04cec
Author: Tomaž Vajngerl <tomaz.vajngerl at collabora.com>
Date:   Sun Nov 30 18:07:19 2014 +0100

    android: extract JavaPanZoomController (PZC becomes an interface)
    
    Change-Id: I87e63008fe7c6db62c18bf461dc4dcda733393ac

diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
index 97d0944..d195249 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -11,7 +11,6 @@ import android.graphics.PointF;
 import android.graphics.RectF;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.view.GestureDetector;
 
 import org.libreoffice.LOEvent;
 import org.libreoffice.LOEventFactory;
@@ -102,7 +101,7 @@ public class GeckoLayerClient implements PanZoomTarget, LayerView.Listener {
         mCheckerboardColor = Color.WHITE;
         mCheckerboardShouldShowChecks = true;
 
-        mPanZoomController = new PanZoomController(this);
+        mPanZoomController = PanZoomController.Factory.create(this);
     }
 
     public void setView(LayerView v) {
@@ -465,18 +464,6 @@ public class GeckoLayerClient implements PanZoomTarget, LayerView.Listener {
         return mContext;
     }
 
-    public GestureDetector.OnGestureListener getGestureListener() {
-        return mPanZoomController;
-    }
-
-    public SimpleScaleGestureDetector.SimpleScaleGestureListener getScaleGestureListener() {
-        return mPanZoomController;
-    }
-
-    public GestureDetector.OnDoubleTapListener getDoubleTapListener() {
-        return mPanZoomController;
-    }
-
     private class AdjustRunnable implements Runnable {
         public void run() {
             mPendingViewportAdjust = false;
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
new file mode 100644
index 0000000..af47136
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -0,0 +1,972 @@
+/* -*- 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.graphics.PointF;
+import android.graphics.RectF;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.ZoomConstraints;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+/*
+ * Handles the kinetic scrolling and zooming physics for a layer controller.
+ *
+ * Many ideas are from Joe Hewitt's Scrollability:
+ *   https://github.com/joehewitt/scrollability/
+ */
+public class JavaPanZoomController
+        extends GestureDetector.SimpleOnGestureListener
+        implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener
+{
+    private static final String LOGTAG = "GeckoPanZoomController";
+
+
+    // Animation stops if the velocity is below this value when overscrolled or panning.
+    private static final float STOPPED_THRESHOLD = 4.0f;
+
+    // Animation stops is the velocity is below this threshold when flinging.
+    private static final float FLING_STOPPED_THRESHOLD = 0.1f;
+
+    // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+    // between the touch-down and touch-up of a click). In units of density-independent pixels.
+    public static final float PAN_THRESHOLD = 1/16f * LOKitShell.getDpi();
+
+    // Angle from axis within which we stay axis-locked
+    private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
+
+    // The maximum amount we allow you to zoom into a page
+    private static final float MAX_ZOOM = 4.0f;
+
+    // The maximum amount we would like to scroll with the mouse
+    private static final float MAX_SCROLL = 0.075f * LOKitShell.getDpi();
+
+    private enum PanZoomState {
+        NOTHING,        /* no touch-start events received */
+        FLING,          /* all touches removed, but we're still scrolling page */
+        TOUCHING,       /* one touch-start event received */
+        PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */
+        PANNING,        /* panning without axis lock */
+        PANNING_HOLD,   /* in panning, but not moving.
+                         * similar to TOUCHING but after starting a pan */
+        PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
+        PINCHING,       /* nth touch-start, where n > 1. this mode allows pan and zoom */
+        ANIMATED_ZOOM,  /* animated zoom to a new rect */
+        BOUNCE,         /* in a bounce animation */
+
+        WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
+                        put a finger down, but we don't yet know if a touch listener has
+                        prevented the default actions yet. we still need to abort animations. */
+    }
+
+    private final PanZoomTarget mTarget;
+    private final SubdocumentScrollHelper mSubscroller;
+    private final Axis mX;
+    private final Axis mY;
+
+    private Thread mMainThread;
+
+    /* The timer that handles flings or bounces. */
+    private Timer mAnimationTimer;
+    /* The runnable being scheduled by the animation timer. */
+    private AnimationRunnable mAnimationRunnable;
+    /* The zoom focus at the first zoom event (in page coordinates). */
+    private PointF mLastZoomFocus;
+    /* The time the last motion event took place. */
+    private long mLastEventTime;
+    /* Current state the pan/zoom UI is in. */
+    private PanZoomState mState;
+
+    public JavaPanZoomController(PanZoomTarget target) {
+        mTarget = target;
+        mSubscroller = new SubdocumentScrollHelper();
+        mX = new AxisX(mSubscroller);
+        mY = new AxisY(mSubscroller);
+
+        mMainThread = LibreOfficeMainActivity.mAppContext.getMainLooper().getThread();
+        checkMainThread();
+
+        setState(PanZoomState.NOTHING);
+    }
+
+    public void destroy() {
+        mSubscroller.destroy();
+    }
+
+    private final static float easeOut(float t) {
+        // ease-out approx.
+        // -(t-1)^2+1
+        t = t-1;
+        return -t*t+1;
+    }
+
+    private void setState(PanZoomState state) {
+        if (state != mState) {
+            mState = state;
+        }
+    }
+
+    private ImmutableViewportMetrics getMetrics() {
+        return mTarget.getViewportMetrics();
+    }
+
+    // for debugging bug 713011; it can be taken out once that is resolved.
+    private void checkMainThread() {
+        if (mMainThread != Thread.currentThread()) {
+            // log with full stack trace
+            Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
+        }
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction() & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:   return onTouchStart(event);
+            case MotionEvent.ACTION_MOVE:   return onTouchMove(event);
+            case MotionEvent.ACTION_UP:     return onTouchEnd(event);
+            case MotionEvent.ACTION_CANCEL: return onTouchCancel(event);
+            case MotionEvent.ACTION_SCROLL: return onScroll(event);
+            default:                        return false;
+        }
+    }
+
+    /** This function must be called from the UI thread. */
+    public void abortAnimation() {
+        checkMainThread();
+        // this happens when gecko changes the viewport on us or if the device is rotated.
+        // if that's the case, abort any animation in progress and re-zoom so that the page
+        // snaps to edges. for other cases (where the user's finger(s) are down) don't do
+        // anything special.
+        switch (mState) {
+            case FLING:
+                mX.stopFling();
+                mY.stopFling();
+                // fall through
+            case BOUNCE:
+            case ANIMATED_ZOOM:
+                // the zoom that's in progress likely makes no sense any more (such as if
+                // the screen orientation changed) so abort it
+                setState(PanZoomState.NOTHING);
+                // fall through
+            case NOTHING:
+                // Don't do animations here; they're distracting and can cause flashes on page
+                // transitions.
+                synchronized (mTarget.getLock()) {
+                    mTarget.setViewportMetrics(getValidViewportMetrics());
+                    mTarget.forceRedraw();
+                }
+                break;
+        }
+    }
+
+    /** This function must be called on the UI thread. */
+    public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
+        checkMainThread();
+        mSubscroller.cancel();
+        if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+            // this is the first touch point going down, so we enter the pending state
+            // seting the state will kill any animations in progress, possibly leaving
+            // the page in overscroll
+            setState(PanZoomState.WAITING_LISTENERS);
+        }
+    }
+
+    /** This function must be called on the UI thread. */
+    public void preventedTouchFinished() {
+        checkMainThread();
+        if (mState == PanZoomState.WAITING_LISTENERS) {
+            // if we enter here, we just finished a block of events whose default actions
+            // were prevented by touch listeners. Now there are no touch points left, so
+            // we need to reset our state and re-bounce because we might be in overscroll
+            bounce();
+        }
+    }
+
+    /** This must be called on the UI thread. */
+    public void pageRectUpdated() {
+        if (mState == PanZoomState.NOTHING) {
+            synchronized (mTarget.getLock()) {
+                ImmutableViewportMetrics validated = getValidViewportMetrics();
+                if (!getMetrics().fuzzyEquals(validated)) {
+                    // page size changed such that we are now in overscroll. snap to the
+                    // the nearest valid viewport
+                    mTarget.setViewportMetrics(validated);
+                }
+            }
+        }
+    }
+
+    /*
+     * Panning/scrolling
+     */
+
+    private boolean onTouchStart(MotionEvent event) {
+        // user is taking control of movement, so stop
+        // any auto-movement we have going
+        stopAnimationTimer();
+
+        switch (mState) {
+            case ANIMATED_ZOOM:
+                // We just interrupted a double-tap animation, so force a redraw in
+                // case this touchstart is just a tap that doesn't end up triggering
+                // a redraw
+                mTarget.forceRedraw();
+                // fall through
+            case FLING:
+            case BOUNCE:
+            case NOTHING:
+            case WAITING_LISTENERS:
+                startTouch(event.getX(0), event.getY(0), event.getEventTime());
+                return false;
+            case TOUCHING:
+            case PANNING:
+            case PANNING_LOCKED:
+            case PANNING_HOLD:
+            case PANNING_HOLD_LOCKED:
+            case PINCHING:
+                Log.e(LOGTAG, "Received impossible touch down while in " + mState);
+                return false;
+        }
+        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchStart");
+        return false;
+    }
+
+    private boolean onTouchMove(MotionEvent event) {
+
+        switch (mState) {
+            case FLING:
+            case BOUNCE:
+            case WAITING_LISTENERS:
+                // should never happen
+                Log.e(LOGTAG, "Received impossible touch move while in " + mState);
+                // fall through
+            case ANIMATED_ZOOM:
+            case NOTHING:
+                // may happen if user double-taps and drags without lifting after the
+                // second tap. ignore the move if this happens.
+                return false;
+
+            case TOUCHING:
+                if (panDistance(event) < PAN_THRESHOLD) {
+                    return false;
+                }
+                cancelTouch();
+                startPanning(event.getX(0), event.getY(0), event.getEventTime());
+                track(event);
+                return true;
+
+            case PANNING_HOLD_LOCKED:
+                setState(PanZoomState.PANNING_LOCKED);
+                // fall through
+            case PANNING_LOCKED:
+                track(event);
+                return true;
+
+            case PANNING_HOLD:
+                setState(PanZoomState.PANNING);
+                // fall through
+            case PANNING:
+                track(event);
+                return true;
+
+            case PINCHING:
+                // scale gesture listener will handle this
+                return false;
+        }
+        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchMove");
+        return false;
+    }
+
+    private boolean onTouchEnd(MotionEvent event) {
+
+        switch (mState) {
+            case FLING:
+            case BOUNCE:
+            case WAITING_LISTENERS:
+                // should never happen
+                Log.e(LOGTAG, "Received impossible touch end while in " + mState);
+                // fall through
+            case ANIMATED_ZOOM:
+            case NOTHING:
+                // may happen if user double-taps and drags without lifting after the
+                // second tap. ignore if this happens.
+                return false;
+
+            case TOUCHING:
+                // the switch into TOUCHING might have happened while the page was
+                // snapping back after overscroll. we need to finish the snap if that
+                // was the case
+                bounce();
+                return false;
+
+            case PANNING:
+            case PANNING_LOCKED:
+            case PANNING_HOLD:
+            case PANNING_HOLD_LOCKED:
+                setState(PanZoomState.FLING);
+                fling();
+                return true;
+
+            case PINCHING:
+                setState(PanZoomState.NOTHING);
+                return true;
+        }
+        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd");
+        return false;
+    }
+
+    private boolean onTouchCancel(MotionEvent event) {
+        cancelTouch();
+
+        if (mState == PanZoomState.WAITING_LISTENERS) {
+            // we might get a cancel event from the TouchEventHandler while in the
+            // WAITING_LISTENERS state if the touch listeners prevent-default the
+            // block of events. at this point being in WAITING_LISTENERS is equivalent
+            // to being in NOTHING with the exception of possibly being in overscroll.
+            // so here we don't want to do anything right now; the overscroll will be
+            // corrected in preventedTouchFinished().
+            return false;
+        }
+
+        // ensure we snap back if we're overscrolled
+        bounce();
+        return false;
+    }
+
+    private boolean onScroll(MotionEvent event) {
+        if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
+            float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+            float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+
+            scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
+            bounce();
+            return true;
+        }
+        return false;
+    }
+
+    private void startTouch(float x, float y, long time) {
+        mX.startTouch(x);
+        mY.startTouch(y);
+        setState(PanZoomState.TOUCHING);
+        mLastEventTime = time;
+    }
+
+    private void startPanning(float x, float y, long time) {
+        float dx = mX.panDistance(x);
+        float dy = mY.panDistance(y);
+        double angle = Math.atan2(dy, dx); // range [-pi, pi]
+        angle = Math.abs(angle); // range [0, pi]
+
+        // When the touch move breaks through the pan threshold, reposition the touch down origin
+        // so the page won't jump when we start panning.
+        mX.startTouch(x);
+        mY.startTouch(y);
+        mLastEventTime = time;
+
+        if (!mX.scrollable() || !mY.scrollable()) {
+            setState(PanZoomState.PANNING);
+        } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
+            mY.setScrollingDisabled(true);
+            setState(PanZoomState.PANNING_LOCKED);
+        } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
+            mX.setScrollingDisabled(true);
+            setState(PanZoomState.PANNING_LOCKED);
+        } else {
+            setState(PanZoomState.PANNING);
+        }
+    }
+
+    private float panDistance(MotionEvent move) {
+        float dx = mX.panDistance(move.getX(0));
+        float dy = mY.panDistance(move.getY(0));
+        return FloatMath.sqrt(dx * dx + dy * dy);
+    }
+
+    private void track(float x, float y, long time) {
+        float timeDelta = (float)(time - mLastEventTime);
+        if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
+            // probably a duplicate event, ignore it. using a zero timeDelta will mess
+            // up our velocity
+            return;
+        }
+        mLastEventTime = time;
+
+        mX.updateWithTouchAt(x, timeDelta);
+        mY.updateWithTouchAt(y, timeDelta);
+    }
+
+    private void track(MotionEvent event) {
+        mX.saveTouchPos();
+        mY.saveTouchPos();
+
+        for (int i = 0; i < event.getHistorySize(); i++) {
+            track(event.getHistoricalX(0, i),
+                    event.getHistoricalY(0, i),
+                    event.getHistoricalEventTime(i));
+        }
+        track(event.getX(0), event.getY(0), event.getEventTime());
+
+        if (stopped()) {
+            if (mState == PanZoomState.PANNING) {
+                setState(PanZoomState.PANNING_HOLD);
+            } else if (mState == PanZoomState.PANNING_LOCKED) {
+                setState(PanZoomState.PANNING_HOLD_LOCKED);
+            } else {
+                // should never happen, but handle anyway for robustness
+                Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
+                setState(PanZoomState.PANNING_HOLD_LOCKED);
+            }
+        }
+
+        mX.startPan();
+        mY.startPan();
+        updatePosition();
+    }
+
+    private void scrollBy(float dx, float dy) {
+        ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy);
+        mTarget.setViewportMetrics(scrolled);
+    }
+
+    private void fling() {
+        updatePosition();
+
+        stopAnimationTimer();
+
+        boolean stopped = stopped();
+        mX.startFling(stopped);
+        mY.startFling(stopped);
+
+        startAnimationTimer(new FlingRunnable());
+    }
+
+    /* Performs a bounce-back animation to the given viewport metrics. */
+    private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
+        stopAnimationTimer();
+
+        ImmutableViewportMetrics bounceStartMetrics = getMetrics();
+        if (bounceStartMetrics.fuzzyEquals(metrics)) {
+            setState(PanZoomState.NOTHING);
+            return;
+        }
+
+        setState(state);
+
+        // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
+        // getRedrawHint() is returning false. This means we can safely call
+        // setAnimationTarget to set the new final display port and not have it get
+        // clobbered by display ports from intermediate animation frames.
+        mTarget.setAnimationTarget(metrics);
+        startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
+    }
+
+    /* Performs a bounce-back animation to the nearest valid viewport metrics. */
+    private void bounce() {
+        bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
+    }
+
+    /* Starts the fling or bounce animation. */
+    private void startAnimationTimer(final AnimationRunnable runnable) {
+        if (mAnimationTimer != null) {
+            Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!");
+            stopAnimationTimer();
+        }
+
+        mAnimationTimer = new Timer("Animation Timer");
+        mAnimationRunnable = runnable;
+        mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() { mTarget.post(runnable); }
+        }, 0, (int)Axis.MS_PER_FRAME);
+    }
+
+    /* Stops the fling or bounce animation. */
+    private void stopAnimationTimer() {
+        if (mAnimationTimer != null) {
+            mAnimationTimer.cancel();
+            mAnimationTimer = null;
+        }
+        if (mAnimationRunnable != null) {
+            mAnimationRunnable.terminate();
+            mAnimationRunnable = null;
+        }
+    }
+
+    private float getVelocity() {
+        float xvel = mX.getRealVelocity();
+        float yvel = mY.getRealVelocity();
+        return FloatMath.sqrt(xvel * xvel + yvel * yvel);
+    }
+
+    public PointF getVelocityVector() {
+        return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
+    }
+
+    private boolean stopped() {
+        return getVelocity() < STOPPED_THRESHOLD;
+    }
+
+    PointF resetDisplacement() {
+        return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
+    }
+
+    private void updatePosition() {
+        mX.displace();
+        mY.displace();
+        PointF displacement = resetDisplacement();
+        if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
+            return;
+        }
+        if (! mSubscroller.scrollBy(displacement)) {
+            synchronized (mTarget.getLock()) {
+                scrollBy(displacement.x, displacement.y);
+            }
+        }
+    }
+
+    private abstract class AnimationRunnable implements Runnable {
+        private boolean mAnimationTerminated;
+
+        /* This should always run on the UI thread */
+        public final void run() {
+            /*
+             * Since the animation timer queues this runnable on the UI thread, it
+             * is possible that even when the animation timer is cancelled, there
+             * are multiple instances of this queued, so we need to have another
+             * mechanism to abort. This is done by using the mAnimationTerminated flag.
+             */
+            if (mAnimationTerminated) {
+                return;
+            }
+            animateFrame();
+        }
+
+        protected abstract void animateFrame();
+
+        /* This should always run on the UI thread */
+        protected final void terminate() {
+            mAnimationTerminated = true;
+        }
+    }
+
+    /* The callback that performs the bounce animation. */
+    private class BounceRunnable extends AnimationRunnable {
+        /* The current frame of the bounce-back animation */
+        private int mBounceFrame;
+        /*
+         * The viewport metrics that represent the start and end of the bounce-back animation,
+         * respectively.
+         */
+        private ImmutableViewportMetrics mBounceStartMetrics;
+        private ImmutableViewportMetrics mBounceEndMetrics;
+
+        BounceRunnable(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) {
+            mBounceStartMetrics = startMetrics;
+            mBounceEndMetrics = endMetrics;
+        }
+
+        protected void animateFrame() {
+            /*
+             * The pan/zoom controller might have signaled to us that it wants to abort the
+             * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+             * out.
+             */
+            if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
+                finishAnimation();
+                return;
+            }
+
+            /* Perform the next frame of the bounce-back animation. */
+            if (mBounceFrame < (int)(256f/Axis.MS_PER_FRAME)) {
+                advanceBounce();
+                return;
+            }
+
+            /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
+            finishBounce();
+            finishAnimation();
+            setState(PanZoomState.NOTHING);
+        }
+
+        /* Performs one frame of a bounce animation. */
+        private void advanceBounce() {
+            synchronized (mTarget.getLock()) {
+                float t = easeOut(mBounceFrame * Axis.MS_PER_FRAME / 256f);
+                ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
+                mTarget.setViewportMetrics(newMetrics);
+                mBounceFrame++;
+            }
+        }
+
+        /* Concludes a bounce animation and snaps the viewport into place. */
+        private void finishBounce() {
+            synchronized (mTarget.getLock()) {
+                mTarget.setViewportMetrics(mBounceEndMetrics);
+                mBounceFrame = -1;
+            }
+        }
+    }
+
+    // The callback that performs the fling animation.
+    private class FlingRunnable extends AnimationRunnable {
+        protected void animateFrame() {
+            /*
+             * The pan/zoom controller might have signaled to us that it wants to abort the
+             * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+             * out.
+             */
+            if (mState != PanZoomState.FLING) {
+                finishAnimation();
+                return;
+            }
+
+            /* Advance flings, if necessary. */
+            boolean flingingX = mX.advanceFling();
+            boolean flingingY = mY.advanceFling();
+
+            boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
+
+            /* If we're still flinging in any direction, update the origin. */
+            if (flingingX || flingingY) {
+                updatePosition();
+
+                /*
+                 * Check to see if we're still flinging with an appreciable velocity. The threshold is
+                 * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
+                 * coast smoothly to a stop when not. In other words, require a greater velocity to
+                 * maintain the fling once we enter overscroll.
+                 */
+                float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
+                if (getVelocity() >= threshold) {
+                    // we're still flinging
+                    return;
+                }
+
+                mX.stopFling();
+                mY.stopFling();
+            }
+
+            /* Perform a bounce-back animation if overscrolled. */
+            if (overscrolled) {
+                bounce();
+            } else {
+                finishAnimation();
+                setState(PanZoomState.NOTHING);
+            }
+        }
+    }
+
+    private void finishAnimation() {
+        checkMainThread();
+
+        stopAnimationTimer();
+
+        // Force a viewport synchronisation
+        mTarget.forceRedraw();
+    }
+
+    /* Returns the nearest viewport metrics with no overscroll visible. */
+    private ImmutableViewportMetrics getValidViewportMetrics() {
+        return getValidViewportMetrics(getMetrics());
+    }
+
+    private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
+        /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
+        float zoomFactor = viewportMetrics.zoomFactor;
+        RectF pageRect = viewportMetrics.getPageRect();
+        RectF viewport = viewportMetrics.getViewport();
+
+        float focusX = viewport.width() / 2.0f;
+        float focusY = viewport.height() / 2.0f;
+
+        float minZoomFactor = 0.0f;
+        float maxZoomFactor = MAX_ZOOM;
+
+        ZoomConstraints constraints = mTarget.getZoomConstraints();
+
+        if (constraints.getMinZoom() > 0)
+            minZoomFactor = constraints.getMinZoom();
+        if (constraints.getMaxZoom() > 0)
+            maxZoomFactor = constraints.getMaxZoom();
+
+        if (!constraints.getAllowZoom()) {
+            // If allowZoom is false, clamp to the default zoom level.
+            maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
+        }
+
+        // Ensure minZoomFactor keeps the page at least as big as the viewport.
+        if (pageRect.width() > 0) {
+            float scaleFactor = viewport.width() / pageRect.width();
+            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
+            if (viewport.width() > pageRect.width())
+                focusX = 0.0f;
+        }
+        /*if (pageRect.height() > 0) {
+            float scaleFactor = viewport.height() / pageRect.height();
+            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
+            if (viewport.height() > pageRect.height())
+                focusY = 0.0f;
+        }*/
+
+        maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
+
+        if (zoomFactor < minZoomFactor) {
+            // if one (or both) of the page dimensions is smaller than the viewport,
+            // zoom using the top/left as the focus on that axis. this prevents the
+            // scenario where, if both dimensions are smaller than the viewport, but
+            // by different scale factors, we end up scrolled to the end on one axis
+            // after applying the scale
+            PointF center = new PointF(focusX, focusY);
+            viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center);
+        } else if (zoomFactor > maxZoomFactor) {
+            PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
+            viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
+        }
+
+        /* Now we pan to the right origin. */
+        viewportMetrics = viewportMetrics.clamp();
+
+        return viewportMetrics;
+    }
+
+    private class AxisX extends Axis {
+        AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
+        @Override
+        public float getOrigin() { return getMetrics().viewportRectLeft; }
+        @Override
+        protected float getViewportLength() { return getMetrics().getWidth(); }
+        @Override
+        protected float getPageStart() { return getMetrics().pageRectLeft; }
+        @Override
+        protected float getPageLength() { return getMetrics().getPageWidth(); }
+    }
+
+    private class AxisY extends Axis {
+        AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
+        @Override
+        public float getOrigin() { return getMetrics().viewportRectTop; }
+        @Override
+        protected float getViewportLength() { return getMetrics().getHeight(); }
+        @Override
+        protected float getPageStart() { return getMetrics().pageRectTop; }
+        @Override
+        protected float getPageLength() { return getMetrics().getPageHeight(); }
+    }
+
+    /*
+     * Zooming
+     */
+    @Override
+    public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
+        if (mState == PanZoomState.ANIMATED_ZOOM)
+            return false;
+
+        if (!mTarget.getZoomConstraints().getAllowZoom())
+            return false;
+
+        setState(PanZoomState.PINCHING);
+        mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
+        cancelTouch();
+
+        return true;
+    }
+
+    @Override
+    public boolean onScale(SimpleScaleGestureDetector detector) {
+        if (mState != PanZoomState.PINCHING)
+            return false;
+
+        float prevSpan = detector.getPreviousSpan();
+        if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
+            // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
+            return true;
+        }
+
+        float spanRatio = detector.getCurrentSpan() / prevSpan;
+
+        /*
+         * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
+         * factor toward 1.0.
+         */
+        float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true));
+        if (spanRatio > 1.0f)
+            spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
+        else
+            spanRatio = 1.0f - (1.0f - spanRatio) * resistance;
+
+        synchronized (mTarget.getLock()) {
+            float newZoomFactor = getMetrics().zoomFactor * spanRatio;
+            float minZoomFactor = 0.0f;
+            float maxZoomFactor = MAX_ZOOM;
+
+            ZoomConstraints constraints = mTarget.getZoomConstraints();
+
+            if (constraints.getMinZoom() > 0)
+                minZoomFactor = constraints.getMinZoom();
+            if (constraints.getMaxZoom() > 0)
+                maxZoomFactor = constraints.getMaxZoom();
+
+            if (newZoomFactor < minZoomFactor) {
+                // apply resistance when zooming past minZoomFactor,
+                // such that it asymptotically reaches minZoomFactor / 2.0
+                // but never exceeds that
+                final float rate = 0.5f; // controls how quickly we approach the limit
+                float excessZoom = minZoomFactor - newZoomFactor;
+                excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
+                newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
+            }
+
+            if (newZoomFactor > maxZoomFactor) {
+                // apply resistance when zooming past maxZoomFactor,
+                // such that it asymptotically reaches maxZoomFactor + 1.0
+                // but never exceeds that
+                float excessZoom = newZoomFactor - maxZoomFactor;
+                excessZoom = 1.0f - (float)Math.exp(-excessZoom);
+                newZoomFactor = maxZoomFactor + excessZoom;
+            }
+
+            scrollBy(mLastZoomFocus.x - detector.getFocusX(),
+                    mLastZoomFocus.y - detector.getFocusY());
+            PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
+            scaleWithFocus(newZoomFactor, focus);
+        }
+
+        mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
+
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(SimpleScaleGestureDetector detector) {
+        if (mState == PanZoomState.ANIMATED_ZOOM)
+            return;
+
+        // switch back to the touching state
+        startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
+
+        // Force a viewport synchronisation
+        mTarget.forceRedraw();
+
+    }
+
+    /**
+     * Scales the viewport, keeping the given focus point in the same place before and after the
+     * scale operation. You must hold the monitor while calling this.
+     */
+    private void scaleWithFocus(float zoomFactor, PointF focus) {
+        ImmutableViewportMetrics viewportMetrics = getMetrics();
+        viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus);
+        mTarget.setViewportMetrics(viewportMetrics);
+    }
+
+    public boolean getRedrawHint() {
+        switch (mState) {
+            case PINCHING:
+            case ANIMATED_ZOOM:
+            case BOUNCE:
+                // don't redraw during these because the zoom is (or might be, in the case
+                // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
+                // display port area. we trigger a force-redraw upon exiting these states.
+                return false;
+            default:
+                // allow redrawing in other states
+                return true;
+        }
+    }
+
+    @Override
+    public void onLongPress(MotionEvent motionEvent) {
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent motionEvent) {
+        // When zooming is enabled, wait to see if there's a double-tap.
+        return false;
+    }
+
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
+        // When zooming is disabled, we handle this in onSingleTapUp.
+        return true;
+    }
+
+    @Override
+    public boolean onDoubleTap(MotionEvent motionEvent) {
+        return true;
+    }
+
+    private void cancelTouch() {
+    }
+
+    /**
+     * Zoom to a specified rect IN CSS PIXELS.
+     *
+     * While we usually use device pixels, @zoomToRect must be specified in CSS
+     * pixels.
+     */
+    private boolean animatedZoomTo(RectF zoomToRect) {
+        final float startZoom = getMetrics().zoomFactor;
+
+        RectF viewport = getMetrics().getViewport();
+        // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
+        // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
+        // while enlarging make sure we enlarge equally on both sides to keep the target rect
+        // centered.
+        float targetRatio = viewport.width() / viewport.height();
+        float rectRatio = zoomToRect.width() / zoomToRect.height();
+        if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
+            // all good, do nothing
+        } else if (targetRatio < rectRatio) {
+            // need to increase zoomToRect height
+            float newHeight = zoomToRect.width() / targetRatio;
+            zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
+            zoomToRect.bottom = zoomToRect.top + newHeight;
+        } else { // targetRatio > rectRatio) {
+            // need to increase zoomToRect width
+            float newWidth = targetRatio * zoomToRect.height();
+            zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
+            zoomToRect.right = zoomToRect.left + newWidth;
+        }
+
+        float finalZoom = viewport.width() / zoomToRect.width();
+
+        ImmutableViewportMetrics finalMetrics = getMetrics();
+        finalMetrics = finalMetrics.setViewportOrigin(
+                zoomToRect.left * finalMetrics.zoomFactor,
+                zoomToRect.top * finalMetrics.zoomFactor);
+        finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
+
+        // 2. now run getValidViewportMetrics on it, so that the target viewport is
+        // clamped down to prevent overscroll, over-zoom, and other bad conditions.
+        finalMetrics = getValidViewportMetrics(finalMetrics);
+
+        bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
+        return true;
+    }
+
+    /** This function must be called from the UI thread. */
+    public void abortPanning() {
+        checkMainThread();
+        bounce();
+    }
+
+    public void setOverScrollMode(int overscrollMode) {
+        mX.setOverScrollMode(overscrollMode);
+        mY.setOverScrollMode(overscrollMode);
+    }
+
+    public int getOverScrollMode() {
+        return mX.getOverScrollMode();
+    }
+}
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
index 33471e0..86036bb 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -6,968 +6,29 @@
 package org.mozilla.gecko.gfx;
 
 import android.graphics.PointF;
-import android.graphics.RectF;
-import android.util.FloatMath;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
 
 import org.libreoffice.LOKitShell;
-import org.libreoffice.LibreOfficeMainActivity;
-import org.mozilla.gecko.ZoomConstraints;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.util.FloatUtils;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-/*
- * Handles the kinetic scrolling and zooming physics for a layer controller.
- *
- * Many ideas are from Joe Hewitt's Scrollability:
- *   https://github.com/joehewitt/scrollability/
- */
-public class PanZoomController
-    extends GestureDetector.SimpleOnGestureListener
-    implements SimpleScaleGestureDetector.SimpleScaleGestureListener
-{
-    private static final String LOGTAG = "GeckoPanZoomController";
-
-
-    // Animation stops if the velocity is below this value when overscrolled or panning.
-    private static final float STOPPED_THRESHOLD = 4.0f;
-
-    // Animation stops is the velocity is below this threshold when flinging.
-    private static final float FLING_STOPPED_THRESHOLD = 0.1f;
 
+public interface PanZoomController {
     // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
     // between the touch-down and touch-up of a click). In units of density-independent pixels.
     public static final float PAN_THRESHOLD = 1/16f * LOKitShell.getDpi();
 
-    // Angle from axis within which we stay axis-locked
-    private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
-
-    // The maximum amount we allow you to zoom into a page
-    private static final float MAX_ZOOM = 4.0f;
-
-    // The maximum amount we would like to scroll with the mouse
-    private static final float MAX_SCROLL = 0.075f * LOKitShell.getDpi();
-
-    private enum PanZoomState {
-        NOTHING,        /* no touch-start events received */
-        FLING,          /* all touches removed, but we're still scrolling page */
-        TOUCHING,       /* one touch-start event received */
-        PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */
-        PANNING,        /* panning without axis lock */
-        PANNING_HOLD,   /* in panning, but not moving.
-                         * similar to TOUCHING but after starting a pan */
-        PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
-        PINCHING,       /* nth touch-start, where n > 1. this mode allows pan and zoom */
-        ANIMATED_ZOOM,  /* animated zoom to a new rect */
-        BOUNCE,         /* in a bounce animation */
-
-        WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
-                        put a finger down, but we don't yet know if a touch listener has
-                        prevented the default actions yet. we still need to abort animations. */
-    }
-
-    private final PanZoomTarget mTarget;
-    private final SubdocumentScrollHelper mSubscroller;
-    private final Axis mX;
-    private final Axis mY;
-
-    private Thread mMainThread;
-
-    /* The timer that handles flings or bounces. */
-    private Timer mAnimationTimer;
-    /* The runnable being scheduled by the animation timer. */
-    private AnimationRunnable mAnimationRunnable;
-    /* The zoom focus at the first zoom event (in page coordinates). */
-    private PointF mLastZoomFocus;
-    /* The time the last motion event took place. */
-    private long mLastEventTime;
-    /* Current state the pan/zoom UI is in. */
-    private PanZoomState mState;
-
-    public PanZoomController(PanZoomTarget target) {
-        mTarget = target;
-        mSubscroller = new SubdocumentScrollHelper();
-        mX = new AxisX(mSubscroller);
-        mY = new AxisY(mSubscroller);
-
-        mMainThread = LibreOfficeMainActivity.mAppContext.getMainLooper().getThread();
-        checkMainThread();
-
-        setState(PanZoomState.NOTHING);
-    }
-
-    public void destroy() {
-        mSubscroller.destroy();
-    }
-
-    private final static float easeOut(float t) {
-        // ease-out approx.
-        // -(t-1)^2+1
-        t = t-1;
-        return -t*t+1;
-    }
-
-    private void setState(PanZoomState state) {
-        if (state != mState) {
-            mState = state;
-        }
-    }
-
-    private ImmutableViewportMetrics getMetrics() {
-        return mTarget.getViewportMetrics();
-    }
-
-    // for debugging bug 713011; it can be taken out once that is resolved.
-    private void checkMainThread() {
-        if (mMainThread != Thread.currentThread()) {
-            // log with full stack trace
-            Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
-        }
-    }
-
-    public boolean onTouchEvent(MotionEvent event) {
-        switch (event.getAction() & MotionEvent.ACTION_MASK) {
-        case MotionEvent.ACTION_DOWN:   return onTouchStart(event);
-        case MotionEvent.ACTION_MOVE:   return onTouchMove(event);
-        case MotionEvent.ACTION_UP:     return onTouchEnd(event);
-        case MotionEvent.ACTION_CANCEL: return onTouchCancel(event);
-        case MotionEvent.ACTION_SCROLL: return onScroll(event);
-        default:                        return false;
-        }
-    }
-
-    /** This function must be called from the UI thread. */
-    public void abortAnimation() {
-        checkMainThread();
-        // this happens when gecko changes the viewport on us or if the device is rotated.
-        // if that's the case, abort any animation in progress and re-zoom so that the page
-        // snaps to edges. for other cases (where the user's finger(s) are down) don't do
-        // anything special.
-        switch (mState) {
-        case FLING:
-            mX.stopFling();
-            mY.stopFling();
-            // fall through
-        case BOUNCE:
-        case ANIMATED_ZOOM:
-            // the zoom that's in progress likely makes no sense any more (such as if
-            // the screen orientation changed) so abort it
-            setState(PanZoomState.NOTHING);
-            // fall through
-        case NOTHING:
-            // Don't do animations here; they're distracting and can cause flashes on page
-            // transitions.
-            synchronized (mTarget.getLock()) {
-                mTarget.setViewportMetrics(getValidViewportMetrics());
-                mTarget.forceRedraw();
-            }
-            break;
+    static class Factory {
+        static PanZoomController create(PanZoomTarget target) {
+            return new JavaPanZoomController(target);
         }
     }
 
-    /** This function must be called on the UI thread. */
-    public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
-        checkMainThread();
-        mSubscroller.cancel();
-        if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
-            // this is the first touch point going down, so we enter the pending state
-            // seting the state will kill any animations in progress, possibly leaving
-            // the page in overscroll
-            setState(PanZoomState.WAITING_LISTENERS);
-        }
-    }
+    public void destroy();
 
-    /** This function must be called on the UI thread. */
-    public void preventedTouchFinished() {
-        checkMainThread();
-        if (mState == PanZoomState.WAITING_LISTENERS) {
-            // if we enter here, we just finished a block of events whose default actions
-            // were prevented by touch listeners. Now there are no touch points left, so
-            // we need to reset our state and re-bounce because we might be in overscroll
-            bounce();
-        }
-    }
+    public boolean getRedrawHint();
+    public PointF getVelocityVector();
 
-    /** This must be called on the UI thread. */
-    public void pageRectUpdated() {
-        if (mState == PanZoomState.NOTHING) {
-            synchronized (mTarget.getLock()) {
-                ImmutableViewportMetrics validated = getValidViewportMetrics();
-                if (!getMetrics().fuzzyEquals(validated)) {
-                    // page size changed such that we are now in overscroll. snap to the
-                    // the nearest valid viewport
-                    mTarget.setViewportMetrics(validated);
-                }
-            }
-        }
-    }
-
-    /*
-     * Panning/scrolling
-     */
-
-    private boolean onTouchStart(MotionEvent event) {
-        // user is taking control of movement, so stop
-        // any auto-movement we have going
-        stopAnimationTimer();
-
-        switch (mState) {
-        case ANIMATED_ZOOM:
-            // We just interrupted a double-tap animation, so force a redraw in
-            // case this touchstart is just a tap that doesn't end up triggering
-            // a redraw
-            mTarget.forceRedraw();
-            // fall through
-        case FLING:
-        case BOUNCE:
-        case NOTHING:
-        case WAITING_LISTENERS:
-            startTouch(event.getX(0), event.getY(0), event.getEventTime());
-            return false;
-        case TOUCHING:
-        case PANNING:
-        case PANNING_LOCKED:
-        case PANNING_HOLD:
-        case PANNING_HOLD_LOCKED:
-        case PINCHING:
-            Log.e(LOGTAG, "Received impossible touch down while in " + mState);
-            return false;
-        }
-        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchStart");
-        return false;
-    }
-
-    private boolean onTouchMove(MotionEvent event) {
-
-        switch (mState) {
-        case FLING:
-        case BOUNCE:
-        case WAITING_LISTENERS:
-            // should never happen
-            Log.e(LOGTAG, "Received impossible touch move while in " + mState);
-            // fall through
-        case ANIMATED_ZOOM:
-        case NOTHING:
-            // may happen if user double-taps and drags without lifting after the
-            // second tap. ignore the move if this happens.
-            return false;
-
-        case TOUCHING:
-            if (panDistance(event) < PAN_THRESHOLD) {
-                return false;
-            }
-            cancelTouch();
-            startPanning(event.getX(0), event.getY(0), event.getEventTime());
-            track(event);
-            return true;
-
-        case PANNING_HOLD_LOCKED:
-            setState(PanZoomState.PANNING_LOCKED);
-            // fall through
-        case PANNING_LOCKED:
-            track(event);
-            return true;
-
-        case PANNING_HOLD:
-            setState(PanZoomState.PANNING);
-            // fall through
-        case PANNING:
-            track(event);
-            return true;
-
-        case PINCHING:
-            // scale gesture listener will handle this
-            return false;
-        }
-        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchMove");
-        return false;
-    }
-
-    private boolean onTouchEnd(MotionEvent event) {
-
-        switch (mState) {
-        case FLING:
-        case BOUNCE:
-        case WAITING_LISTENERS:
-            // should never happen
-            Log.e(LOGTAG, "Received impossible touch end while in " + mState);
-            // fall through
-        case ANIMATED_ZOOM:
-        case NOTHING:
-            // may happen if user double-taps and drags without lifting after the
-            // second tap. ignore if this happens.
-            return false;
-
-        case TOUCHING:
-            // the switch into TOUCHING might have happened while the page was
-            // snapping back after overscroll. we need to finish the snap if that
-            // was the case
-            bounce();
-            return false;
-
-        case PANNING:
-        case PANNING_LOCKED:
-        case PANNING_HOLD:
-        case PANNING_HOLD_LOCKED:
-            setState(PanZoomState.FLING);
-            fling();
-            return true;
-
-        case PINCHING:
-            setState(PanZoomState.NOTHING);
-            return true;
-        }
-        Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd");
-        return false;
-    }
-
-    private boolean onTouchCancel(MotionEvent event) {
-        cancelTouch();
-
-        if (mState == PanZoomState.WAITING_LISTENERS) {
-            // we might get a cancel event from the TouchEventHandler while in the
-            // WAITING_LISTENERS state if the touch listeners prevent-default the
-            // block of events. at this point being in WAITING_LISTENERS is equivalent
-            // to being in NOTHING with the exception of possibly being in overscroll.
-            // so here we don't want to do anything right now; the overscroll will be
-            // corrected in preventedTouchFinished().
-            return false;
-        }
-
-        // ensure we snap back if we're overscrolled
-        bounce();
-        return false;
-    }
-
-    private boolean onScroll(MotionEvent event) {
-        if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
-            float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
-            float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
-
-            scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
-            bounce();
-            return true;
-        }
-        return false;
-    }
-
-    private void startTouch(float x, float y, long time) {
-        mX.startTouch(x);
-        mY.startTouch(y);
-        setState(PanZoomState.TOUCHING);
-        mLastEventTime = time;
-    }
-
-    private void startPanning(float x, float y, long time) {
-        float dx = mX.panDistance(x);
-        float dy = mY.panDistance(y);
-        double angle = Math.atan2(dy, dx); // range [-pi, pi]
-        angle = Math.abs(angle); // range [0, pi]
-
-        // When the touch move breaks through the pan threshold, reposition the touch down origin
-        // so the page won't jump when we start panning.
-        mX.startTouch(x);
-        mY.startTouch(y);
-        mLastEventTime = time;
-
-        if (!mX.scrollable() || !mY.scrollable()) {
-            setState(PanZoomState.PANNING);
-        } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
-            mY.setScrollingDisabled(true);
-            setState(PanZoomState.PANNING_LOCKED);
-        } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
-            mX.setScrollingDisabled(true);
-            setState(PanZoomState.PANNING_LOCKED);
-        } else {
-            setState(PanZoomState.PANNING);
-        }
-    }
-
-    private float panDistance(MotionEvent move) {
-        float dx = mX.panDistance(move.getX(0));
-        float dy = mY.panDistance(move.getY(0));
-        return FloatMath.sqrt(dx * dx + dy * dy);
-    }
-
-    private void track(float x, float y, long time) {
-        float timeDelta = (float)(time - mLastEventTime);
-        if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
-            // probably a duplicate event, ignore it. using a zero timeDelta will mess
-            // up our velocity
-            return;
-        }
-        mLastEventTime = time;
-
-        mX.updateWithTouchAt(x, timeDelta);
-        mY.updateWithTouchAt(y, timeDelta);
-    }
-
-    private void track(MotionEvent event) {
-        mX.saveTouchPos();
-        mY.saveTouchPos();
-
-        for (int i = 0; i < event.getHistorySize(); i++) {
-            track(event.getHistoricalX(0, i),
-                  event.getHistoricalY(0, i),
-                  event.getHistoricalEventTime(i));
-        }
-        track(event.getX(0), event.getY(0), event.getEventTime());
-
-        if (stopped()) {
-            if (mState == PanZoomState.PANNING) {
-                setState(PanZoomState.PANNING_HOLD);
-            } else if (mState == PanZoomState.PANNING_LOCKED) {
-                setState(PanZoomState.PANNING_HOLD_LOCKED);
-            } else {
-                // should never happen, but handle anyway for robustness
-                Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
-                setState(PanZoomState.PANNING_HOLD_LOCKED);
-            }
-        }
-
-        mX.startPan();
-        mY.startPan();
-        updatePosition();
-    }
-
-    private void scrollBy(float dx, float dy) {
-        ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy);
-        mTarget.setViewportMetrics(scrolled);
-    }
-
-    private void fling() {
-        updatePosition();
-
-        stopAnimationTimer();
-
-        boolean stopped = stopped();
-        mX.startFling(stopped);
-        mY.startFling(stopped);
-
-        startAnimationTimer(new FlingRunnable());
-    }
-
-    /* Performs a bounce-back animation to the given viewport metrics. */
-    private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
-        stopAnimationTimer();
-
-        ImmutableViewportMetrics bounceStartMetrics = getMetrics();
-        if (bounceStartMetrics.fuzzyEquals(metrics)) {
-            setState(PanZoomState.NOTHING);
-            return;
-        }
-
-        setState(state);
-
-        // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
-        // getRedrawHint() is returning false. This means we can safely call
-        // setAnimationTarget to set the new final display port and not have it get
-        // clobbered by display ports from intermediate animation frames.
-        mTarget.setAnimationTarget(metrics);
-        startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
-    }
-
-    /* Performs a bounce-back animation to the nearest valid viewport metrics. */
-    private void bounce() {
-        bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
-    }
-
-    /* Starts the fling or bounce animation. */
-    private void startAnimationTimer(final AnimationRunnable runnable) {
-        if (mAnimationTimer != null) {
-            Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!");
-            stopAnimationTimer();
-        }
-
-        mAnimationTimer = new Timer("Animation Timer");
-        mAnimationRunnable = runnable;
-        mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
-            @Override
-            public void run() { mTarget.post(runnable); }
-        }, 0, (int)Axis.MS_PER_FRAME);
-    }
-
-    /* Stops the fling or bounce animation. */
-    private void stopAnimationTimer() {
-        if (mAnimationTimer != null) {
-            mAnimationTimer.cancel();
-            mAnimationTimer = null;
-        }
-        if (mAnimationRunnable != null) {
-            mAnimationRunnable.terminate();
-            mAnimationRunnable = null;
-        }
-    }
-
-    private float getVelocity() {
-        float xvel = mX.getRealVelocity();
-        float yvel = mY.getRealVelocity();
-        return FloatMath.sqrt(xvel * xvel + yvel * yvel);
-    }
-
-    public PointF getVelocityVector() {
-        return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
-    }
+    public void pageRectUpdated();
+    public void abortPanning();
+    public void abortAnimation();
 
-    private boolean stopped() {
-        return getVelocity() < STOPPED_THRESHOLD;
-    }
-
-    PointF resetDisplacement() {
-        return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
-    }
-
-    private void updatePosition() {
-        mX.displace();
-        mY.displace();
-        PointF displacement = resetDisplacement();
-        if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
-            return;
-        }
-        if (! mSubscroller.scrollBy(displacement)) {
-            synchronized (mTarget.getLock()) {
-                scrollBy(displacement.x, displacement.y);
-            }
-        }
-    }
-
-    private abstract class AnimationRunnable implements Runnable {
-        private boolean mAnimationTerminated;
-
-        /* This should always run on the UI thread */
-        public final void run() {
-            /*
-             * Since the animation timer queues this runnable on the UI thread, it
-             * is possible that even when the animation timer is cancelled, there
-             * are multiple instances of this queued, so we need to have another
-             * mechanism to abort. This is done by using the mAnimationTerminated flag.
-             */
-            if (mAnimationTerminated) {
-                return;
-            }
-            animateFrame();
-        }
-
-        protected abstract void animateFrame();
-
-        /* This should always run on the UI thread */
-        protected final void terminate() {
-            mAnimationTerminated = true;
-        }
-    }
-
-    /* The callback that performs the bounce animation. */
-    private class BounceRunnable extends AnimationRunnable {
-        /* The current frame of the bounce-back animation */
-        private int mBounceFrame;
-        /*
-         * The viewport metrics that represent the start and end of the bounce-back animation,
-         * respectively.
-         */
-        private ImmutableViewportMetrics mBounceStartMetrics;
-        private ImmutableViewportMetrics mBounceEndMetrics;
-
-        BounceRunnable(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) {
-            mBounceStartMetrics = startMetrics;
-            mBounceEndMetrics = endMetrics;
-        }
-
-        protected void animateFrame() {
-            /*
-             * The pan/zoom controller might have signaled to us that it wants to abort the
-             * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
-             * out.
-             */
-            if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
-                finishAnimation();
-                return;
-            }
-
-            /* Perform the next frame of the bounce-back animation. */
-            if (mBounceFrame < (int)(256f/Axis.MS_PER_FRAME)) {
-                advanceBounce();
-                return;
-            }
-
-            /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
-            finishBounce();
-            finishAnimation();
-            setState(PanZoomState.NOTHING);
-        }
-
-        /* Performs one frame of a bounce animation. */
-        private void advanceBounce() {
-            synchronized (mTarget.getLock()) {
-                float t = easeOut(mBounceFrame * Axis.MS_PER_FRAME / 256f);
-                ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
-                mTarget.setViewportMetrics(newMetrics);
-                mBounceFrame++;
-            }
-        }
-
-        /* Concludes a bounce animation and snaps the viewport into place. */
-        private void finishBounce() {
-            synchronized (mTarget.getLock()) {
-                mTarget.setViewportMetrics(mBounceEndMetrics);
-                mBounceFrame = -1;
-            }
-        }
-    }
-
-    // The callback that performs the fling animation.
-    private class FlingRunnable extends AnimationRunnable {
-        protected void animateFrame() {
-            /*
-             * The pan/zoom controller might have signaled to us that it wants to abort the
-             * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
-             * out.
-             */
-            if (mState != PanZoomState.FLING) {
-                finishAnimation();
-                return;
-            }
-
-            /* Advance flings, if necessary. */
-            boolean flingingX = mX.advanceFling();
-            boolean flingingY = mY.advanceFling();
-
-            boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
-
-            /* If we're still flinging in any direction, update the origin. */
-            if (flingingX || flingingY) {
-                updatePosition();
-
-                /*
-                 * Check to see if we're still flinging with an appreciable velocity. The threshold is
-                 * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
-                 * coast smoothly to a stop when not. In other words, require a greater velocity to
-                 * maintain the fling once we enter overscroll.
-                 */
-                float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
-                if (getVelocity() >= threshold) {
-                    // we're still flinging
-                    return;
-                }
-
-                mX.stopFling();
-                mY.stopFling();
-            }
-
-            /* Perform a bounce-back animation if overscrolled. */
-            if (overscrolled) {
-                bounce();
-            } else {
-                finishAnimation();
-                setState(PanZoomState.NOTHING);
-            }
-        }
-    }
-
-    private void finishAnimation() {
-        checkMainThread();
-
-        stopAnimationTimer();
-
-        // Force a viewport synchronisation
-        mTarget.forceRedraw();
-    }
-
-    /* Returns the nearest viewport metrics with no overscroll visible. */
-    private ImmutableViewportMetrics getValidViewportMetrics() {
-        return getValidViewportMetrics(getMetrics());
-    }
-
-    private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
-        /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
-        float zoomFactor = viewportMetrics.zoomFactor;
-        RectF pageRect = viewportMetrics.getPageRect();
-        RectF viewport = viewportMetrics.getViewport();
-
-        float focusX = viewport.width() / 2.0f;
-        float focusY = viewport.height() / 2.0f;
-
-        float minZoomFactor = 0.0f;
-        float maxZoomFactor = MAX_ZOOM;
-
-        ZoomConstraints constraints = mTarget.getZoomConstraints();
-
-        if (constraints.getMinZoom() > 0)
-            minZoomFactor = constraints.getMinZoom();
-        if (constraints.getMaxZoom() > 0)
-            maxZoomFactor = constraints.getMaxZoom();
-
-        if (!constraints.getAllowZoom()) {
-            // If allowZoom is false, clamp to the default zoom level.
-            maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
-        }
-
-        // Ensure minZoomFactor keeps the page at least as big as the viewport.
-        if (pageRect.width() > 0) {
-            float scaleFactor = viewport.width() / pageRect.width();
-            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
-            if (viewport.width() > pageRect.width())
-                focusX = 0.0f;
-        }
-        /*if (pageRect.height() > 0) {
-            float scaleFactor = viewport.height() / pageRect.height();
-            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
-            if (viewport.height() > pageRect.height())
-                focusY = 0.0f;
-        }*/
-
-        maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
-
-        if (zoomFactor < minZoomFactor) {
-            // if one (or both) of the page dimensions is smaller than the viewport,
-            // zoom using the top/left as the focus on that axis. this prevents the
-            // scenario where, if both dimensions are smaller than the viewport, but
-            // by different scale factors, we end up scrolled to the end on one axis
-            // after applying the scale
-            PointF center = new PointF(focusX, focusY);
-            viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center);
-        } else if (zoomFactor > maxZoomFactor) {
-            PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
-            viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
-        }
-
-        /* Now we pan to the right origin. */
-        viewportMetrics = viewportMetrics.clamp();
-
-        return viewportMetrics;
-    }
-
-    private class AxisX extends Axis {
-        AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
-        @Override
-        public float getOrigin() { return getMetrics().viewportRectLeft; }
-        @Override
-        protected float getViewportLength() { return getMetrics().getWidth(); }
-        @Override
-        protected float getPageStart() { return getMetrics().pageRectLeft; }
-        @Override
-        protected float getPageLength() { return getMetrics().getPageWidth(); }
-    }
-
-    private class AxisY extends Axis {
-        AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
-        @Override
-        public float getOrigin() { return getMetrics().viewportRectTop; }
-        @Override
-        protected float getViewportLength() { return getMetrics().getHeight(); }
-        @Override
-        protected float getPageStart() { return getMetrics().pageRectTop; }
-        @Override
-        protected float getPageLength() { return getMetrics().getPageHeight(); }
-    }
-
-    /*
-     * Zooming
-     */
-    @Override
-    public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
-        if (mState == PanZoomState.ANIMATED_ZOOM)
-            return false;
-
-        if (!mTarget.getZoomConstraints().getAllowZoom())
-            return false;
-
-        setState(PanZoomState.PINCHING);
-        mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
-        cancelTouch();
-
-        return true;
-    }
-
-    @Override
-    public boolean onScale(SimpleScaleGestureDetector detector) {
-        if (mState != PanZoomState.PINCHING)
-            return false;
-
-        float prevSpan = detector.getPreviousSpan();
-        if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
-            // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
-            return true;
-        }
-
-        float spanRatio = detector.getCurrentSpan() / prevSpan;
-
-        /*
-         * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
-         * factor toward 1.0.
-         */
-        float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true));
-        if (spanRatio > 1.0f)
-            spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
-        else
-            spanRatio = 1.0f - (1.0f - spanRatio) * resistance;
-
-        synchronized (mTarget.getLock()) {
-            float newZoomFactor = getMetrics().zoomFactor * spanRatio;
-            float minZoomFactor = 0.0f;
-            float maxZoomFactor = MAX_ZOOM;
-
-            ZoomConstraints constraints = mTarget.getZoomConstraints();
-
-            if (constraints.getMinZoom() > 0)
-                minZoomFactor = constraints.getMinZoom();
-            if (constraints.getMaxZoom() > 0)
-                maxZoomFactor = constraints.getMaxZoom();
-
-            if (newZoomFactor < minZoomFactor) {
-                // apply resistance when zooming past minZoomFactor,
-                // such that it asymptotically reaches minZoomFactor / 2.0
-                // but never exceeds that
-                final float rate = 0.5f; // controls how quickly we approach the limit
-                float excessZoom = minZoomFactor - newZoomFactor;
-                excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
-                newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
-            }
-
-            if (newZoomFactor > maxZoomFactor) {
-                // apply resistance when zooming past maxZoomFactor,
-                // such that it asymptotically reaches maxZoomFactor + 1.0
-                // but never exceeds that
-                float excessZoom = newZoomFactor - maxZoomFactor;
-                excessZoom = 1.0f - (float)Math.exp(-excessZoom);
-                newZoomFactor = maxZoomFactor + excessZoom;
-            }
-
-            scrollBy(mLastZoomFocus.x - detector.getFocusX(),
-                     mLastZoomFocus.y - detector.getFocusY());
-            PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
-            scaleWithFocus(newZoomFactor, focus);
-        }
-
-        mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
-
-        return true;
-    }
-
-    @Override
-    public void onScaleEnd(SimpleScaleGestureDetector detector) {
-        if (mState == PanZoomState.ANIMATED_ZOOM)
-            return;
-
-        // switch back to the touching state
-        startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
-
-        // Force a viewport synchronisation
-        mTarget.forceRedraw();
-
-    }
-
-    /**
-     * Scales the viewport, keeping the given focus point in the same place before and after the
-     * scale operation. You must hold the monitor while calling this.
-     */
-    private void scaleWithFocus(float zoomFactor, PointF focus) {
-        ImmutableViewportMetrics viewportMetrics = getMetrics();
-        viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus);
-        mTarget.setViewportMetrics(viewportMetrics);
-    }
-
-    public boolean getRedrawHint() {
-        switch (mState) {
-            case PINCHING:
-            case ANIMATED_ZOOM:
-            case BOUNCE:
-                // don't redraw during these because the zoom is (or might be, in the case
-                // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
-                // display port area. we trigger a force-redraw upon exiting these states.
-                return false;
-            default:
-                // allow redrawing in other states
-                return true;
-        }
-    }
-
-    @Override
-    public void onLongPress(MotionEvent motionEvent) {
-    }
-
-    @Override
-    public boolean onSingleTapUp(MotionEvent motionEvent) {
-        // When zooming is enabled, wait to see if there's a double-tap.
-        return false;
-    }
-
-    @Override
-    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
-        // When zooming is disabled, we handle this in onSingleTapUp.
-        return true;
-    }
-
-    @Override
-    public boolean onDoubleTap(MotionEvent motionEvent) {
-        return true;
-    }
-
-    private void cancelTouch() {
-    }
-
-    /**
-     * Zoom to a specified rect IN CSS PIXELS.
-     *
-     * While we usually use device pixels, @zoomToRect must be specified in CSS
-     * pixels.
-     */
-    private boolean animatedZoomTo(RectF zoomToRect) {
-        final float startZoom = getMetrics().zoomFactor;
-
-        RectF viewport = getMetrics().getViewport();
-        // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
-        // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
-        // while enlarging make sure we enlarge equally on both sides to keep the target rect
-        // centered.
-        float targetRatio = viewport.width() / viewport.height();
-        float rectRatio = zoomToRect.width() / zoomToRect.height();
-        if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
-            // all good, do nothing
-        } else if (targetRatio < rectRatio) {
-            // need to increase zoomToRect height
-            float newHeight = zoomToRect.width() / targetRatio;
-            zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
-            zoomToRect.bottom = zoomToRect.top + newHeight;
-        } else { // targetRatio > rectRatio) {
-            // need to increase zoomToRect width
-            float newWidth = targetRatio * zoomToRect.height();
-            zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
-            zoomToRect.right = zoomToRect.left + newWidth;
-        }
-
-        float finalZoom = viewport.width() / zoomToRect.width();
-
-        ImmutableViewportMetrics finalMetrics = getMetrics();
-        finalMetrics = finalMetrics.setViewportOrigin(
-            zoomToRect.left * finalMetrics.zoomFactor,
-            zoomToRect.top * finalMetrics.zoomFactor);
-        finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
-
-        // 2. now run getValidViewportMetrics on it, so that the target viewport is
-        // clamped down to prevent overscroll, over-zoom, and other bad conditions.
-        finalMetrics = getValidViewportMetrics(finalMetrics);
-
-        bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
-        return true;
-    }
-
-    /** This function must be called from the UI thread. */
-    public void abortPanning() {
-        checkMainThread();
-        bounce();
-    }
-
-    public void setOverScrollMode(int overscrollMode) {
-        mX.setOverScrollMode(overscrollMode);
-        mY.setOverScrollMode(overscrollMode);
-    }
-
-    public int getOverScrollMode() {
-        return mX.getOverScrollMode();
-    }
+    public void setOverScrollMode(int overscrollMode);
+    public int getOverScrollMode();
 }
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
index 5e88cc2..cba3802 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -57,7 +57,7 @@ public final class TouchEventHandler {
     private final LayerView mView;
     private final GestureDetector mGestureDetector;
     private final SimpleScaleGestureDetector mScaleGestureDetector;
-    private final PanZoomController mPanZoomController;
+    private final JavaPanZoomController mPanZoomController;
 
     // the queue of events that we are holding on to while waiting for a preventDefault
     // notification
@@ -126,7 +126,7 @@ public final class TouchEventHandler {
         mView = view;
 
         mEventQueue = new LinkedList<MotionEvent>();
-        mPanZoomController = layerClient.getPanZoomController();
+        mPanZoomController = (JavaPanZoomController)layerClient.getPanZoomController();
         mGestureDetector = new GestureDetector(context, mPanZoomController);
         mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
         mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
commit 17f363f6c70b11237dd58d8be509b8b91e534276
Author: Tomaž Vajngerl <tomaz.vajngerl at collabora.com>
Date:   Sun Nov 30 17:26:50 2014 +0100

    android: Fennec change - move classes from gecko.ui to gecko.gfx
    
    Change-Id: I510e0d601e293ed8e013a0273d5ae0f9be078d8a

diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Axis.java
similarity index 99%
rename from android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java
rename to android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Axis.java
index 7ae8084..61407cb 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Axis.java
@@ -3,7 +3,7 @@
  * 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.ui;
+package org.mozilla.gecko.gfx;
 
 import android.util.Log;
 import android.view.View;
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
index aa47aaa..97d0944 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -19,9 +19,6 @@ import org.libreoffice.LOKitShell;
 import org.libreoffice.TileIdentifier;
 import org.libreoffice.TileProvider;
 import org.mozilla.gecko.ZoomConstraints;
-import org.mozilla.gecko.ui.PanZoomController;
-import org.mozilla.gecko.ui.PanZoomTarget;
-import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
 import org.mozilla.gecko.util.FloatUtils;
 
 public class GeckoLayerClient implements PanZoomTarget, LayerView.Listener {
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
similarity index 99%
rename from android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
rename to android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
index 65daf76..33471e0 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -3,7 +3,7 @@
  * 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.ui;
+package org.mozilla.gecko.gfx;
 
 import android.graphics.PointF;
 import android.graphics.RectF;
@@ -713,12 +713,12 @@ public class PanZoomController
             if (viewport.width() > pageRect.width())
                 focusX = 0.0f;
         }
-        if (pageRect.height() > 0) {
+        /*if (pageRect.height() > 0) {
             float scaleFactor = viewport.height() / pageRect.height();
             minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
             if (viewport.height() > pageRect.height())
                 focusY = 0.0f;
-        }
+        }*/
 
         maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
 
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java
similarity index 91%
rename from android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java
rename to android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java
index b4eeb52..f79819c 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -3,12 +3,11 @@
  * 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.ui;
+package org.mozilla.gecko.gfx;
 
 import android.graphics.PointF;
 
 import org.mozilla.gecko.ZoomConstraints;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 
 public interface PanZoomTarget {
     public ImmutableViewportMetrics getViewportMetrics();
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
similarity index 99%
rename from android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java
rename to android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
index faafad0..76b8d27 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
@@ -3,7 +3,7 @@
  * 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.ui;
+package org.mozilla.gecko.gfx;
 
 import android.graphics.PointF;
 import android.util.Log;
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
similarity index 98%
rename from android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java
rename to android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
index 86f92d3..5a752e3 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
@@ -3,7 +3,7 @@
  * 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.ui;
+package org.mozilla.gecko.gfx;
 
 import android.graphics.PointF;
 import android.os.Handler;
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
index a17e63c..5e88cc2 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -13,8 +13,6 @@ import android.view.GestureDetector;
 import android.view.MotionEvent;
 
 import org.mozilla.gecko.OnInterceptTouchListener;
-import org.mozilla.gecko.ui.PanZoomController;
-import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
 
 import java.util.LinkedList;
 import java.util.Queue;
commit a312e6eabf158c1a5f7f9216515582ca7d99beeb
Author: Tomaž Vajngerl <tomaz.vajngerl at collabora.com>
Date:   Sun Nov 30 17:21:10 2014 +0100

    android: change LOKitThread.draw to return void
    
    Change-Id: I00f3ed059bd633e662e5bee09703ca505bf3b9a5

diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
index 30a8bc9..1b233f0 100644
--- a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
@@ -23,10 +23,10 @@ public class LOKitThread extends Thread {
         TileProviderFactory.initialize();
     }
 
-    private boolean draw() {
+    private void draw() {
         if (mTileProvider == null || mApplication == null) {
             // called too early...
-            return false;
+            return;
         }
 
         RectF rect = new RectF(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
@@ -34,8 +34,6 @@ public class LOKitThread extends Thread {
         mViewportMetrics = new ImmutableViewportMetrics(displayMetrics);
         mViewportMetrics = mViewportMetrics.setPageRect(rect, rect);
         mLayerClient.reevaluateTiles();
-
-        return true;
     }
 
     private void tileRequest(TileIdentifier tileId) {
commit e0c5df41457441bb4414fd342795c47cda659b43
Author: Tomaž Vajngerl <tomaz.vajngerl at collabora.com>
Date:   Sun Nov 30 17:18:56 2014 +0100

    android: Fennec PanZoomControler updates..
    
    Change-Id: I277fbf522b16d9b479260df8372a5ee160adcc37

diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
index c6af5e7..30a8bc9 100644
--- a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
@@ -55,7 +55,7 @@ public class LOKitThread extends Thread {
 
         mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
         mLayerClient.setViewportMetrics(mLayerClient.getViewportMetrics());
-        mLayerClient.setForceRedraw();
+        mLayerClient.forceRedraw();
     }
 
     /** Invalidate everything + handle the geometry change + draw. */
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/OnInterceptTouchListener.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/OnInterceptTouchListener.java
new file mode 100644
index 0000000..d0cd3d4
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/OnInterceptTouchListener.java
@@ -0,0 +1,14 @@
+/* -*- 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;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface OnInterceptTouchListener extends View.OnTouchListener {
+    /** Override this method for a chance to consume events before the view or its children */
+    public boolean onInterceptTouchEvent(View view, MotionEvent event);
+}
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
index 7efed04..aa47aaa 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -400,7 +400,7 @@ public class GeckoLayerClient implements PanZoomTarget, LayerView.Listener {
     }
 
     /** Implementation of PanZoomTarget */
-    public void setForceRedraw() {
+    public void forceRedraw() {
         mForceRedraw = true;
         if (mGeckoIsReady) {
             geometryChanged();
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
index f0fe21b..a17e63c 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -6,12 +6,13 @@
 package org.mozilla.gecko.gfx;
 
 import android.content.Context;
+import android.os.Build;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
-import android.view.View.OnTouchListener;
 
+import org.mozilla.gecko.OnInterceptTouchListener;
 import org.mozilla.gecko.ui.PanZoomController;
 import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
 
@@ -66,7 +67,7 @@ public final class TouchEventHandler {
     private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
 
     // the listener we use to notify gecko of touch events
-    private OnTouchListener mOnTouchListener;
+    private OnInterceptTouchListener mOnTouchListener;
 
     // whether or not we should wait for touch listeners to respond (this state is
     // per-tab and is updated when we switch tabs).
@@ -127,13 +128,16 @@ public final class TouchEventHandler {
         mView = view;
 
         mEventQueue = new LinkedList<MotionEvent>();
-        mGestureDetector = new GestureDetector(context, layerClient.getGestureListener());
-        mScaleGestureDetector = new SimpleScaleGestureDetector(layerClient.getScaleGestureListener());
         mPanZoomController = layerClient.getPanZoomController();
+        mGestureDetector = new GestureDetector(context, mPanZoomController);
+        mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
         mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
         mDispatchEvents = true;
 
-        mGestureDetector.setOnDoubleTapListener(layerClient.getDoubleTapListener());
+        mGestureDetector.setOnDoubleTapListener(mPanZoomController);
+    }
+
+    void destroy() {
     }
 
     /* This function MUST be called on the UI thread */
@@ -145,12 +149,21 @@ public final class TouchEventHandler {
             return true;
         }
 
+        if (mOnTouchListener.onInterceptTouchEvent(mView, event)) {
+            return true;
+        }
+
         // if this is a hover event just notify gecko, we don't have any interest in the java layer.
         if (isHoverEvent(event)) {
             mOnTouchListener.onTouch(mView, event);
             return true;
         }
 
+        if (isScrollEvent(event)) {
+            dispatchEvent(event);
+            return true;
+        }
+
         if (isDownEvent(event)) {
             // this is the start of a new block of events! whee!
             mHoldInQueue = mWaitForTouchListeners;
@@ -225,7 +238,7 @@ public final class TouchEventHandler {
     }
 
     /* This function MUST be called on the UI thread. */
-    public void setOnTouchListener(OnTouchListener onTouchListener) {
+    public void setOnTouchListener(OnInterceptTouchListener onTouchListener) {
         mOnTouchListener = onTouchListener;
     }
 
@@ -244,6 +257,14 @@ public final class TouchEventHandler {
         return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
     }
 
+    private boolean isScrollEvent(MotionEvent event) {
+        if (Build.VERSION.SDK_INT <= 11) {
+            return false;
+        }
+        int action = (event.getAction() & MotionEvent.ACTION_MASK);
+        return (action == MotionEvent.ACTION_SCROLL);
+    }
+
     /**
      * Dispatch the event to the gesture detectors and the pan/zoom controller.
      */
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
index 555c7ebe..65daf76 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
@@ -50,6 +50,9 @@ public class PanZoomController
     // The maximum amount we allow you to zoom into a page
     private static final float MAX_ZOOM = 4.0f;
 
+    // The maximum amount we would like to scroll with the mouse
+    private static final float MAX_SCROLL = 0.075f * LOKitShell.getDpi();
+
     private enum PanZoomState {
         NOTHING,        /* no touch-start events received */
         FLING,          /* all touches removed, but we're still scrolling page */
@@ -110,7 +113,9 @@ public class PanZoomController
     }
 
     private void setState(PanZoomState state) {
-        mState = state;
+        if (state != mState) {
+            mState = state;
+        }
     }
 
     private ImmutableViewportMetrics getMetrics() {
@@ -131,6 +136,7 @@ public class PanZoomController
         case MotionEvent.ACTION_MOVE:   return onTouchMove(event);
         case MotionEvent.ACTION_UP:     return onTouchEnd(event);
         case MotionEvent.ACTION_CANCEL: return onTouchCancel(event);
+        case MotionEvent.ACTION_SCROLL: return onScroll(event);
         default:                        return false;
         }
     }
@@ -158,6 +164,7 @@ public class PanZoomController
             // transitions.
             synchronized (mTarget.getLock()) {
                 mTarget.setViewportMetrics(getValidViewportMetrics());
+                mTarget.forceRedraw();
             }
             break;
         }
@@ -214,7 +221,7 @@ public class PanZoomController
             // We just interrupted a double-tap animation, so force a redraw in
             // case this touchstart is just a tap that doesn't end up triggering
             // a redraw
-            mTarget.setForceRedraw();
+            mTarget.forceRedraw();
             // fall through
         case FLING:
         case BOUNCE:
@@ -337,6 +344,18 @@ public class PanZoomController
         return false;
     }
 
+    private boolean onScroll(MotionEvent event) {
+        if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
+            float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+            float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+
+            scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
+            bounce();
+            return true;
+        }
+        return false;
+    }
+
     private void startTouch(float x, float y, long time) {
         mX.startTouch(x);
         mY.startTouch(y);
@@ -434,7 +453,7 @@ public class PanZoomController
     }
 
     /* Performs a bounce-back animation to the given viewport metrics. */
-    private void bounce(ImmutableViewportMetrics metrics) {
+    private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
         stopAnimationTimer();
 
         ImmutableViewportMetrics bounceStartMetrics = getMetrics();
@@ -443,6 +462,8 @@ public class PanZoomController
             return;
         }
 
+        setState(state);
+
         // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
         // getRedrawHint() is returning false. This means we can safely call
         // setAnimationTarget to set the new final display port and not have it get
@@ -453,8 +474,7 @@ public class PanZoomController
 
     /* Performs a bounce-back animation to the nearest valid viewport metrics. */
     private void bounce() {
-        setState(PanZoomState.BOUNCE);
-        bounce(getValidViewportMetrics());
+        bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
     }
 
     /* Starts the fling or bounce animation. */
@@ -654,7 +674,7 @@ public class PanZoomController
         stopAnimationTimer();
 
         // Force a viewport synchronisation
-        mTarget.setForceRedraw();
+        mTarget.forceRedraw();
     }
 
     /* Returns the nearest viewport metrics with no overscroll visible. */
@@ -837,7 +857,8 @@ public class PanZoomController
         startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
 
         // Force a viewport synchronisation
-        mTarget.setForceRedraw();
+        mTarget.forceRedraw();
+
     }
 
     /**
@@ -896,7 +917,6 @@ public class PanZoomController
      * pixels.
      */
     private boolean animatedZoomTo(RectF zoomToRect) {
-        setState(PanZoomState.ANIMATED_ZOOM);
         final float startZoom = getMetrics().zoomFactor;
 
         RectF viewport = getMetrics().getViewport();
@@ -932,7 +952,7 @@ public class PanZoomController
         // clamped down to prevent overscroll, over-zoom, and other bad conditions.
         finalMetrics = getValidViewportMetrics(finalMetrics);
 
-        bounce(finalMetrics);
+        bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
         return true;
     }
 
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java
index fdac874..b4eeb52 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomTarget.java
@@ -16,7 +16,8 @@ public interface PanZoomTarget {
 
     public void setAnimationTarget(ImmutableViewportMetrics viewport);
     public void setViewportMetrics(ImmutableViewportMetrics viewport);
-    public void setForceRedraw();
+    /** This triggers an (asynchronous) viewport update/redraw. */
+    public void forceRedraw();
 
     public boolean post(Runnable action);
     public Object getLock();
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java
index 6f920ca..faafad0 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list