2011-11-18 10:28:17 -08:00
|
|
|
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
*
|
|
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
|
|
* the License. You may obtain a copy of the License at
|
|
|
|
* http://www.mozilla.org/MPL/
|
|
|
|
*
|
|
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
|
|
* for the specific language governing rights and limitations under the
|
|
|
|
* License.
|
|
|
|
*
|
|
|
|
* The Original Code is Mozilla Android code.
|
|
|
|
*
|
|
|
|
* The Initial Developer of the Original Code is Mozilla Foundation.
|
|
|
|
* Portions created by the Initial Developer are Copyright (C) 2009-2010
|
|
|
|
* the Initial Developer. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Contributor(s):
|
|
|
|
* Patrick Walton <pcwalton@mozilla.com>
|
|
|
|
*
|
|
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
|
|
* the provisions above, a recipient may use your version of this file under
|
|
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
*
|
|
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
|
|
|
|
package org.mozilla.gecko.ui;
|
|
|
|
|
|
|
|
import org.json.JSONObject;
|
2011-12-06 07:13:14 -08:00
|
|
|
import org.json.JSONException;
|
2011-11-23 11:07:47 -08:00
|
|
|
import org.mozilla.gecko.gfx.FloatSize;
|
2011-11-18 10:28:17 -08:00
|
|
|
import org.mozilla.gecko.gfx.LayerController;
|
2011-11-15 13:41:19 -08:00
|
|
|
import org.mozilla.gecko.gfx.PointUtils;
|
2011-12-07 10:44:36 -08:00
|
|
|
import org.mozilla.gecko.gfx.ViewportMetrics;
|
2011-11-23 11:07:47 -08:00
|
|
|
import org.mozilla.gecko.FloatUtils;
|
2011-11-18 10:28:17 -08:00
|
|
|
import org.mozilla.gecko.GeckoApp;
|
|
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
|
|
import org.mozilla.gecko.GeckoEvent;
|
2011-11-15 13:41:19 -08:00
|
|
|
import org.mozilla.gecko.GeckoEventListener;
|
2011-11-18 10:28:17 -08:00
|
|
|
import android.graphics.PointF;
|
|
|
|
import android.graphics.RectF;
|
2012-01-10 07:05:51 -08:00
|
|
|
import android.util.FloatMath;
|
2011-11-18 10:28:17 -08:00
|
|
|
import android.util.Log;
|
|
|
|
import android.view.GestureDetector;
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
import android.view.ScaleGestureDetector;
|
|
|
|
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
|
2011-11-15 13:41:19 -08:00
|
|
|
implements ScaleGestureDetector.OnScaleGestureListener, GeckoEventListener
|
2011-11-18 10:28:17 -08:00
|
|
|
{
|
|
|
|
private static final String LOGTAG = "GeckoPanZoomController";
|
|
|
|
|
2011-12-15 12:04:49 -08:00
|
|
|
// This fraction of velocity remains after every animation frame when the velocity is low.
|
|
|
|
private static final float FRICTION_SLOW = 0.85f;
|
|
|
|
// This fraction of velocity remains after every animation frame when the velocity is high.
|
|
|
|
private static final float FRICTION_FAST = 0.97f;
|
|
|
|
// Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST
|
|
|
|
// to FRICTION_SLOW.
|
|
|
|
private static final float VELOCITY_THRESHOLD = 10.0f;
|
|
|
|
// Animation stops if the velocity is below this value when overscrolled or panning.
|
2011-11-18 10:28:17 -08:00
|
|
|
private static final float STOPPED_THRESHOLD = 4.0f;
|
2011-12-15 12:04:49 -08:00
|
|
|
// Animation stops is the velocity is below this threshold when flinging.
|
|
|
|
private static final float FLING_STOPPED_THRESHOLD = 0.1f;
|
2011-11-18 10:28:17 -08:00
|
|
|
// The percentage of the surface which can be overscrolled before it must snap back.
|
|
|
|
private static final float SNAP_LIMIT = 0.75f;
|
|
|
|
// The rate of deceleration when the surface has overscrolled.
|
|
|
|
private static final float OVERSCROLL_DECEL_RATE = 0.04f;
|
|
|
|
// The distance the user has to pan before we recognize it as such (e.g. to avoid
|
2011-11-30 13:10:22 -08:00
|
|
|
// 1-pixel pans between the touch-down and touch-up of a click). In units of inches.
|
|
|
|
private static final float PAN_THRESHOLD = 0.1f;
|
2011-11-18 13:08:17 -08:00
|
|
|
// Angle from axis within which we stay axis-locked
|
2011-11-22 22:01:45 -08:00
|
|
|
private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
|
2011-11-30 03:32:49 -08:00
|
|
|
// The maximum velocity change factor between events, per ms, in %.
|
|
|
|
// Direction changes are excluded.
|
|
|
|
private static final float MAX_EVENT_ACCELERATION = 0.012f;
|
2011-12-08 21:31:04 -08:00
|
|
|
// The minimum amount of space that must be present for an axis to be considered scrollable,
|
|
|
|
// in pixels.
|
|
|
|
private static final float MIN_SCROLLABLE_DISTANCE = 0.5f;
|
2012-01-10 07:05:49 -08:00
|
|
|
// The number of milliseconds per frame assuming 60 fps
|
|
|
|
private static final float MS_PER_FRAME = 1000.0f / 60.0f;
|
2011-12-09 09:03:19 -08:00
|
|
|
// The maximum amount we allow you to zoom into a page
|
|
|
|
private static final float MAX_ZOOM = 4.0f;
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2011-12-07 10:41:58 -08:00
|
|
|
/* 16 precomputed frames of the _ease-out_ animation from the CSS Transitions specification. */
|
|
|
|
private static final float[] EASE_OUT_ANIMATION_FRAMES = {
|
|
|
|
0.00000f, /* 0 */
|
|
|
|
0.10211f, /* 1 */
|
|
|
|
0.19864f, /* 2 */
|
|
|
|
0.29043f, /* 3 */
|
|
|
|
0.37816f, /* 4 */
|
|
|
|
0.46155f, /* 5 */
|
|
|
|
0.54054f, /* 6 */
|
|
|
|
0.61496f, /* 7 */
|
|
|
|
0.68467f, /* 8 */
|
|
|
|
0.74910f, /* 9 */
|
|
|
|
0.80794f, /* 10 */
|
|
|
|
0.86069f, /* 11 */
|
|
|
|
0.90651f, /* 12 */
|
|
|
|
0.94471f, /* 13 */
|
|
|
|
0.97401f, /* 14 */
|
|
|
|
0.99309f, /* 15 */
|
|
|
|
};
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
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 */
|
2011-11-18 13:08:17 -08:00
|
|
|
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.
|
2011-11-18 10:28:17 -08:00
|
|
|
* similar to TOUCHING but after starting a pan */
|
2011-11-18 13:08:17 -08:00
|
|
|
PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
|
2011-11-18 10:28:17 -08:00
|
|
|
PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
|
2011-12-12 08:22:34 -08:00
|
|
|
ANIMATED_ZOOM /* animated zoom to a new rect */
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:46 -08:00
|
|
|
private final LayerController mController;
|
|
|
|
private final Axis mX;
|
|
|
|
private final Axis mY;
|
|
|
|
|
|
|
|
/* 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. */
|
2011-11-18 10:28:17 -08:00
|
|
|
private PanZoomState mState;
|
|
|
|
|
2011-12-06 07:13:14 -08:00
|
|
|
private boolean mOverridePanning;
|
|
|
|
private boolean mOverrideScrollAck;
|
|
|
|
private boolean mOverrideScrollPending;
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
public PanZoomController(LayerController controller) {
|
|
|
|
mController = controller;
|
2012-01-10 07:05:46 -08:00
|
|
|
mX = new AxisX();
|
|
|
|
mY = new AxisY();
|
2011-11-18 10:28:17 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
|
|
|
|
2011-11-15 13:41:19 -08:00
|
|
|
GeckoAppShell.registerGeckoEventListener("Browser:ZoomToRect", this);
|
|
|
|
GeckoAppShell.registerGeckoEventListener("Browser:ZoomToPageWidth", this);
|
2011-12-06 07:13:14 -08:00
|
|
|
GeckoAppShell.registerGeckoEventListener("Panning:Override", this);
|
|
|
|
GeckoAppShell.registerGeckoEventListener("Panning:CancelOverride", this);
|
|
|
|
GeckoAppShell.registerGeckoEventListener("Gesture:ScrollAck", this);
|
2011-11-15 13:41:19 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
public void handleMessage(String event, JSONObject message) {
|
|
|
|
Log.i(LOGTAG, "Got message: " + event);
|
|
|
|
try {
|
2011-12-06 07:13:14 -08:00
|
|
|
if ("Panning:Override".equals(event)) {
|
|
|
|
mOverridePanning = true;
|
|
|
|
mOverrideScrollAck = true;
|
|
|
|
} else if ("Panning:CancelOverride".equals(event)) {
|
|
|
|
mOverridePanning = false;
|
|
|
|
} else if ("Gesture:ScrollAck".equals(event)) {
|
|
|
|
mController.post(new Runnable() {
|
|
|
|
public void run() {
|
|
|
|
mOverrideScrollAck = true;
|
|
|
|
if (mOverridePanning && mOverrideScrollPending)
|
|
|
|
updatePosition();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (event.equals("Browser:ZoomToRect")) {
|
2012-01-10 07:05:46 -08:00
|
|
|
float scale = mController.getZoomFactor();
|
|
|
|
float x = (float)message.getDouble("x");
|
|
|
|
float y = (float)message.getDouble("y");
|
|
|
|
final RectF zoomRect = new RectF(x, y,
|
|
|
|
x + (float)message.getDouble("w"),
|
|
|
|
y + (float)message.getDouble("h"));
|
|
|
|
mController.post(new Runnable() {
|
|
|
|
public void run() {
|
|
|
|
animatedZoomTo(zoomRect);
|
|
|
|
}
|
|
|
|
});
|
2011-11-15 13:41:19 -08:00
|
|
|
} else if (event.equals("Browser:ZoomToPageWidth")) {
|
2012-01-10 07:05:46 -08:00
|
|
|
float scale = mController.getZoomFactor();
|
|
|
|
FloatSize pageSize = mController.getPageSize();
|
|
|
|
|
|
|
|
RectF viewableRect = mController.getViewport();
|
|
|
|
float y = viewableRect.top;
|
|
|
|
// attempt to keep zoom keep focused on the center of the viewport
|
|
|
|
float dh = viewableRect.height()*(1 - pageSize.width/viewableRect.width()); // increase in the height
|
|
|
|
final RectF r = new RectF(0.0f,
|
|
|
|
y + dh/2,
|
|
|
|
pageSize.width,
|
|
|
|
(y + pageSize.width * viewableRect.height()/viewableRect.width()));
|
|
|
|
mController.post(new Runnable() {
|
|
|
|
public void run() {
|
|
|
|
animatedZoomTo(r);
|
|
|
|
}
|
|
|
|
});
|
2011-11-15 13:41:19 -08:00
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
|
|
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean onTouchEvent(MotionEvent event) {
|
2011-12-01 14:47:40 -08:00
|
|
|
switch (event.getAction() & event.ACTION_MASK) {
|
2011-11-18 10:28:17 -08:00
|
|
|
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);
|
|
|
|
default: return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-20 13:53:39 -08:00
|
|
|
/** This function must be called from the UI thread. */
|
2012-01-05 10:49:33 -08:00
|
|
|
@SuppressWarnings("fallthrough")
|
2011-12-20 13:53:39 -08:00
|
|
|
public void abortAnimation() {
|
|
|
|
// 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.velocity = mY.velocity = 0.0f;
|
|
|
|
mState = PanZoomState.NOTHING;
|
|
|
|
// fall through
|
|
|
|
case ANIMATED_ZOOM:
|
|
|
|
// the zoom that's in progress likely makes no sense any more (such as if
|
2012-01-10 07:05:46 -08:00
|
|
|
// the screen orientation changed) so abort it
|
|
|
|
// fall through
|
2011-12-20 13:53:39 -08:00
|
|
|
case NOTHING:
|
2012-01-03 22:55:04 -08:00
|
|
|
// Don't do animations here; they're distracting and can cause flashes on page
|
|
|
|
// transitions.
|
|
|
|
mController.setViewportMetrics(getValidViewportMetrics());
|
|
|
|
mController.notifyLayerClientOfGeometryChange();
|
2011-12-20 13:53:39 -08:00
|
|
|
break;
|
2011-12-01 06:37:37 -08:00
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Panning/scrolling
|
|
|
|
*/
|
|
|
|
|
|
|
|
private boolean onTouchStart(MotionEvent event) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onTouchStart in state " + mState);
|
2011-11-21 12:26:45 -08:00
|
|
|
// user is taking control of movement, so stop
|
|
|
|
// any auto-movement we have going
|
2011-12-07 13:07:24 -08:00
|
|
|
stopAnimationTimer();
|
2011-12-06 07:13:14 -08:00
|
|
|
mOverridePanning = false;
|
2011-11-21 12:26:45 -08:00
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
switch (mState) {
|
2011-11-15 13:41:19 -08:00
|
|
|
case ANIMATED_ZOOM:
|
|
|
|
return false;
|
2011-11-18 10:28:17 -08:00
|
|
|
case FLING:
|
|
|
|
case NOTHING:
|
2012-01-10 07:05:48 -08:00
|
|
|
startTouch(event.getX(0), event.getY(0), event.getEventTime());
|
2011-11-18 10:28:17 -08:00
|
|
|
return false;
|
|
|
|
case TOUCHING:
|
|
|
|
case PANNING:
|
2011-11-18 13:08:17 -08:00
|
|
|
case PANNING_LOCKED:
|
2011-11-18 10:28:17 -08:00
|
|
|
case PANNING_HOLD:
|
2011-11-18 13:08:17 -08:00
|
|
|
case PANNING_HOLD_LOCKED:
|
2011-11-18 10:28:17 -08:00
|
|
|
case PINCHING:
|
2012-01-10 07:05:43 -08:00
|
|
|
Log.e(LOGTAG, "Received impossible touch down while in " + mState);
|
2011-11-18 10:28:17 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchStart");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-01-05 10:49:33 -08:00
|
|
|
@SuppressWarnings("fallthrough")
|
2011-11-18 10:28:17 -08:00
|
|
|
private boolean onTouchMove(MotionEvent event) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onTouchMove in state " + mState);
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
switch (mState) {
|
|
|
|
case NOTHING:
|
|
|
|
case FLING:
|
|
|
|
// should never happen
|
|
|
|
Log.e(LOGTAG, "Received impossible touch move while in " + mState);
|
|
|
|
return false;
|
|
|
|
case TOUCHING:
|
2012-01-10 07:05:51 -08:00
|
|
|
if (panDistance(event) < PAN_THRESHOLD * GeckoAppShell.getDpi()) {
|
2011-11-18 10:28:17 -08:00
|
|
|
return false;
|
2012-01-10 07:05:51 -08:00
|
|
|
}
|
2011-11-30 13:10:25 -08:00
|
|
|
cancelTouch();
|
2011-11-18 10:28:17 -08:00
|
|
|
// fall through
|
2011-11-18 13:08:17 -08:00
|
|
|
case PANNING_HOLD_LOCKED:
|
2011-12-06 16:44:08 -08:00
|
|
|
GeckoApp.mAppContext.mAutoCompletePopup.hide();
|
2011-11-18 13:08:17 -08:00
|
|
|
mState = PanZoomState.PANNING_LOCKED;
|
|
|
|
// fall through
|
|
|
|
case PANNING_LOCKED:
|
2011-11-21 12:26:45 -08:00
|
|
|
track(event);
|
2011-11-18 13:08:17 -08:00
|
|
|
return true;
|
2011-11-18 10:28:17 -08:00
|
|
|
case PANNING_HOLD:
|
2011-12-06 16:44:08 -08:00
|
|
|
GeckoApp.mAppContext.mAutoCompletePopup.hide();
|
2011-11-18 10:28:17 -08:00
|
|
|
mState = PanZoomState.PANNING;
|
|
|
|
// fall through
|
|
|
|
case PANNING:
|
2011-11-21 12:26:45 -08:00
|
|
|
track(event);
|
2011-11-18 10:28:17 -08:00
|
|
|
return true;
|
2011-11-15 13:41:19 -08:00
|
|
|
case ANIMATED_ZOOM:
|
2011-11-18 10:28:17 -08:00
|
|
|
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) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onTouchEnd in " + mState);
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
switch (mState) {
|
|
|
|
case NOTHING:
|
|
|
|
case FLING:
|
|
|
|
// should never happen
|
|
|
|
Log.e(LOGTAG, "Received impossible touch end while in " + mState);
|
|
|
|
return false;
|
|
|
|
case TOUCHING:
|
|
|
|
mState = PanZoomState.NOTHING;
|
2011-11-21 12:26:45 -08:00
|
|
|
// 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
|
2011-12-07 13:33:55 -08:00
|
|
|
bounce();
|
2011-11-18 10:28:17 -08:00
|
|
|
return false;
|
|
|
|
case PANNING:
|
2011-11-18 13:08:17 -08:00
|
|
|
case PANNING_LOCKED:
|
2011-11-18 10:28:17 -08:00
|
|
|
case PANNING_HOLD:
|
2011-11-18 13:08:17 -08:00
|
|
|
case PANNING_HOLD_LOCKED:
|
2011-11-18 10:28:17 -08:00
|
|
|
mState = PanZoomState.FLING;
|
2011-11-21 12:26:45 -08:00
|
|
|
fling();
|
2011-11-18 10:28:17 -08:00
|
|
|
return true;
|
|
|
|
case PINCHING:
|
2012-01-10 07:05:43 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
2011-11-18 10:28:17 -08:00
|
|
|
return true;
|
2011-11-15 13:41:19 -08:00
|
|
|
case ANIMATED_ZOOM:
|
|
|
|
return false;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean onTouchCancel(MotionEvent event) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onTouchCancel in " + mState);
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
2011-11-21 12:26:45 -08:00
|
|
|
// ensure we snap back if we're overscrolled
|
2011-12-07 13:33:55 -08:00
|
|
|
bounce();
|
2011-11-18 10:28:17 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:48 -08:00
|
|
|
private void startTouch(float x, float y, long time) {
|
|
|
|
mX.startTouch(x);
|
|
|
|
mY.startTouch(y);
|
|
|
|
mState = PanZoomState.TOUCHING;
|
|
|
|
mLastEventTime = time;
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
private float panDistance(MotionEvent move) {
|
2012-01-10 07:05:51 -08:00
|
|
|
float dx = mX.panDistance(move.getX(0));
|
|
|
|
float dy = mY.panDistance(move.getY(0));
|
|
|
|
return FloatMath.sqrt(dx * dx + dy * dy);
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:49 -08:00
|
|
|
private void track(float x, float y, long time) {
|
|
|
|
float timeDelta = (float)(time - mLastEventTime);
|
2011-12-19 06:26:41 -08:00
|
|
|
if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
|
|
|
|
// probably a duplicate event, ignore it. using a zero timeDelta will mess
|
|
|
|
// up our velocity
|
|
|
|
return;
|
|
|
|
}
|
2012-01-10 07:05:49 -08:00
|
|
|
mLastEventTime = time;
|
2011-12-19 06:26:41 -08:00
|
|
|
|
2011-11-18 13:08:17 -08:00
|
|
|
if (mState == PanZoomState.PANNING_LOCKED) {
|
|
|
|
// check to see if we should break the axis lock
|
2012-01-10 07:05:51 -08:00
|
|
|
double angle = Math.atan2(mY.panDistance(y), mX.panDistance(x)); // range [-pi, pi]
|
2011-11-18 13:08:17 -08:00
|
|
|
angle = Math.abs(angle); // range [0, pi]
|
|
|
|
if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
|
|
|
|
// lock to x-axis
|
2011-11-30 03:32:49 -08:00
|
|
|
mX.locked = false;
|
|
|
|
mY.locked = true;
|
2011-11-18 13:08:17 -08:00
|
|
|
} else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
|
|
|
|
// lock to y-axis
|
2011-11-30 03:32:49 -08:00
|
|
|
mX.locked = true;
|
|
|
|
mY.locked = false;
|
2011-11-18 13:08:17 -08:00
|
|
|
} else {
|
|
|
|
// break axis lock but log the angle so we can fine-tune this when people complain
|
|
|
|
mState = PanZoomState.PANNING;
|
2011-11-30 03:32:49 -08:00
|
|
|
mX.locked = mY.locked = false;
|
2011-11-18 13:08:17 -08:00
|
|
|
angle = Math.abs(angle - (Math.PI / 2)); // range [0, pi/2]
|
|
|
|
Log.i(LOGTAG, "Breaking axis lock at " + (angle * 180.0 / Math.PI) + " degrees");
|
|
|
|
}
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2012-01-10 07:05:49 -08:00
|
|
|
mX.updateWithTouchAt(x, timeDelta);
|
|
|
|
mY.updateWithTouchAt(y, timeDelta);
|
2011-11-30 03:32:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private void track(MotionEvent event) {
|
2012-01-10 07:05:53 -08:00
|
|
|
mX.saveTouchPos();
|
|
|
|
mY.saveTouchPos();
|
2011-11-30 03:32:49 -08:00
|
|
|
|
|
|
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
2012-01-10 07:05:49 -08:00
|
|
|
track(event.getHistoricalX(0, i),
|
|
|
|
event.getHistoricalY(0, i),
|
|
|
|
event.getHistoricalEventTime(i));
|
2011-11-30 03:32:49 -08:00
|
|
|
}
|
2012-01-10 07:05:49 -08:00
|
|
|
track(event.getX(0), event.getY(0), event.getEventTime());
|
2011-11-18 13:08:17 -08:00
|
|
|
|
|
|
|
if (stopped()) {
|
|
|
|
if (mState == PanZoomState.PANNING) {
|
|
|
|
mState = PanZoomState.PANNING_HOLD;
|
|
|
|
} else if (mState == PanZoomState.PANNING_LOCKED) {
|
|
|
|
mState = PanZoomState.PANNING_HOLD_LOCKED;
|
|
|
|
} else {
|
|
|
|
// should never happen, but handle anyway for robustness
|
|
|
|
Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
|
|
|
|
mState = PanZoomState.PANNING_HOLD_LOCKED;
|
|
|
|
}
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2011-12-09 12:06:15 -08:00
|
|
|
mX.setFlingState(Axis.FlingStates.PANNING); mY.setFlingState(Axis.FlingStates.PANNING);
|
2012-01-04 10:25:11 -08:00
|
|
|
mX.displace(mOverridePanning); mY.displace(mOverridePanning);
|
2011-11-18 10:28:17 -08:00
|
|
|
updatePosition();
|
|
|
|
}
|
|
|
|
|
2011-11-21 12:26:45 -08:00
|
|
|
private void fling() {
|
2011-12-06 07:13:14 -08:00
|
|
|
mX.disableSnap = mY.disableSnap = mOverridePanning;
|
|
|
|
|
2012-01-04 10:25:11 -08:00
|
|
|
mX.displace(mOverridePanning); mY.displace(mOverridePanning);
|
2011-11-18 10:28:17 -08:00
|
|
|
updatePosition();
|
|
|
|
|
2011-12-07 13:07:24 -08:00
|
|
|
stopAnimationTimer();
|
2011-11-18 10:28:17 -08:00
|
|
|
|
|
|
|
boolean stopped = stopped();
|
|
|
|
mX.startFling(stopped); mY.startFling(stopped);
|
|
|
|
|
2011-12-07 13:07:24 -08:00
|
|
|
startAnimationTimer(new FlingRunnable());
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/* Performs a bounce-back animation to the given viewport metrics. */
|
|
|
|
private void bounce(ViewportMetrics metrics) {
|
|
|
|
stopAnimationTimer();
|
|
|
|
|
2012-01-02 16:06:49 -08:00
|
|
|
ViewportMetrics bounceStartMetrics = new ViewportMetrics(mController.getViewportMetrics());
|
|
|
|
if (bounceStartMetrics.fuzzyEquals(metrics)) {
|
2011-12-16 14:01:02 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
mState = PanZoomState.FLING;
|
2012-01-02 16:06:49 -08:00
|
|
|
Log.d(LOGTAG, "end bounce at " + metrics);
|
2011-12-07 13:33:55 -08:00
|
|
|
|
2012-01-02 16:06:49 -08:00
|
|
|
startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
|
2011-12-07 13:33:55 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Performs a bounce-back animation to the nearest valid viewport metrics. */
|
|
|
|
private void bounce() {
|
|
|
|
bounce(getValidViewportMetrics());
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:07:24 -08:00
|
|
|
/* Starts the fling or bounce animation. */
|
2012-01-05 18:13:25 -08:00
|
|
|
private void startAnimationTimer(final AnimationRunnable runnable) {
|
2011-12-07 13:07:24 -08:00
|
|
|
if (mAnimationTimer != null) {
|
|
|
|
Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!");
|
|
|
|
stopAnimationTimer();
|
|
|
|
}
|
|
|
|
|
2011-12-12 00:27:35 -08:00
|
|
|
mAnimationTimer = new Timer("Animation Timer");
|
2012-01-05 18:13:25 -08:00
|
|
|
mAnimationRunnable = runnable;
|
2011-12-07 13:07:24 -08:00
|
|
|
mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
|
|
|
|
@Override
|
|
|
|
public void run() { mController.post(runnable); }
|
2011-11-18 10:28:17 -08:00
|
|
|
}, 0, 1000L/60L);
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:07:24 -08:00
|
|
|
/* Stops the fling or bounce animation. */
|
|
|
|
private void stopAnimationTimer() {
|
|
|
|
if (mAnimationTimer != null) {
|
|
|
|
mAnimationTimer.cancel();
|
|
|
|
mAnimationTimer = null;
|
|
|
|
}
|
2012-01-05 18:13:25 -08:00
|
|
|
if (mAnimationRunnable != null) {
|
|
|
|
mAnimationRunnable.terminate();
|
|
|
|
mAnimationRunnable = null;
|
|
|
|
}
|
2011-12-07 13:07:24 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:57 -08:00
|
|
|
private float getVelocity() {
|
|
|
|
float xvel = mX.getRealVelocity();
|
|
|
|
float yvel = mY.getRealVelocity();
|
|
|
|
return FloatMath.sqrt(xvel * xvel + yvel * yvel);
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
private boolean stopped() {
|
|
|
|
float absVelocity = (float)Math.sqrt(mX.velocity * mX.velocity +
|
|
|
|
mY.velocity * mY.velocity);
|
|
|
|
return absVelocity < STOPPED_THRESHOLD;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void updatePosition() {
|
2011-12-06 07:13:14 -08:00
|
|
|
if (mOverridePanning) {
|
|
|
|
if (!mOverrideScrollAck) {
|
|
|
|
mOverrideScrollPending = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
mOverrideScrollPending = false;
|
|
|
|
JSONObject json = new JSONObject();
|
|
|
|
|
|
|
|
try {
|
|
|
|
json.put("x", mX.displacement);
|
|
|
|
json.put("y", mY.displacement);
|
|
|
|
} catch (JSONException e) {
|
|
|
|
Log.e(LOGTAG, "Error forming Gesture:Scroll message: " + e);
|
|
|
|
}
|
|
|
|
|
|
|
|
GeckoEvent e = new GeckoEvent("Gesture:Scroll", json.toString());
|
|
|
|
GeckoAppShell.sendEventToGecko(e);
|
|
|
|
mOverrideScrollAck = false;
|
|
|
|
} else {
|
2011-12-13 14:43:08 -08:00
|
|
|
synchronized (mController) {
|
|
|
|
mController.scrollBy(new PointF(mX.displacement, mY.displacement));
|
|
|
|
}
|
2011-12-06 07:13:14 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
mX.displacement = mY.displacement = 0;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-05 18:13:25 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/* The callback that performs the bounce animation. */
|
2012-01-05 18:13:25 -08:00
|
|
|
private class BounceRunnable extends AnimationRunnable {
|
2012-01-02 16:06:49 -08:00
|
|
|
/* 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 ViewportMetrics mBounceStartMetrics;
|
|
|
|
private ViewportMetrics mBounceEndMetrics;
|
|
|
|
|
|
|
|
BounceRunnable(ViewportMetrics startMetrics, ViewportMetrics endMetrics) {
|
|
|
|
mBounceStartMetrics = startMetrics;
|
|
|
|
mBounceEndMetrics = endMetrics;
|
|
|
|
}
|
|
|
|
|
2012-01-05 18:13:25 -08:00
|
|
|
protected void animateFrame() {
|
2011-12-07 13:33:55 -08:00
|
|
|
/*
|
|
|
|
* 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;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/* Perform the next frame of the bounce-back animation. */
|
|
|
|
if (mBounceFrame < EASE_OUT_ANIMATION_FRAMES.length) {
|
|
|
|
advanceBounce();
|
|
|
|
return;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
2011-12-07 13:33:55 -08:00
|
|
|
|
|
|
|
/* Finally, if there's nothing else to do, complete the animation and go to sleep. */
|
|
|
|
finishBounce();
|
|
|
|
finishAnimation();
|
2011-12-20 13:53:39 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/* Performs one frame of a bounce animation. */
|
|
|
|
private void advanceBounce() {
|
2011-12-13 14:43:08 -08:00
|
|
|
synchronized (mController) {
|
|
|
|
float t = EASE_OUT_ANIMATION_FRAMES[mBounceFrame];
|
|
|
|
ViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
|
|
|
|
mController.setViewportMetrics(newMetrics);
|
|
|
|
mController.notifyLayerClientOfGeometryChange();
|
|
|
|
mBounceFrame++;
|
|
|
|
}
|
2011-12-07 13:33:55 -08:00
|
|
|
}
|
2011-11-23 11:08:20 -08:00
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/* Concludes a bounce animation and snaps the viewport into place. */
|
|
|
|
private void finishBounce() {
|
2011-12-13 14:43:08 -08:00
|
|
|
synchronized (mController) {
|
|
|
|
mController.setViewportMetrics(mBounceEndMetrics);
|
|
|
|
mController.notifyLayerClientOfGeometryChange();
|
|
|
|
mBounceFrame = -1;
|
|
|
|
}
|
2011-12-07 13:33:55 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The callback that performs the fling animation.
|
2012-01-05 18:13:25 -08:00
|
|
|
private class FlingRunnable extends AnimationRunnable {
|
|
|
|
protected void animateFrame() {
|
2011-12-07 13:33:55 -08:00
|
|
|
/*
|
|
|
|
* 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. */
|
2012-01-10 07:05:57 -08:00
|
|
|
boolean flingingX = mX.advanceFling();
|
|
|
|
boolean flingingY = mY.advanceFling();
|
|
|
|
|
|
|
|
boolean overscrolled = ((mX.overscrolled() || mY.overscrolled()) && !mOverridePanning);
|
2011-12-07 13:33:55 -08:00
|
|
|
|
2011-12-09 12:06:06 -08:00
|
|
|
/* If we're still flinging in any direction, update the origin. */
|
2011-12-07 13:33:55 -08:00
|
|
|
if (flingingX || flingingY) {
|
2012-01-04 10:25:11 -08:00
|
|
|
mX.displace(mOverridePanning); mY.displace(mOverridePanning);
|
2011-12-07 13:33:55 -08:00
|
|
|
updatePosition();
|
|
|
|
|
2011-12-29 19:29:16 -08:00
|
|
|
/*
|
2012-01-10 07:05:57 -08:00
|
|
|
* Check to see if we're still flinging with an appreciable velocity. The threshold is
|
2011-12-29 19:29:16 -08:00
|
|
|
* higher in the case of overscroll, so we bounce back eagerly when overscrolling but
|
2012-01-10 07:05:57 -08:00
|
|
|
* coast smoothly to a stop when not. In other words, require a greater velocity to
|
|
|
|
* maintain the fling once we enter overscroll.
|
2011-12-29 19:29:16 -08:00
|
|
|
*/
|
2012-01-10 07:05:57 -08:00
|
|
|
float threshold = (overscrolled ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
|
|
|
|
if (getVelocity() >= threshold) {
|
|
|
|
// we're still flinging
|
2011-12-29 19:29:16 -08:00
|
|
|
return;
|
2012-01-10 07:05:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
mX.stopFling();
|
|
|
|
mY.stopFling();
|
2011-12-29 19:29:16 -08:00
|
|
|
}
|
2011-12-09 12:06:06 -08:00
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
/*
|
|
|
|
* Perform a bounce-back animation if overscrolled, unless panning is being overridden
|
|
|
|
* (which happens e.g. when the user is panning an iframe).
|
|
|
|
*/
|
2012-01-10 07:05:57 -08:00
|
|
|
if (overscrolled) {
|
2011-12-07 13:33:55 -08:00
|
|
|
bounce();
|
2011-12-20 13:53:39 -08:00
|
|
|
} else {
|
2011-12-07 13:33:55 -08:00
|
|
|
finishAnimation();
|
2011-12-20 13:53:39 -08:00
|
|
|
mState = PanZoomState.NOTHING;
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:33:55 -08:00
|
|
|
private void finishAnimation() {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "Finishing animation at " + mController.getViewportMetrics());
|
2011-12-07 13:33:55 -08:00
|
|
|
stopAnimationTimer();
|
|
|
|
|
|
|
|
// Force a viewport synchronisation
|
|
|
|
mController.setForceRedraw();
|
|
|
|
mController.notifyLayerClientOfGeometryChange();
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
// Physics information for one axis (X or Y).
|
2011-12-07 10:44:02 -08:00
|
|
|
private abstract static class Axis {
|
2011-11-18 10:28:17 -08:00
|
|
|
public enum FlingStates {
|
|
|
|
STOPPED,
|
2011-12-01 11:27:50 -08:00
|
|
|
PANNING,
|
|
|
|
FLINGING,
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:57 -08:00
|
|
|
private enum Overscroll {
|
2011-11-18 10:28:17 -08:00
|
|
|
NONE,
|
|
|
|
MINUS, // Overscrolled in the negative direction
|
|
|
|
PLUS, // Overscrolled in the positive direction
|
|
|
|
BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen)
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:51 -08:00
|
|
|
private float firstTouchPos; /* Position of the first touch event on the current drag. */
|
2012-01-10 07:05:53 -08:00
|
|
|
private float touchPos; /* Position of the most recent touch event on the current drag. */
|
|
|
|
private float lastTouchPos; /* Position of the touch event before touchPos. */
|
2011-11-18 10:28:17 -08:00
|
|
|
public float velocity; /* Velocity in this direction. */
|
2011-11-30 03:32:49 -08:00
|
|
|
public boolean locked; /* Whether movement on this axis is locked. */
|
2011-12-06 07:13:14 -08:00
|
|
|
public boolean disableSnap; /* Whether overscroll snapping is disabled. */
|
2011-11-18 10:28:17 -08:00
|
|
|
|
|
|
|
private FlingStates mFlingState; /* The fling state we're in on this axis. */
|
|
|
|
|
2011-12-07 10:44:02 -08:00
|
|
|
public abstract float getOrigin();
|
|
|
|
protected abstract float getViewportLength();
|
|
|
|
protected abstract float getPageLength();
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2011-12-06 07:13:14 -08:00
|
|
|
public float displacement;
|
2011-12-07 10:41:58 -08:00
|
|
|
|
2012-01-10 07:05:55 -08:00
|
|
|
public Axis() {}
|
2011-12-06 07:13:14 -08:00
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
public FlingStates getFlingState() { return mFlingState; }
|
|
|
|
|
2011-11-30 03:32:49 -08:00
|
|
|
public void setFlingState(FlingStates aFlingState) {
|
|
|
|
mFlingState = aFlingState;
|
|
|
|
}
|
|
|
|
|
2011-12-07 10:44:02 -08:00
|
|
|
private float getViewportEnd() { return getOrigin() + getViewportLength(); }
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2012-01-10 07:05:48 -08:00
|
|
|
public void startTouch(float pos) {
|
|
|
|
velocity = 0.0f;
|
|
|
|
locked = false;
|
|
|
|
firstTouchPos = touchPos = lastTouchPos = pos;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:51 -08:00
|
|
|
float panDistance(float currentPos) {
|
|
|
|
return currentPos - firstTouchPos;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:53 -08:00
|
|
|
void saveTouchPos() {
|
|
|
|
lastTouchPos = touchPos;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:49 -08:00
|
|
|
void updateWithTouchAt(float pos, float timeDelta) {
|
|
|
|
float newVelocity = (touchPos - pos) / timeDelta * MS_PER_FRAME;
|
|
|
|
|
|
|
|
// If there's a direction change, or current velocity is very low,
|
|
|
|
// allow setting of the velocity outright. Otherwise, use the current
|
|
|
|
// velocity and a maximum change factor to set the new velocity.
|
|
|
|
boolean curVelocityIsLow = Math.abs(velocity) < 1.0f;
|
|
|
|
boolean directionChange = (velocity > 0) != (newVelocity > 0);
|
|
|
|
if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) {
|
|
|
|
velocity = newVelocity;
|
|
|
|
} else {
|
|
|
|
float maxChange = Math.abs(velocity * timeDelta * MAX_EVENT_ACCELERATION);
|
|
|
|
velocity = Math.min(velocity + maxChange, Math.max(velocity - maxChange, newVelocity));
|
|
|
|
}
|
|
|
|
|
|
|
|
touchPos = pos;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:57 -08:00
|
|
|
boolean overscrolled() {
|
|
|
|
return getOverscroll() != Overscroll.NONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
private Overscroll getOverscroll() {
|
2011-12-07 10:44:02 -08:00
|
|
|
boolean minus = (getOrigin() < 0.0f);
|
|
|
|
boolean plus = (getViewportEnd() > getPageLength());
|
2011-11-18 10:28:17 -08:00
|
|
|
if (minus && plus)
|
|
|
|
return Overscroll.BOTH;
|
|
|
|
else if (minus)
|
|
|
|
return Overscroll.MINUS;
|
|
|
|
else if (plus)
|
|
|
|
return Overscroll.PLUS;
|
|
|
|
else
|
|
|
|
return Overscroll.NONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the amount that the page has been overscrolled. If the page hasn't been
|
|
|
|
// overscrolled on this axis, returns 0.
|
2012-01-10 07:05:57 -08:00
|
|
|
private float getExcess() {
|
2011-11-18 10:28:17 -08:00
|
|
|
switch (getOverscroll()) {
|
2011-12-09 12:06:15 -08:00
|
|
|
case MINUS: return -getOrigin();
|
|
|
|
case PLUS: return getViewportEnd() - getPageLength();
|
|
|
|
case BOTH: return getViewportEnd() - getPageLength() - getOrigin();
|
2011-11-18 10:28:17 -08:00
|
|
|
default: return 0.0f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-08 21:31:04 -08:00
|
|
|
/*
|
|
|
|
* Returns true if the page is zoomed in to some degree along this axis such that scrolling
|
|
|
|
* is possible. Otherwise, returns false.
|
|
|
|
*/
|
|
|
|
private boolean scrollable() {
|
|
|
|
return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE;
|
|
|
|
}
|
|
|
|
|
2011-12-09 12:06:15 -08:00
|
|
|
/*
|
|
|
|
* Returns the resistance, as a multiplier, that should be taken into account when
|
|
|
|
* tracking or pinching.
|
|
|
|
*/
|
|
|
|
public float getEdgeResistance() {
|
2011-11-18 10:28:17 -08:00
|
|
|
float excess = getExcess();
|
2011-12-09 12:06:15 -08:00
|
|
|
return (excess > 0.0f) ? SNAP_LIMIT - excess / getViewportLength() : 1.0f;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2011-12-09 12:06:06 -08:00
|
|
|
/* Returns the velocity. If the axis is locked, returns 0. */
|
|
|
|
public float getRealVelocity() {
|
|
|
|
return locked ? 0.0f : velocity;
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
public void startFling(boolean stopped) {
|
2012-01-10 07:05:55 -08:00
|
|
|
if (stopped) {
|
|
|
|
setFlingState(FlingStates.STOPPED);
|
|
|
|
} else {
|
2011-12-01 11:27:50 -08:00
|
|
|
setFlingState(FlingStates.FLINGING);
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-07 13:34:15 -08:00
|
|
|
/* Advances a fling animation by one step. */
|
2012-01-10 07:05:57 -08:00
|
|
|
public boolean advanceFling() {
|
|
|
|
if (mFlingState != FlingStates.FLINGING) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
float excess = getExcess();
|
2011-12-06 07:13:14 -08:00
|
|
|
if (disableSnap || FloatUtils.fuzzyEquals(excess, 0.0f)) {
|
2012-01-10 07:05:57 -08:00
|
|
|
// If we aren't overscrolled, just apply friction.
|
2011-12-15 12:04:49 -08:00
|
|
|
if (Math.abs(velocity) >= VELOCITY_THRESHOLD) {
|
|
|
|
velocity *= FRICTION_FAST;
|
|
|
|
} else {
|
|
|
|
float t = velocity / VELOCITY_THRESHOLD;
|
|
|
|
velocity *= FloatUtils.interpolate(FRICTION_SLOW, FRICTION_FAST, t);
|
|
|
|
}
|
2012-01-10 07:05:57 -08:00
|
|
|
} else {
|
|
|
|
// Otherwise, decrease the velocity linearly.
|
|
|
|
float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT);
|
|
|
|
if (getOverscroll() == Overscroll.MINUS)
|
|
|
|
velocity = Math.min((velocity + OVERSCROLL_DECEL_RATE) * elasticity, 0.0f);
|
|
|
|
else // must be Overscroll.PLUS
|
|
|
|
velocity = Math.max((velocity - OVERSCROLL_DECEL_RATE) * elasticity, 0.0f);
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
2012-01-10 07:05:57 -08:00
|
|
|
return true;
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2012-01-10 07:05:57 -08:00
|
|
|
void stopFling() {
|
|
|
|
velocity = 0.0f;
|
|
|
|
setFlingState(FlingStates.STOPPED);
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Performs displacement of the viewport position according to the current velocity.
|
2012-01-04 10:25:11 -08:00
|
|
|
public void displace(boolean panningOverridden) {
|
|
|
|
if (!panningOverridden && (locked || !scrollable()))
|
2011-11-30 03:32:49 -08:00
|
|
|
return;
|
|
|
|
|
2011-12-01 11:27:50 -08:00
|
|
|
if (mFlingState == FlingStates.PANNING)
|
2011-12-09 12:06:15 -08:00
|
|
|
displacement += (lastTouchPos - touchPos) * getEdgeResistance();
|
2011-12-01 11:27:50 -08:00
|
|
|
else
|
2011-12-06 07:13:14 -08:00
|
|
|
displacement += velocity;
|
2011-11-30 03:32:49 -08:00
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2011-12-07 10:44:36 -08:00
|
|
|
/* Returns the nearest viewport metrics with no overscroll visible. */
|
|
|
|
private ViewportMetrics getValidViewportMetrics() {
|
|
|
|
ViewportMetrics viewportMetrics = new ViewportMetrics(mController.getViewportMetrics());
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "generating valid viewport using " + viewportMetrics);
|
2011-12-07 10:44:36 -08:00
|
|
|
|
|
|
|
/* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
|
|
|
|
float zoomFactor = viewportMetrics.getZoomFactor();
|
|
|
|
FloatSize pageSize = viewportMetrics.getPageSize();
|
|
|
|
RectF viewport = viewportMetrics.getViewport();
|
|
|
|
|
|
|
|
float minZoomFactor = 0.0f;
|
2011-12-09 19:57:57 -08:00
|
|
|
if (viewport.width() > pageSize.width && pageSize.width > 0) {
|
2011-12-07 10:44:36 -08:00
|
|
|
float scaleFactor = viewport.width() / pageSize.width;
|
2012-01-06 12:21:49 -08:00
|
|
|
minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
|
2011-12-07 10:44:36 -08:00
|
|
|
}
|
2011-12-09 19:57:57 -08:00
|
|
|
if (viewport.height() > pageSize.height && pageSize.height > 0) {
|
2011-12-07 10:44:36 -08:00
|
|
|
float scaleFactor = viewport.height() / pageSize.height;
|
2012-01-06 12:21:49 -08:00
|
|
|
minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
|
2011-12-07 10:44:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!FloatUtils.fuzzyEquals(minZoomFactor, 0.0f)) {
|
|
|
|
PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
|
|
|
|
viewportMetrics.scaleTo(minZoomFactor, center);
|
2011-12-09 09:03:19 -08:00
|
|
|
} else if (zoomFactor > MAX_ZOOM) {
|
|
|
|
PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
|
|
|
|
viewportMetrics.scaleTo(MAX_ZOOM, center);
|
2011-12-07 10:44:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Now we pan to the right origin. */
|
|
|
|
viewportMetrics.setViewport(viewportMetrics.getClampedViewport());
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "generated valid viewport as " + viewportMetrics);
|
2011-12-07 10:44:36 -08:00
|
|
|
|
|
|
|
return viewportMetrics;
|
|
|
|
}
|
|
|
|
|
2011-12-07 10:44:02 -08:00
|
|
|
private class AxisX extends Axis {
|
|
|
|
@Override
|
|
|
|
public float getOrigin() { return mController.getOrigin().x; }
|
|
|
|
@Override
|
|
|
|
protected float getViewportLength() { return mController.getViewportSize().width; }
|
|
|
|
@Override
|
|
|
|
protected float getPageLength() { return mController.getPageSize().width; }
|
|
|
|
}
|
|
|
|
|
|
|
|
private class AxisY extends Axis {
|
|
|
|
@Override
|
|
|
|
public float getOrigin() { return mController.getOrigin().y; }
|
|
|
|
@Override
|
|
|
|
protected float getViewportLength() { return mController.getViewportSize().height; }
|
|
|
|
@Override
|
|
|
|
protected float getPageLength() { return mController.getPageSize().height; }
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
/*
|
|
|
|
* Zooming
|
|
|
|
*/
|
2012-01-10 07:05:46 -08:00
|
|
|
@Override
|
|
|
|
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
|
|
|
Log.d(LOGTAG, "onScaleBegin in " + mState);
|
|
|
|
|
|
|
|
if (mState == PanZoomState.ANIMATED_ZOOM)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
mState = PanZoomState.PINCHING;
|
|
|
|
mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
|
|
|
|
GeckoApp.mAppContext.hidePluginViews();
|
|
|
|
GeckoApp.mAppContext.mAutoCompletePopup.hide();
|
|
|
|
cancelTouch();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
@Override
|
|
|
|
public boolean onScale(ScaleGestureDetector detector) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onScale in state " + mState);
|
|
|
|
|
2011-11-15 13:41:19 -08:00
|
|
|
if (mState == PanZoomState.ANIMATED_ZOOM)
|
|
|
|
return false;
|
|
|
|
|
2011-12-20 14:15:09 -08:00
|
|
|
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;
|
2011-12-09 12:06:15 -08:00
|
|
|
|
|
|
|
/*
|
|
|
|
* 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(), mY.getEdgeResistance());
|
|
|
|
if (spanRatio > 1.0f)
|
|
|
|
spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
|
|
|
|
else
|
|
|
|
spanRatio = 1.0f - (1.0f - spanRatio) * resistance;
|
|
|
|
|
2011-12-13 14:43:08 -08:00
|
|
|
synchronized (mController) {
|
|
|
|
float newZoomFactor = mController.getZoomFactor() * spanRatio;
|
2011-12-26 21:56:57 -08:00
|
|
|
if (newZoomFactor >= MAX_ZOOM) {
|
|
|
|
// apply resistance when zooming past MAX_ZOOM,
|
|
|
|
// such that it asymptotically reaches MAX_ZOOM + 1.0
|
|
|
|
// but never exceeds that
|
|
|
|
float excessZoom = newZoomFactor - MAX_ZOOM;
|
|
|
|
excessZoom = 1.0f - (float)Math.exp(-excessZoom);
|
|
|
|
newZoomFactor = MAX_ZOOM + excessZoom;
|
|
|
|
}
|
2011-11-23 11:07:47 -08:00
|
|
|
|
2011-12-13 14:43:08 -08:00
|
|
|
mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(),
|
|
|
|
mLastZoomFocus.y - detector.getFocusY()));
|
|
|
|
PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
|
|
|
|
mController.scaleWithFocus(newZoomFactor, focus);
|
|
|
|
}
|
2011-11-23 11:07:47 -08:00
|
|
|
|
|
|
|
mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
|
|
|
|
|
2011-11-18 10:28:17 -08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onScaleEnd(ScaleGestureDetector detector) {
|
2011-12-19 19:32:41 -08:00
|
|
|
Log.d(LOGTAG, "onScaleEnd in " + mState);
|
|
|
|
|
2011-11-15 13:41:19 -08:00
|
|
|
if (mState == PanZoomState.ANIMATED_ZOOM)
|
|
|
|
return;
|
|
|
|
|
2012-01-10 07:05:48 -08:00
|
|
|
// switch back to the touching state
|
|
|
|
startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
|
2011-11-18 10:28:17 -08:00
|
|
|
|
2011-12-07 13:34:52 -08:00
|
|
|
// Force a viewport synchronisation
|
|
|
|
mController.setForceRedraw();
|
|
|
|
mController.notifyLayerClientOfGeometryChange();
|
|
|
|
GeckoApp.mAppContext.showPluginViews();
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:46 -08:00
|
|
|
public boolean getRedrawHint() {
|
|
|
|
return (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING);
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:42 -08:00
|
|
|
private void sendPointToGecko(String event, MotionEvent motionEvent) {
|
2012-01-06 12:21:49 -08:00
|
|
|
String json;
|
2011-11-18 10:28:17 -08:00
|
|
|
try {
|
|
|
|
PointF point = new PointF(motionEvent.getX(), motionEvent.getY());
|
|
|
|
point = mController.convertViewPointToLayerPoint(point);
|
|
|
|
if (point == null) {
|
|
|
|
return;
|
|
|
|
}
|
2012-01-06 12:21:49 -08:00
|
|
|
json = PointUtils.toJSON(point).toString();
|
2012-01-10 07:05:42 -08:00
|
|
|
} catch (Exception e) {
|
|
|
|
Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e);
|
|
|
|
return;
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:42 -08:00
|
|
|
GeckoAppShell.sendEventToGecko(new GeckoEvent(event, json));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onLongPress(MotionEvent motionEvent) {
|
|
|
|
sendPointToGecko("Gesture:LongPress", motionEvent);
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|
2011-11-23 11:08:20 -08:00
|
|
|
|
2011-11-30 13:10:25 -08:00
|
|
|
@Override
|
|
|
|
public boolean onDown(MotionEvent motionEvent) {
|
2012-01-10 07:05:42 -08:00
|
|
|
sendPointToGecko("Gesture:ShowPress", motionEvent);
|
2011-11-30 13:10:25 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
|
2011-12-06 16:44:08 -08:00
|
|
|
GeckoApp.mAppContext.mAutoCompletePopup.hide();
|
2012-01-10 07:05:42 -08:00
|
|
|
sendPointToGecko("Gesture:SingleTap", motionEvent);
|
2011-11-30 13:10:25 -08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-11-15 13:41:19 -08:00
|
|
|
@Override
|
|
|
|
public boolean onDoubleTap(MotionEvent motionEvent) {
|
2012-01-10 07:05:42 -08:00
|
|
|
sendPointToGecko("Gesture:DoubleTap", motionEvent);
|
2011-11-15 13:41:19 -08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2012-01-10 07:05:46 -08:00
|
|
|
private void cancelTouch() {
|
|
|
|
GeckoEvent e = new GeckoEvent("Gesture:CancelTouch", "");
|
|
|
|
GeckoAppShell.sendEventToGecko(e);
|
|
|
|
}
|
|
|
|
|
2011-12-20 13:53:39 -08:00
|
|
|
private boolean animatedZoomTo(RectF zoomToRect) {
|
2011-11-15 13:41:19 -08:00
|
|
|
GeckoApp.mAppContext.hidePluginViews();
|
2011-12-21 17:03:21 -08:00
|
|
|
GeckoApp.mAppContext.mAutoCompletePopup.hide();
|
2011-11-15 13:41:19 -08:00
|
|
|
|
|
|
|
mState = PanZoomState.ANIMATED_ZOOM;
|
|
|
|
final float startZoom = mController.getZoomFactor();
|
|
|
|
final PointF startPoint = mController.getOrigin();
|
|
|
|
|
|
|
|
RectF viewport = mController.getViewport();
|
|
|
|
|
|
|
|
float newHeight = zoomToRect.width() * viewport.height() / viewport.width();
|
|
|
|
// if the requested rect would not fill the screen, shift it to be centered
|
|
|
|
if (zoomToRect.height() < newHeight) {
|
|
|
|
zoomToRect.top -= (newHeight - zoomToRect.height())/2;
|
|
|
|
zoomToRect.bottom = zoomToRect.top + newHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
zoomToRect = mController.restrictToPageSize(zoomToRect);
|
2011-12-07 13:34:52 -08:00
|
|
|
float finalZoom = viewport.width() * startZoom / zoomToRect.width();
|
2011-11-15 13:41:19 -08:00
|
|
|
|
2011-12-07 13:34:52 -08:00
|
|
|
ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics());
|
|
|
|
finalMetrics.setOrigin(new PointF(zoomToRect.left, zoomToRect.top));
|
|
|
|
finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
|
2011-11-15 13:41:19 -08:00
|
|
|
|
2011-12-07 13:34:52 -08:00
|
|
|
bounce(finalMetrics);
|
2011-11-15 13:41:19 -08:00
|
|
|
return true;
|
|
|
|
}
|
2011-11-18 10:28:17 -08:00
|
|
|
}
|