summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomaž Vajngerl <tomaz.vajngerl@collabora.com>2014-11-30 18:07:19 +0100
committerTomaž Vajngerl <tomaz.vajngerl@collabora.com>2014-11-30 18:07:19 +0100
commit365c82688f6cb6ff7fa89010d3da4e7188f04cec (patch)
tree5c35cf79f43543d19083a423621041d8e10fe471
parent17f363f6c70b11237dd58d8be509b8b91e534276 (diff)
android: extract JavaPanZoomController (PZC becomes an interface)feature/droid_calcimpress3
Change-Id: I87e63008fe7c6db62c18bf461dc4dcda733393ac
-rw-r--r--android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java15
-rw-r--r--android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java972
-rw-r--r--android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanZoomController.java963
-rw-r--r--android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java4
4 files changed, 987 insertions, 967 deletions
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 97d0944f1177..d1952491e2d0 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 000000000000..af4713675cd5
--- /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 33471e0a1d52..86036bbea287 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 5e88cc20627c..cba380236465 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();