mobile/android/base/gfx/JavaPanZoomController.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko.gfx;
michael@0 7
michael@0 8 import org.mozilla.gecko.GeckoAppShell;
michael@0 9 import org.mozilla.gecko.GeckoEvent;
michael@0 10 import org.mozilla.gecko.PrefsHelper;
michael@0 11 import org.mozilla.gecko.Tab;
michael@0 12 import org.mozilla.gecko.Tabs;
michael@0 13 import org.mozilla.gecko.ZoomConstraints;
michael@0 14 import org.mozilla.gecko.EventDispatcher;
michael@0 15 import org.mozilla.gecko.util.FloatUtils;
michael@0 16 import org.mozilla.gecko.util.GamepadUtils;
michael@0 17 import org.mozilla.gecko.util.GeckoEventListener;
michael@0 18 import org.mozilla.gecko.util.ThreadUtils;
michael@0 19
michael@0 20 import org.json.JSONObject;
michael@0 21
michael@0 22 import android.graphics.PointF;
michael@0 23 import android.graphics.RectF;
michael@0 24 import android.os.Build;
michael@0 25 import android.util.FloatMath;
michael@0 26 import android.util.Log;
michael@0 27 import android.view.GestureDetector;
michael@0 28 import android.view.InputDevice;
michael@0 29 import android.view.KeyEvent;
michael@0 30 import android.view.MotionEvent;
michael@0 31 import android.view.View;
michael@0 32
michael@0 33 /*
michael@0 34 * Handles the kinetic scrolling and zooming physics for a layer controller.
michael@0 35 *
michael@0 36 * Many ideas are from Joe Hewitt's Scrollability:
michael@0 37 * https://github.com/joehewitt/scrollability/
michael@0 38 */
michael@0 39 class JavaPanZoomController
michael@0 40 extends GestureDetector.SimpleOnGestureListener
michael@0 41 implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener
michael@0 42 {
michael@0 43 private static final String LOGTAG = "GeckoPanZoomController";
michael@0 44
michael@0 45 private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect";
michael@0 46 private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth";
michael@0 47 private static String MESSAGE_TOUCH_LISTENER = "Tab:HasTouchListener";
michael@0 48
michael@0 49 // Animation stops if the velocity is below this value when overscrolled or panning.
michael@0 50 private static final float STOPPED_THRESHOLD = 4.0f;
michael@0 51
michael@0 52 // Animation stops is the velocity is below this threshold when flinging.
michael@0 53 private static final float FLING_STOPPED_THRESHOLD = 0.1f;
michael@0 54
michael@0 55 // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
michael@0 56 // between the touch-down and touch-up of a click). In units of density-independent pixels.
michael@0 57 public static final float PAN_THRESHOLD = 1/16f * GeckoAppShell.getDpi();
michael@0 58
michael@0 59 // Angle from axis within which we stay axis-locked
michael@0 60 private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
michael@0 61
michael@0 62 // Axis-lock breakout angle
michael@0 63 private static final double AXIS_BREAKOUT_ANGLE = Math.PI / 8.0;
michael@0 64
michael@0 65 // The distance the user has to pan before we consider breaking out of a locked axis
michael@0 66 public static final float AXIS_BREAKOUT_THRESHOLD = 1/32f * GeckoAppShell.getDpi();
michael@0 67
michael@0 68 // The maximum amount we allow you to zoom into a page
michael@0 69 private static final float MAX_ZOOM = 4.0f;
michael@0 70
michael@0 71 // The maximum amount we would like to scroll with the mouse
michael@0 72 private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
michael@0 73
michael@0 74 // The maximum zoom factor adjustment per frame of the AUTONAV animation
michael@0 75 private static final float MAX_ZOOM_DELTA = 0.125f;
michael@0 76
michael@0 77 // The duration of the bounce animation in ns
michael@0 78 private static final int BOUNCE_ANIMATION_DURATION = 250000000;
michael@0 79
michael@0 80 private enum PanZoomState {
michael@0 81 NOTHING, /* no touch-start events received */
michael@0 82 FLING, /* all touches removed, but we're still scrolling page */
michael@0 83 TOUCHING, /* one touch-start event received */
michael@0 84 PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis lock) X axis */
michael@0 85 PANNING_LOCKED_Y, /* as above for Y axis */
michael@0 86 PANNING, /* panning without axis lock */
michael@0 87 PANNING_HOLD, /* in panning, but not moving.
michael@0 88 * similar to TOUCHING but after starting a pan */
michael@0 89 PANNING_HOLD_LOCKED_X, /* like PANNING_HOLD, but axis lock still in effect for X axis */
michael@0 90 PANNING_HOLD_LOCKED_Y, /* as above but for Y axis */
michael@0 91 PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
michael@0 92 ANIMATED_ZOOM, /* animated zoom to a new rect */
michael@0 93 BOUNCE, /* in a bounce animation */
michael@0 94 WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
michael@0 95 put a finger down, but we don't yet know if a touch listener has
michael@0 96 prevented the default actions yet. we still need to abort animations. */
michael@0 97 AUTONAV, /* We are scrolling using an AutonavRunnable animation. This is similar
michael@0 98 to the FLING state except that it must be stopped manually by the code that
michael@0 99 started it, and it's velocity can be updated while it's running. */
michael@0 100 }
michael@0 101
michael@0 102 private enum AxisLockMode {
michael@0 103 STANDARD, /* Default axis locking mode that doesn't break out until finger release */
michael@0 104 FREE, /* No locking at all */
michael@0 105 STICKY /* Break out with hysteresis so that it feels as free as possible whilst locking */
michael@0 106 }
michael@0 107
michael@0 108 private final PanZoomTarget mTarget;
michael@0 109 private final SubdocumentScrollHelper mSubscroller;
michael@0 110 private final Axis mX;
michael@0 111 private final Axis mY;
michael@0 112 private final TouchEventHandler mTouchEventHandler;
michael@0 113 private final EventDispatcher mEventDispatcher;
michael@0 114
michael@0 115 /* The task that handles flings, autonav or bounces. */
michael@0 116 private PanZoomRenderTask mAnimationRenderTask;
michael@0 117 /* The zoom focus at the first zoom event (in page coordinates). */
michael@0 118 private PointF mLastZoomFocus;
michael@0 119 /* The time the last motion event took place. */
michael@0 120 private long mLastEventTime;
michael@0 121 /* Current state the pan/zoom UI is in. */
michael@0 122 private PanZoomState mState;
michael@0 123 /* The per-frame zoom delta for the currently-running AUTONAV animation. */
michael@0 124 private float mAutonavZoomDelta;
michael@0 125 /* The user selected panning mode */
michael@0 126 private AxisLockMode mMode;
michael@0 127 /* A medium-length tap/press is happening */
michael@0 128 private boolean mMediumPress;
michael@0 129 /* Used to change the scrollY direction */
michael@0 130 private boolean mNegateWheelScrollY;
michael@0 131 /* Whether the current event has been default-prevented. */
michael@0 132 private boolean mDefaultPrevented;
michael@0 133
michael@0 134 // Handler to be notified when overscroll occurs
michael@0 135 private Overscroll mOverscroll;
michael@0 136
michael@0 137 public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) {
michael@0 138 mTarget = target;
michael@0 139 mSubscroller = new SubdocumentScrollHelper(eventDispatcher);
michael@0 140 mX = new AxisX(mSubscroller);
michael@0 141 mY = new AxisY(mSubscroller);
michael@0 142 mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this);
michael@0 143
michael@0 144 checkMainThread();
michael@0 145
michael@0 146 setState(PanZoomState.NOTHING);
michael@0 147
michael@0 148 mEventDispatcher = eventDispatcher;
michael@0 149 registerEventListener(MESSAGE_ZOOM_RECT);
michael@0 150 registerEventListener(MESSAGE_ZOOM_PAGE);
michael@0 151 registerEventListener(MESSAGE_TOUCH_LISTENER);
michael@0 152
michael@0 153 mMode = AxisLockMode.STANDARD;
michael@0 154
michael@0 155 String[] prefs = { "ui.scrolling.axis_lock_mode",
michael@0 156 "ui.scrolling.negate_wheel_scrollY",
michael@0 157 "ui.scrolling.gamepad_dead_zone" };
michael@0 158 mNegateWheelScrollY = false;
michael@0 159 PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
michael@0 160 @Override public void prefValue(String pref, String value) {
michael@0 161 if (pref.equals("ui.scrolling.axis_lock_mode")) {
michael@0 162 if (value.equals("standard")) {
michael@0 163 mMode = AxisLockMode.STANDARD;
michael@0 164 } else if (value.equals("free")) {
michael@0 165 mMode = AxisLockMode.FREE;
michael@0 166 } else {
michael@0 167 mMode = AxisLockMode.STICKY;
michael@0 168 }
michael@0 169 }
michael@0 170 }
michael@0 171
michael@0 172 @Override public void prefValue(String pref, int value) {
michael@0 173 if (pref.equals("ui.scrolling.gamepad_dead_zone")) {
michael@0 174 GamepadUtils.overrideDeadZoneThreshold((float)value / 1000f);
michael@0 175 }
michael@0 176 }
michael@0 177
michael@0 178 @Override public void prefValue(String pref, boolean value) {
michael@0 179 if (pref.equals("ui.scrolling.negate_wheel_scrollY")) {
michael@0 180 mNegateWheelScrollY = value;
michael@0 181 }
michael@0 182 }
michael@0 183
michael@0 184 @Override
michael@0 185 public boolean isObserver() {
michael@0 186 return true;
michael@0 187 }
michael@0 188
michael@0 189 });
michael@0 190
michael@0 191 Axis.initPrefs();
michael@0 192 }
michael@0 193
michael@0 194 @Override
michael@0 195 public void destroy() {
michael@0 196 unregisterEventListener(MESSAGE_ZOOM_RECT);
michael@0 197 unregisterEventListener(MESSAGE_ZOOM_PAGE);
michael@0 198 unregisterEventListener(MESSAGE_TOUCH_LISTENER);
michael@0 199 mSubscroller.destroy();
michael@0 200 mTouchEventHandler.destroy();
michael@0 201 }
michael@0 202
michael@0 203 private final static float easeOut(float t) {
michael@0 204 // ease-out approx.
michael@0 205 // -(t-1)^2+1
michael@0 206 t = t-1;
michael@0 207 return -t*t+1;
michael@0 208 }
michael@0 209
michael@0 210 private void registerEventListener(String event) {
michael@0 211 mEventDispatcher.registerEventListener(event, this);
michael@0 212 }
michael@0 213
michael@0 214 private void unregisterEventListener(String event) {
michael@0 215 mEventDispatcher.unregisterEventListener(event, this);
michael@0 216 }
michael@0 217
michael@0 218 private void setState(PanZoomState state) {
michael@0 219 if (state != mState) {
michael@0 220 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PanZoom:StateChange", state.toString()));
michael@0 221 mState = state;
michael@0 222
michael@0 223 // Let the target know we've finished with it (for now)
michael@0 224 if (state == PanZoomState.NOTHING) {
michael@0 225 mTarget.panZoomStopped();
michael@0 226 }
michael@0 227 }
michael@0 228 }
michael@0 229
michael@0 230 private ImmutableViewportMetrics getMetrics() {
michael@0 231 return mTarget.getViewportMetrics();
michael@0 232 }
michael@0 233
michael@0 234 private void checkMainThread() {
michael@0 235 if (!ThreadUtils.isOnUiThread()) {
michael@0 236 // log with full stack trace
michael@0 237 Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
michael@0 238 }
michael@0 239 }
michael@0 240
michael@0 241 @Override
michael@0 242 public void handleMessage(String event, JSONObject message) {
michael@0 243 try {
michael@0 244 if (MESSAGE_ZOOM_RECT.equals(event)) {
michael@0 245 float x = (float)message.getDouble("x");
michael@0 246 float y = (float)message.getDouble("y");
michael@0 247 final RectF zoomRect = new RectF(x, y,
michael@0 248 x + (float)message.getDouble("w"),
michael@0 249 y + (float)message.getDouble("h"));
michael@0 250 if (message.optBoolean("animate", true)) {
michael@0 251 mTarget.post(new Runnable() {
michael@0 252 @Override
michael@0 253 public void run() {
michael@0 254 animatedZoomTo(zoomRect);
michael@0 255 }
michael@0 256 });
michael@0 257 } else {
michael@0 258 mTarget.setViewportMetrics(getMetricsToZoomTo(zoomRect));
michael@0 259 }
michael@0 260 } else if (MESSAGE_ZOOM_PAGE.equals(event)) {
michael@0 261 ImmutableViewportMetrics metrics = getMetrics();
michael@0 262 RectF cssPageRect = metrics.getCssPageRect();
michael@0 263
michael@0 264 RectF viewableRect = metrics.getCssViewport();
michael@0 265 float y = viewableRect.top;
michael@0 266 // attempt to keep zoom keep focused on the center of the viewport
michael@0 267 float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width();
michael@0 268 float dh = viewableRect.height() - newHeight; // increase in the height
michael@0 269 final RectF r = new RectF(0.0f,
michael@0 270 y + dh/2,
michael@0 271 cssPageRect.width(),
michael@0 272 y + dh/2 + newHeight);
michael@0 273 if (message.optBoolean("animate", true)) {
michael@0 274 mTarget.post(new Runnable() {
michael@0 275 @Override
michael@0 276 public void run() {
michael@0 277 animatedZoomTo(r);
michael@0 278 }
michael@0 279 });
michael@0 280 } else {
michael@0 281 mTarget.setViewportMetrics(getMetricsToZoomTo(r));
michael@0 282 }
michael@0 283 } else if (MESSAGE_TOUCH_LISTENER.equals(event)) {
michael@0 284 int tabId = message.getInt("tabID");
michael@0 285 final Tab tab = Tabs.getInstance().getTab(tabId);
michael@0 286 tab.setHasTouchListeners(true);
michael@0 287 mTarget.post(new Runnable() {
michael@0 288 @Override
michael@0 289 public void run() {
michael@0 290 if (Tabs.getInstance().isSelectedTab(tab))
michael@0 291 mTouchEventHandler.setWaitForTouchListeners(true);
michael@0 292 }
michael@0 293 });
michael@0 294 }
michael@0 295 } catch (Exception e) {
michael@0 296 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
michael@0 297 }
michael@0 298 }
michael@0 299
michael@0 300 /** This function MUST be called on the UI thread */
michael@0 301 @Override
michael@0 302 public boolean onKeyEvent(KeyEvent event) {
michael@0 303 if (Build.VERSION.SDK_INT <= 11) {
michael@0 304 return false;
michael@0 305 }
michael@0 306
michael@0 307 if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
michael@0 308 && event.getAction() == KeyEvent.ACTION_DOWN) {
michael@0 309
michael@0 310 switch (event.getKeyCode()) {
michael@0 311 case KeyEvent.KEYCODE_ZOOM_IN:
michael@0 312 return animatedScale(0.2f);
michael@0 313 case KeyEvent.KEYCODE_ZOOM_OUT:
michael@0 314 return animatedScale(-0.2f);
michael@0 315 }
michael@0 316 }
michael@0 317 return false;
michael@0 318 }
michael@0 319
michael@0 320 /** This function MUST be called on the UI thread */
michael@0 321 @Override
michael@0 322 public boolean onMotionEvent(MotionEvent event) {
michael@0 323 if (Build.VERSION.SDK_INT <= 11) {
michael@0 324 return false;
michael@0 325 }
michael@0 326
michael@0 327 switch (event.getSource() & InputDevice.SOURCE_CLASS_MASK) {
michael@0 328 case InputDevice.SOURCE_CLASS_POINTER:
michael@0 329 switch (event.getAction() & MotionEvent.ACTION_MASK) {
michael@0 330 case MotionEvent.ACTION_SCROLL: return handlePointerScroll(event);
michael@0 331 }
michael@0 332 break;
michael@0 333 case InputDevice.SOURCE_CLASS_JOYSTICK:
michael@0 334 switch (event.getAction() & MotionEvent.ACTION_MASK) {
michael@0 335 case MotionEvent.ACTION_MOVE: return handleJoystickNav(event);
michael@0 336 }
michael@0 337 break;
michael@0 338 }
michael@0 339 return false;
michael@0 340 }
michael@0 341
michael@0 342 /** This function MUST be called on the UI thread */
michael@0 343 @Override
michael@0 344 public boolean onTouchEvent(MotionEvent event) {
michael@0 345 return mTouchEventHandler.handleEvent(event);
michael@0 346 }
michael@0 347
michael@0 348 boolean handleEvent(MotionEvent event, boolean defaultPrevented) {
michael@0 349 mDefaultPrevented = defaultPrevented;
michael@0 350
michael@0 351 switch (event.getAction() & MotionEvent.ACTION_MASK) {
michael@0 352 case MotionEvent.ACTION_DOWN: return handleTouchStart(event);
michael@0 353 case MotionEvent.ACTION_MOVE: return handleTouchMove(event);
michael@0 354 case MotionEvent.ACTION_UP: return handleTouchEnd(event);
michael@0 355 case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event);
michael@0 356 }
michael@0 357 return false;
michael@0 358 }
michael@0 359
michael@0 360 /** This function MUST be called on the UI thread */
michael@0 361 @Override
michael@0 362 public void notifyDefaultActionPrevented(boolean prevented) {
michael@0 363 mTouchEventHandler.handleEventListenerAction(!prevented);
michael@0 364 }
michael@0 365
michael@0 366 /** This function must be called from the UI thread. */
michael@0 367 @Override
michael@0 368 public void abortAnimation() {
michael@0 369 checkMainThread();
michael@0 370 // this happens when gecko changes the viewport on us or if the device is rotated.
michael@0 371 // if that's the case, abort any animation in progress and re-zoom so that the page
michael@0 372 // snaps to edges. for other cases (where the user's finger(s) are down) don't do
michael@0 373 // anything special.
michael@0 374 switch (mState) {
michael@0 375 case FLING:
michael@0 376 mX.stopFling();
michael@0 377 mY.stopFling();
michael@0 378 // fall through
michael@0 379 case BOUNCE:
michael@0 380 case ANIMATED_ZOOM:
michael@0 381 // the zoom that's in progress likely makes no sense any more (such as if
michael@0 382 // the screen orientation changed) so abort it
michael@0 383 setState(PanZoomState.NOTHING);
michael@0 384 // fall through
michael@0 385 case NOTHING:
michael@0 386 // Don't do animations here; they're distracting and can cause flashes on page
michael@0 387 // transitions.
michael@0 388 synchronized (mTarget.getLock()) {
michael@0 389 mTarget.setViewportMetrics(getValidViewportMetrics());
michael@0 390 mTarget.forceRedraw(null);
michael@0 391 }
michael@0 392 break;
michael@0 393 }
michael@0 394 }
michael@0 395
michael@0 396 /** This function must be called on the UI thread. */
michael@0 397 public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
michael@0 398 checkMainThread();
michael@0 399 mSubscroller.cancel();
michael@0 400 if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
michael@0 401 // this is the first touch point going down, so we enter the pending state
michael@0 402 // seting the state will kill any animations in progress, possibly leaving
michael@0 403 // the page in overscroll
michael@0 404 setState(PanZoomState.WAITING_LISTENERS);
michael@0 405 }
michael@0 406 }
michael@0 407
michael@0 408 /** This must be called on the UI thread. */
michael@0 409 @Override
michael@0 410 public void pageRectUpdated() {
michael@0 411 if (mState == PanZoomState.NOTHING) {
michael@0 412 synchronized (mTarget.getLock()) {
michael@0 413 ImmutableViewportMetrics validated = getValidViewportMetrics();
michael@0 414 if (!getMetrics().fuzzyEquals(validated)) {
michael@0 415 // page size changed such that we are now in overscroll. snap to the
michael@0 416 // the nearest valid viewport
michael@0 417 mTarget.setViewportMetrics(validated);
michael@0 418 }
michael@0 419 }
michael@0 420 }
michael@0 421 }
michael@0 422
michael@0 423 /*
michael@0 424 * Panning/scrolling
michael@0 425 */
michael@0 426
michael@0 427 private boolean handleTouchStart(MotionEvent event) {
michael@0 428 // user is taking control of movement, so stop
michael@0 429 // any auto-movement we have going
michael@0 430 stopAnimationTask();
michael@0 431
michael@0 432 switch (mState) {
michael@0 433 case ANIMATED_ZOOM:
michael@0 434 // We just interrupted a double-tap animation, so force a redraw in
michael@0 435 // case this touchstart is just a tap that doesn't end up triggering
michael@0 436 // a redraw
michael@0 437 mTarget.forceRedraw(null);
michael@0 438 // fall through
michael@0 439 case FLING:
michael@0 440 case AUTONAV:
michael@0 441 case BOUNCE:
michael@0 442 case NOTHING:
michael@0 443 case WAITING_LISTENERS:
michael@0 444 startTouch(event.getX(0), event.getY(0), event.getEventTime());
michael@0 445 return false;
michael@0 446 case TOUCHING:
michael@0 447 case PANNING:
michael@0 448 case PANNING_LOCKED_X:
michael@0 449 case PANNING_LOCKED_Y:
michael@0 450 case PANNING_HOLD:
michael@0 451 case PANNING_HOLD_LOCKED_X:
michael@0 452 case PANNING_HOLD_LOCKED_Y:
michael@0 453 case PINCHING:
michael@0 454 Log.e(LOGTAG, "Received impossible touch down while in " + mState);
michael@0 455 return false;
michael@0 456 }
michael@0 457 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart");
michael@0 458 return false;
michael@0 459 }
michael@0 460
michael@0 461 private boolean handleTouchMove(MotionEvent event) {
michael@0 462
michael@0 463 switch (mState) {
michael@0 464 case FLING:
michael@0 465 case AUTONAV:
michael@0 466 case BOUNCE:
michael@0 467 case WAITING_LISTENERS:
michael@0 468 // should never happen
michael@0 469 Log.e(LOGTAG, "Received impossible touch move while in " + mState);
michael@0 470 // fall through
michael@0 471 case ANIMATED_ZOOM:
michael@0 472 case NOTHING:
michael@0 473 // may happen if user double-taps and drags without lifting after the
michael@0 474 // second tap. ignore the move if this happens.
michael@0 475 return false;
michael@0 476
michael@0 477 case TOUCHING:
michael@0 478 // Don't allow panning if there is an element in full-screen mode. See bug 775511.
michael@0 479 if ((mTarget.isFullScreen() && !mSubscroller.scrolling()) || panDistance(event) < PAN_THRESHOLD) {
michael@0 480 return false;
michael@0 481 }
michael@0 482 cancelTouch();
michael@0 483 startPanning(event.getX(0), event.getY(0), event.getEventTime());
michael@0 484 track(event);
michael@0 485 return true;
michael@0 486
michael@0 487 case PANNING_HOLD_LOCKED_X:
michael@0 488 setState(PanZoomState.PANNING_LOCKED_X);
michael@0 489 track(event);
michael@0 490 return true;
michael@0 491 case PANNING_HOLD_LOCKED_Y:
michael@0 492 setState(PanZoomState.PANNING_LOCKED_Y);
michael@0 493 // fall through
michael@0 494 case PANNING_LOCKED_X:
michael@0 495 case PANNING_LOCKED_Y:
michael@0 496 track(event);
michael@0 497 return true;
michael@0 498
michael@0 499 case PANNING_HOLD:
michael@0 500 setState(PanZoomState.PANNING);
michael@0 501 // fall through
michael@0 502 case PANNING:
michael@0 503 track(event);
michael@0 504 return true;
michael@0 505
michael@0 506 case PINCHING:
michael@0 507 // scale gesture listener will handle this
michael@0 508 return false;
michael@0 509 }
michael@0 510 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove");
michael@0 511 return false;
michael@0 512 }
michael@0 513
michael@0 514 private boolean handleTouchEnd(MotionEvent event) {
michael@0 515
michael@0 516 switch (mState) {
michael@0 517 case FLING:
michael@0 518 case AUTONAV:
michael@0 519 case BOUNCE:
michael@0 520 case ANIMATED_ZOOM:
michael@0 521 case NOTHING:
michael@0 522 // may happen if user double-taps and drags without lifting after the
michael@0 523 // second tap. ignore if this happens.
michael@0 524 return false;
michael@0 525
michael@0 526 case WAITING_LISTENERS:
michael@0 527 if (!mDefaultPrevented) {
michael@0 528 // should never happen
michael@0 529 Log.e(LOGTAG, "Received impossible touch end while in " + mState);
michael@0 530 }
michael@0 531 // fall through
michael@0 532 case TOUCHING:
michael@0 533 // the switch into TOUCHING might have happened while the page was
michael@0 534 // snapping back after overscroll. we need to finish the snap if that
michael@0 535 // was the case
michael@0 536 bounce();
michael@0 537 return false;
michael@0 538
michael@0 539 case PANNING:
michael@0 540 case PANNING_LOCKED_X:
michael@0 541 case PANNING_LOCKED_Y:
michael@0 542 case PANNING_HOLD:
michael@0 543 case PANNING_HOLD_LOCKED_X:
michael@0 544 case PANNING_HOLD_LOCKED_Y:
michael@0 545 setState(PanZoomState.FLING);
michael@0 546 fling();
michael@0 547 return true;
michael@0 548
michael@0 549 case PINCHING:
michael@0 550 setState(PanZoomState.NOTHING);
michael@0 551 return true;
michael@0 552 }
michael@0 553 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd");
michael@0 554 return false;
michael@0 555 }
michael@0 556
michael@0 557 private boolean handleTouchCancel(MotionEvent event) {
michael@0 558 cancelTouch();
michael@0 559
michael@0 560 // ensure we snap back if we're overscrolled
michael@0 561 bounce();
michael@0 562 return false;
michael@0 563 }
michael@0 564
michael@0 565 private boolean handlePointerScroll(MotionEvent event) {
michael@0 566 if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
michael@0 567 float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
michael@0 568 float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
michael@0 569 if (mNegateWheelScrollY) {
michael@0 570 scrollY *= -1.0;
michael@0 571 }
michael@0 572 scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
michael@0 573 bounce();
michael@0 574 return true;
michael@0 575 }
michael@0 576 return false;
michael@0 577 }
michael@0 578
michael@0 579 private float filterDeadZone(MotionEvent event, int axis) {
michael@0 580 return (GamepadUtils.isValueInDeadZone(event, axis) ? 0 : event.getAxisValue(axis));
michael@0 581 }
michael@0 582
michael@0 583 private float normalizeJoystickScroll(MotionEvent event, int axis) {
michael@0 584 return filterDeadZone(event, axis) * MAX_SCROLL;
michael@0 585 }
michael@0 586
michael@0 587 private float normalizeJoystickZoom(MotionEvent event, int axis) {
michael@0 588 // negate MAX_ZOOM_DELTA so that pushing up on the stick zooms in
michael@0 589 return filterDeadZone(event, axis) * -MAX_ZOOM_DELTA;
michael@0 590 }
michael@0 591
michael@0 592 // Since this event is a position-based event rather than a motion-based event, we need to
michael@0 593 // set up an AUTONAV animation to keep scrolling even while we don't get events.
michael@0 594 private boolean handleJoystickNav(MotionEvent event) {
michael@0 595 float velocityX = normalizeJoystickScroll(event, MotionEvent.AXIS_X);
michael@0 596 float velocityY = normalizeJoystickScroll(event, MotionEvent.AXIS_Y);
michael@0 597 float zoomDelta = normalizeJoystickZoom(event, MotionEvent.AXIS_RZ);
michael@0 598
michael@0 599 if (velocityX == 0 && velocityY == 0 && zoomDelta == 0) {
michael@0 600 if (mState == PanZoomState.AUTONAV) {
michael@0 601 bounce(); // if not needed, this will automatically go to state NOTHING
michael@0 602 return true;
michael@0 603 }
michael@0 604 return false;
michael@0 605 }
michael@0 606
michael@0 607 if (mState == PanZoomState.NOTHING) {
michael@0 608 setState(PanZoomState.AUTONAV);
michael@0 609 startAnimationRenderTask(new AutonavRenderTask());
michael@0 610 }
michael@0 611 if (mState == PanZoomState.AUTONAV) {
michael@0 612 mX.setAutoscrollVelocity(velocityX);
michael@0 613 mY.setAutoscrollVelocity(velocityY);
michael@0 614 mAutonavZoomDelta = zoomDelta;
michael@0 615 return true;
michael@0 616 }
michael@0 617 return false;
michael@0 618 }
michael@0 619
michael@0 620 private void startTouch(float x, float y, long time) {
michael@0 621 mX.startTouch(x);
michael@0 622 mY.startTouch(y);
michael@0 623 setState(PanZoomState.TOUCHING);
michael@0 624 mLastEventTime = time;
michael@0 625 }
michael@0 626
michael@0 627 private void startPanning(float x, float y, long time) {
michael@0 628 float dx = mX.panDistance(x);
michael@0 629 float dy = mY.panDistance(y);
michael@0 630 double angle = Math.atan2(dy, dx); // range [-pi, pi]
michael@0 631 angle = Math.abs(angle); // range [0, pi]
michael@0 632
michael@0 633 // When the touch move breaks through the pan threshold, reposition the touch down origin
michael@0 634 // so the page won't jump when we start panning.
michael@0 635 mX.startTouch(x);
michael@0 636 mY.startTouch(y);
michael@0 637 mLastEventTime = time;
michael@0 638
michael@0 639 if (mMode == AxisLockMode.STANDARD || mMode == AxisLockMode.STICKY) {
michael@0 640 if (!mX.scrollable() || !mY.scrollable()) {
michael@0 641 setState(PanZoomState.PANNING);
michael@0 642 } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
michael@0 643 mY.setScrollingDisabled(true);
michael@0 644 setState(PanZoomState.PANNING_LOCKED_X);
michael@0 645 } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
michael@0 646 mX.setScrollingDisabled(true);
michael@0 647 setState(PanZoomState.PANNING_LOCKED_Y);
michael@0 648 } else {
michael@0 649 setState(PanZoomState.PANNING);
michael@0 650 }
michael@0 651 } else if (mMode == AxisLockMode.FREE) {
michael@0 652 setState(PanZoomState.PANNING);
michael@0 653 }
michael@0 654 }
michael@0 655
michael@0 656 private float panDistance(MotionEvent move) {
michael@0 657 float dx = mX.panDistance(move.getX(0));
michael@0 658 float dy = mY.panDistance(move.getY(0));
michael@0 659 return FloatMath.sqrt(dx * dx + dy * dy);
michael@0 660 }
michael@0 661
michael@0 662 private void track(float x, float y, long time) {
michael@0 663 float timeDelta = (float)(time - mLastEventTime);
michael@0 664 if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
michael@0 665 // probably a duplicate event, ignore it. using a zero timeDelta will mess
michael@0 666 // up our velocity
michael@0 667 return;
michael@0 668 }
michael@0 669 mLastEventTime = time;
michael@0 670
michael@0 671
michael@0 672 // if we're axis-locked check if the user is trying to scroll away from the lock
michael@0 673 if (mMode == AxisLockMode.STICKY) {
michael@0 674 float dx = mX.panDistance(x);
michael@0 675 float dy = mY.panDistance(y);
michael@0 676 double angle = Math.atan2(dy, dx); // range [-pi, pi]
michael@0 677 angle = Math.abs(angle); // range [0, pi]
michael@0 678
michael@0 679 if (Math.abs(dx) > AXIS_BREAKOUT_THRESHOLD || Math.abs(dy) > AXIS_BREAKOUT_THRESHOLD) {
michael@0 680 if (mState == PanZoomState.PANNING_LOCKED_X) {
michael@0 681 if (angle > AXIS_BREAKOUT_ANGLE && angle < (Math.PI - AXIS_BREAKOUT_ANGLE)) {
michael@0 682 mY.setScrollingDisabled(false);
michael@0 683 setState(PanZoomState.PANNING);
michael@0 684 }
michael@0 685 } else if (mState == PanZoomState.PANNING_LOCKED_Y) {
michael@0 686 if (Math.abs(angle - (Math.PI / 2)) > AXIS_BREAKOUT_ANGLE) {
michael@0 687 mX.setScrollingDisabled(false);
michael@0 688 setState(PanZoomState.PANNING);
michael@0 689 }
michael@0 690 }
michael@0 691 }
michael@0 692 }
michael@0 693
michael@0 694 mX.updateWithTouchAt(x, timeDelta);
michael@0 695 mY.updateWithTouchAt(y, timeDelta);
michael@0 696 }
michael@0 697
michael@0 698 private void track(MotionEvent event) {
michael@0 699 mX.saveTouchPos();
michael@0 700 mY.saveTouchPos();
michael@0 701
michael@0 702 for (int i = 0; i < event.getHistorySize(); i++) {
michael@0 703 track(event.getHistoricalX(0, i),
michael@0 704 event.getHistoricalY(0, i),
michael@0 705 event.getHistoricalEventTime(i));
michael@0 706 }
michael@0 707 track(event.getX(0), event.getY(0), event.getEventTime());
michael@0 708
michael@0 709 if (stopped()) {
michael@0 710 if (mState == PanZoomState.PANNING) {
michael@0 711 setState(PanZoomState.PANNING_HOLD);
michael@0 712 } else if (mState == PanZoomState.PANNING_LOCKED_X) {
michael@0 713 setState(PanZoomState.PANNING_HOLD_LOCKED_X);
michael@0 714 } else if (mState == PanZoomState.PANNING_LOCKED_Y) {
michael@0 715 setState(PanZoomState.PANNING_HOLD_LOCKED_Y);
michael@0 716 } else {
michael@0 717 // should never happen, but handle anyway for robustness
michael@0 718 Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
michael@0 719 setState(PanZoomState.PANNING_HOLD);
michael@0 720 }
michael@0 721 }
michael@0 722
michael@0 723 mX.startPan();
michael@0 724 mY.startPan();
michael@0 725 updatePosition();
michael@0 726 }
michael@0 727
michael@0 728 private void scrollBy(float dx, float dy) {
michael@0 729 mTarget.scrollBy(dx, dy);
michael@0 730 }
michael@0 731
michael@0 732 private void fling() {
michael@0 733 updatePosition();
michael@0 734
michael@0 735 stopAnimationTask();
michael@0 736
michael@0 737 boolean stopped = stopped();
michael@0 738 mX.startFling(stopped);
michael@0 739 mY.startFling(stopped);
michael@0 740
michael@0 741 startAnimationRenderTask(new FlingRenderTask());
michael@0 742 }
michael@0 743
michael@0 744 /* Performs a bounce-back animation to the given viewport metrics. */
michael@0 745 private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
michael@0 746 stopAnimationTask();
michael@0 747
michael@0 748 ImmutableViewportMetrics bounceStartMetrics = getMetrics();
michael@0 749 if (bounceStartMetrics.fuzzyEquals(metrics)) {
michael@0 750 setState(PanZoomState.NOTHING);
michael@0 751 return;
michael@0 752 }
michael@0 753
michael@0 754 setState(state);
michael@0 755
michael@0 756 // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
michael@0 757 // getRedrawHint() is returning false. This means we can safely call
michael@0 758 // setAnimationTarget to set the new final display port and not have it get
michael@0 759 // clobbered by display ports from intermediate animation frames.
michael@0 760 mTarget.setAnimationTarget(metrics);
michael@0 761 startAnimationRenderTask(new BounceRenderTask(bounceStartMetrics, metrics));
michael@0 762 }
michael@0 763
michael@0 764 /* Performs a bounce-back animation to the nearest valid viewport metrics. */
michael@0 765 private void bounce() {
michael@0 766 bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
michael@0 767 }
michael@0 768
michael@0 769 /* Starts the fling or bounce animation. */
michael@0 770 private void startAnimationRenderTask(final PanZoomRenderTask task) {
michael@0 771 if (mAnimationRenderTask != null) {
michael@0 772 Log.e(LOGTAG, "Attempted to start a new task without canceling the old one!");
michael@0 773 stopAnimationTask();
michael@0 774 }
michael@0 775
michael@0 776 mAnimationRenderTask = task;
michael@0 777 mTarget.postRenderTask(mAnimationRenderTask);
michael@0 778 }
michael@0 779
michael@0 780 /* Stops the fling or bounce animation. */
michael@0 781 private void stopAnimationTask() {
michael@0 782 if (mAnimationRenderTask != null) {
michael@0 783 mAnimationRenderTask.terminate();
michael@0 784 mTarget.removeRenderTask(mAnimationRenderTask);
michael@0 785 mAnimationRenderTask = null;
michael@0 786 }
michael@0 787 }
michael@0 788
michael@0 789 private float getVelocity() {
michael@0 790 float xvel = mX.getRealVelocity();
michael@0 791 float yvel = mY.getRealVelocity();
michael@0 792 return FloatMath.sqrt(xvel * xvel + yvel * yvel);
michael@0 793 }
michael@0 794
michael@0 795 @Override
michael@0 796 public PointF getVelocityVector() {
michael@0 797 return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
michael@0 798 }
michael@0 799
michael@0 800 private boolean stopped() {
michael@0 801 return getVelocity() < STOPPED_THRESHOLD;
michael@0 802 }
michael@0 803
michael@0 804 PointF resetDisplacement() {
michael@0 805 return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
michael@0 806 }
michael@0 807
michael@0 808 private void updatePosition() {
michael@0 809 mX.displace();
michael@0 810 mY.displace();
michael@0 811 PointF displacement = resetDisplacement();
michael@0 812 if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
michael@0 813 return;
michael@0 814 }
michael@0 815 if (mDefaultPrevented || mSubscroller.scrollBy(displacement)) {
michael@0 816 synchronized (mTarget.getLock()) {
michael@0 817 mTarget.scrollMarginsBy(displacement.x, displacement.y);
michael@0 818 }
michael@0 819 } else {
michael@0 820 synchronized (mTarget.getLock()) {
michael@0 821 scrollBy(displacement.x, displacement.y);
michael@0 822 }
michael@0 823 }
michael@0 824 }
michael@0 825
michael@0 826 /**
michael@0 827 * This class is an implementation of RenderTask which enforces its implementor to run in the UI thread.
michael@0 828 *
michael@0 829 */
michael@0 830 private abstract class PanZoomRenderTask extends RenderTask {
michael@0 831
michael@0 832 /**
michael@0 833 * the time when the current frame was started in ns.
michael@0 834 */
michael@0 835 protected long mCurrentFrameStartTime;
michael@0 836 /**
michael@0 837 * The current frame duration in ns.
michael@0 838 */
michael@0 839 protected long mLastFrameTimeDelta;
michael@0 840
michael@0 841 private final Runnable mRunnable = new Runnable() {
michael@0 842 @Override
michael@0 843 public final void run() {
michael@0 844 if (mContinueAnimation) {
michael@0 845 animateFrame();
michael@0 846 }
michael@0 847 }
michael@0 848 };
michael@0 849
michael@0 850 private boolean mContinueAnimation = true;
michael@0 851
michael@0 852 public PanZoomRenderTask() {
michael@0 853 super(false);
michael@0 854 }
michael@0 855
michael@0 856 @Override
michael@0 857 protected final boolean internalRun(long timeDelta, long currentFrameStartTime) {
michael@0 858
michael@0 859 mCurrentFrameStartTime = currentFrameStartTime;
michael@0 860 mLastFrameTimeDelta = timeDelta;
michael@0 861
michael@0 862 mTarget.post(mRunnable);
michael@0 863 return mContinueAnimation;
michael@0 864 }
michael@0 865
michael@0 866 /**
michael@0 867 * The method subclasses must override. This method is run on the UI thread thanks to internalRun
michael@0 868 */
michael@0 869 protected abstract void animateFrame();
michael@0 870
michael@0 871 /**
michael@0 872 * Terminate the animation.
michael@0 873 */
michael@0 874 public void terminate() {
michael@0 875 mContinueAnimation = false;
michael@0 876 }
michael@0 877 }
michael@0 878
michael@0 879 private class AutonavRenderTask extends PanZoomRenderTask {
michael@0 880 public AutonavRenderTask() {
michael@0 881 super();
michael@0 882 }
michael@0 883
michael@0 884 @Override
michael@0 885 protected void animateFrame() {
michael@0 886 if (mState != PanZoomState.AUTONAV) {
michael@0 887 finishAnimation();
michael@0 888 return;
michael@0 889 }
michael@0 890
michael@0 891 updatePosition();
michael@0 892 synchronized (mTarget.getLock()) {
michael@0 893 mTarget.setViewportMetrics(applyZoomDelta(getMetrics(), mAutonavZoomDelta));
michael@0 894 }
michael@0 895 }
michael@0 896 }
michael@0 897
michael@0 898 /* The task that performs the bounce animation. */
michael@0 899 private class BounceRenderTask extends PanZoomRenderTask {
michael@0 900
michael@0 901 /*
michael@0 902 * The viewport metrics that represent the start and end of the bounce-back animation,
michael@0 903 * respectively.
michael@0 904 */
michael@0 905 private ImmutableViewportMetrics mBounceStartMetrics;
michael@0 906 private ImmutableViewportMetrics mBounceEndMetrics;
michael@0 907 // How long ago this bounce was started in ns.
michael@0 908 private long mBounceDuration;
michael@0 909
michael@0 910 BounceRenderTask(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) {
michael@0 911 super();
michael@0 912 mBounceStartMetrics = startMetrics;
michael@0 913 mBounceEndMetrics = endMetrics;
michael@0 914 }
michael@0 915
michael@0 916 @Override
michael@0 917 protected void animateFrame() {
michael@0 918 /*
michael@0 919 * The pan/zoom controller might have signaled to us that it wants to abort the
michael@0 920 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
michael@0 921 * out.
michael@0 922 */
michael@0 923 if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
michael@0 924 finishAnimation();
michael@0 925 return;
michael@0 926 }
michael@0 927
michael@0 928 /* Perform the next frame of the bounce-back animation. */
michael@0 929 mBounceDuration = mCurrentFrameStartTime - getStartTime();
michael@0 930 if (mBounceDuration < BOUNCE_ANIMATION_DURATION) {
michael@0 931 advanceBounce();
michael@0 932 return;
michael@0 933 }
michael@0 934
michael@0 935 /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
michael@0 936 finishBounce();
michael@0 937 finishAnimation();
michael@0 938 setState(PanZoomState.NOTHING);
michael@0 939 }
michael@0 940
michael@0 941 /* Performs one frame of a bounce animation. */
michael@0 942 private void advanceBounce() {
michael@0 943 synchronized (mTarget.getLock()) {
michael@0 944 float t = easeOut((float)mBounceDuration / BOUNCE_ANIMATION_DURATION);
michael@0 945 ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
michael@0 946 mTarget.setViewportMetrics(newMetrics);
michael@0 947 }
michael@0 948 }
michael@0 949
michael@0 950 /* Concludes a bounce animation and snaps the viewport into place. */
michael@0 951 private void finishBounce() {
michael@0 952 synchronized (mTarget.getLock()) {
michael@0 953 mTarget.setViewportMetrics(mBounceEndMetrics);
michael@0 954 }
michael@0 955 }
michael@0 956 }
michael@0 957
michael@0 958 // The callback that performs the fling animation.
michael@0 959 private class FlingRenderTask extends PanZoomRenderTask {
michael@0 960
michael@0 961 public FlingRenderTask() {
michael@0 962 super();
michael@0 963 }
michael@0 964
michael@0 965 @Override
michael@0 966 protected void animateFrame() {
michael@0 967 /*
michael@0 968 * The pan/zoom controller might have signaled to us that it wants to abort the
michael@0 969 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
michael@0 970 * out.
michael@0 971 */
michael@0 972 if (mState != PanZoomState.FLING) {
michael@0 973 finishAnimation();
michael@0 974 return;
michael@0 975 }
michael@0 976
michael@0 977 /* Advance flings, if necessary. */
michael@0 978 boolean flingingX = mX.advanceFling(mLastFrameTimeDelta);
michael@0 979 boolean flingingY = mY.advanceFling(mLastFrameTimeDelta);
michael@0 980
michael@0 981 boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
michael@0 982
michael@0 983 /* If we're still flinging in any direction, update the origin. */
michael@0 984 if (flingingX || flingingY) {
michael@0 985 updatePosition();
michael@0 986
michael@0 987 /*
michael@0 988 * Check to see if we're still flinging with an appreciable velocity. The threshold is
michael@0 989 * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
michael@0 990 * coast smoothly to a stop when not. In other words, require a greater velocity to
michael@0 991 * maintain the fling once we enter overscroll.
michael@0 992 */
michael@0 993 float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
michael@0 994 if (getVelocity() >= threshold) {
michael@0 995 // we're still flinging
michael@0 996 return;
michael@0 997 }
michael@0 998
michael@0 999 mX.stopFling();
michael@0 1000 mY.stopFling();
michael@0 1001 }
michael@0 1002
michael@0 1003 /* Perform a bounce-back animation if overscrolled. */
michael@0 1004 if (overscrolled) {
michael@0 1005 bounce();
michael@0 1006 } else {
michael@0 1007 finishAnimation();
michael@0 1008 setState(PanZoomState.NOTHING);
michael@0 1009 }
michael@0 1010 }
michael@0 1011 }
michael@0 1012
michael@0 1013 private void finishAnimation() {
michael@0 1014 checkMainThread();
michael@0 1015
michael@0 1016 stopAnimationTask();
michael@0 1017
michael@0 1018 // Force a viewport synchronisation
michael@0 1019 mTarget.forceRedraw(null);
michael@0 1020 }
michael@0 1021
michael@0 1022 /* Returns the nearest viewport metrics with no overscroll visible. */
michael@0 1023 private ImmutableViewportMetrics getValidViewportMetrics() {
michael@0 1024 return getValidViewportMetrics(getMetrics());
michael@0 1025 }
michael@0 1026
michael@0 1027 private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
michael@0 1028 /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
michael@0 1029 float zoomFactor = viewportMetrics.zoomFactor;
michael@0 1030 RectF pageRect = viewportMetrics.getPageRect();
michael@0 1031 RectF viewport = viewportMetrics.getViewport();
michael@0 1032
michael@0 1033 float focusX = viewport.width() / 2.0f;
michael@0 1034 float focusY = viewport.height() / 2.0f;
michael@0 1035
michael@0 1036 float minZoomFactor = 0.0f;
michael@0 1037 float maxZoomFactor = MAX_ZOOM;
michael@0 1038
michael@0 1039 ZoomConstraints constraints = mTarget.getZoomConstraints();
michael@0 1040
michael@0 1041 if (constraints.getMinZoom() > 0)
michael@0 1042 minZoomFactor = constraints.getMinZoom();
michael@0 1043 if (constraints.getMaxZoom() > 0)
michael@0 1044 maxZoomFactor = constraints.getMaxZoom();
michael@0 1045
michael@0 1046 if (!constraints.getAllowZoom()) {
michael@0 1047 // If allowZoom is false, clamp to the default zoom level.
michael@0 1048 maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
michael@0 1049 }
michael@0 1050
michael@0 1051 // Ensure minZoomFactor keeps the page at least as big as the viewport.
michael@0 1052 if (pageRect.width() > 0) {
michael@0 1053 float pageWidth = pageRect.width() +
michael@0 1054 viewportMetrics.marginLeft +
michael@0 1055 viewportMetrics.marginRight;
michael@0 1056 float scaleFactor = viewport.width() / pageWidth;
michael@0 1057 minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
michael@0 1058 if (viewport.width() > pageWidth)
michael@0 1059 focusX = 0.0f;
michael@0 1060 }
michael@0 1061 if (pageRect.height() > 0) {
michael@0 1062 float pageHeight = pageRect.height() +
michael@0 1063 viewportMetrics.marginTop +
michael@0 1064 viewportMetrics.marginBottom;
michael@0 1065 float scaleFactor = viewport.height() / pageHeight;
michael@0 1066 minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
michael@0 1067 if (viewport.height() > pageHeight)
michael@0 1068 focusY = 0.0f;
michael@0 1069 }
michael@0 1070
michael@0 1071 maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
michael@0 1072
michael@0 1073 if (zoomFactor < minZoomFactor) {
michael@0 1074 // if one (or both) of the page dimensions is smaller than the viewport,
michael@0 1075 // zoom using the top/left as the focus on that axis. this prevents the
michael@0 1076 // scenario where, if both dimensions are smaller than the viewport, but
michael@0 1077 // by different scale factors, we end up scrolled to the end on one axis
michael@0 1078 // after applying the scale
michael@0 1079 PointF center = new PointF(focusX, focusY);
michael@0 1080 viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center);
michael@0 1081 } else if (zoomFactor > maxZoomFactor) {
michael@0 1082 PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
michael@0 1083 viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
michael@0 1084 }
michael@0 1085
michael@0 1086 /* Now we pan to the right origin. */
michael@0 1087 viewportMetrics = viewportMetrics.clampWithMargins();
michael@0 1088
michael@0 1089 return viewportMetrics;
michael@0 1090 }
michael@0 1091
michael@0 1092 private class AxisX extends Axis {
michael@0 1093 AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
michael@0 1094 @Override
michael@0 1095 public float getOrigin() { return getMetrics().viewportRectLeft; }
michael@0 1096 @Override
michael@0 1097 protected float getViewportLength() { return getMetrics().getWidth(); }
michael@0 1098 @Override
michael@0 1099 protected float getPageStart() { return getMetrics().pageRectLeft; }
michael@0 1100 @Override
michael@0 1101 protected float getMarginStart() { return mTarget.getMaxMargins().left - getMetrics().marginLeft; }
michael@0 1102 @Override
michael@0 1103 protected float getMarginEnd() { return mTarget.getMaxMargins().right - getMetrics().marginRight; }
michael@0 1104 @Override
michael@0 1105 protected float getPageLength() { return getMetrics().getPageWidthWithMargins(); }
michael@0 1106 @Override
michael@0 1107 protected boolean marginsHidden() {
michael@0 1108 ImmutableViewportMetrics metrics = getMetrics();
michael@0 1109 RectF maxMargins = mTarget.getMaxMargins();
michael@0 1110 return (metrics.marginLeft < maxMargins.left || metrics.marginRight < maxMargins.right);
michael@0 1111 }
michael@0 1112 @Override
michael@0 1113 protected void overscrollFling(final float velocity) {
michael@0 1114 if (mOverscroll != null) {
michael@0 1115 mOverscroll.setVelocity(velocity, Overscroll.Axis.X);
michael@0 1116 }
michael@0 1117 }
michael@0 1118 @Override
michael@0 1119 protected void overscrollPan(final float distance) {
michael@0 1120 if (mOverscroll != null) {
michael@0 1121 mOverscroll.setDistance(distance, Overscroll.Axis.X);
michael@0 1122 }
michael@0 1123 }
michael@0 1124 }
michael@0 1125
michael@0 1126 private class AxisY extends Axis {
michael@0 1127 AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
michael@0 1128 @Override
michael@0 1129 public float getOrigin() { return getMetrics().viewportRectTop; }
michael@0 1130 @Override
michael@0 1131 protected float getViewportLength() { return getMetrics().getHeight(); }
michael@0 1132 @Override
michael@0 1133 protected float getPageStart() { return getMetrics().pageRectTop; }
michael@0 1134 @Override
michael@0 1135 protected float getPageLength() { return getMetrics().getPageHeightWithMargins(); }
michael@0 1136 @Override
michael@0 1137 protected float getMarginStart() { return mTarget.getMaxMargins().top - getMetrics().marginTop; }
michael@0 1138 @Override
michael@0 1139 protected float getMarginEnd() { return mTarget.getMaxMargins().bottom - getMetrics().marginBottom; }
michael@0 1140 @Override
michael@0 1141 protected boolean marginsHidden() {
michael@0 1142 ImmutableViewportMetrics metrics = getMetrics();
michael@0 1143 RectF maxMargins = mTarget.getMaxMargins();
michael@0 1144 return (metrics.marginTop < maxMargins.top || metrics.marginBottom < maxMargins.bottom);
michael@0 1145 }
michael@0 1146 @Override
michael@0 1147 protected void overscrollFling(final float velocity) {
michael@0 1148 if (mOverscroll != null) {
michael@0 1149 mOverscroll.setVelocity(velocity, Overscroll.Axis.Y);
michael@0 1150 }
michael@0 1151 }
michael@0 1152 @Override
michael@0 1153 protected void overscrollPan(final float distance) {
michael@0 1154 if (mOverscroll != null) {
michael@0 1155 mOverscroll.setDistance(distance, Overscroll.Axis.Y);
michael@0 1156 }
michael@0 1157 }
michael@0 1158 }
michael@0 1159
michael@0 1160 /*
michael@0 1161 * Zooming
michael@0 1162 */
michael@0 1163 @Override
michael@0 1164 public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
michael@0 1165 if (mState == PanZoomState.ANIMATED_ZOOM)
michael@0 1166 return false;
michael@0 1167
michael@0 1168 if (!mTarget.getZoomConstraints().getAllowZoom())
michael@0 1169 return false;
michael@0 1170
michael@0 1171 setState(PanZoomState.PINCHING);
michael@0 1172 mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
michael@0 1173 cancelTouch();
michael@0 1174
michael@0 1175 GeckoAppShell.sendEventToGecko(GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_START, mLastZoomFocus, getMetrics().zoomFactor));
michael@0 1176
michael@0 1177 return true;
michael@0 1178 }
michael@0 1179
michael@0 1180 @Override
michael@0 1181 public boolean onScale(SimpleScaleGestureDetector detector) {
michael@0 1182 if (mTarget.isFullScreen())
michael@0 1183 return false;
michael@0 1184
michael@0 1185 if (mState != PanZoomState.PINCHING)
michael@0 1186 return false;
michael@0 1187
michael@0 1188 float prevSpan = detector.getPreviousSpan();
michael@0 1189 if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
michael@0 1190 // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
michael@0 1191 return true;
michael@0 1192 }
michael@0 1193
michael@0 1194 synchronized (mTarget.getLock()) {
michael@0 1195 float zoomFactor = getAdjustedZoomFactor(detector.getCurrentSpan() / prevSpan);
michael@0 1196 scrollBy(mLastZoomFocus.x - detector.getFocusX(),
michael@0 1197 mLastZoomFocus.y - detector.getFocusY());
michael@0 1198 mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
michael@0 1199 ImmutableViewportMetrics target = getMetrics().scaleTo(zoomFactor, mLastZoomFocus);
michael@0 1200
michael@0 1201 // If overscroll is diabled, prevent zooming outside the normal document pans.
michael@0 1202 if (mX.getOverScrollMode() == View.OVER_SCROLL_NEVER || mY.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
michael@0 1203 target = getValidViewportMetrics(target);
michael@0 1204 }
michael@0 1205 mTarget.setViewportMetrics(target);
michael@0 1206 }
michael@0 1207
michael@0 1208 GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY, mLastZoomFocus, getMetrics().zoomFactor);
michael@0 1209 GeckoAppShell.sendEventToGecko(event);
michael@0 1210
michael@0 1211 return true;
michael@0 1212 }
michael@0 1213
michael@0 1214 private ImmutableViewportMetrics applyZoomDelta(ImmutableViewportMetrics metrics, float zoomDelta) {
michael@0 1215 float oldZoom = metrics.zoomFactor;
michael@0 1216 float newZoom = oldZoom + zoomDelta;
michael@0 1217 float adjustedZoom = getAdjustedZoomFactor(newZoom / oldZoom);
michael@0 1218 // since we don't have a particular focus to zoom to, just use the center
michael@0 1219 PointF center = new PointF(metrics.getWidth() / 2.0f, metrics.getHeight() / 2.0f);
michael@0 1220 metrics = metrics.scaleTo(adjustedZoom, center);
michael@0 1221 return metrics;
michael@0 1222 }
michael@0 1223
michael@0 1224 private boolean animatedScale(float zoomDelta) {
michael@0 1225 if (mState != PanZoomState.NOTHING && mState != PanZoomState.BOUNCE) {
michael@0 1226 return false;
michael@0 1227 }
michael@0 1228 synchronized (mTarget.getLock()) {
michael@0 1229 ImmutableViewportMetrics metrics = applyZoomDelta(getMetrics(), zoomDelta);
michael@0 1230 bounce(getValidViewportMetrics(metrics), PanZoomState.BOUNCE);
michael@0 1231 }
michael@0 1232 return true;
michael@0 1233 }
michael@0 1234
michael@0 1235 private float getAdjustedZoomFactor(float zoomRatio) {
michael@0 1236 /*
michael@0 1237 * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
michael@0 1238 * factor toward 1.0.
michael@0 1239 */
michael@0 1240 float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true));
michael@0 1241 if (zoomRatio > 1.0f)
michael@0 1242 zoomRatio = 1.0f + (zoomRatio - 1.0f) * resistance;
michael@0 1243 else
michael@0 1244 zoomRatio = 1.0f - (1.0f - zoomRatio) * resistance;
michael@0 1245
michael@0 1246 float newZoomFactor = getMetrics().zoomFactor * zoomRatio;
michael@0 1247 float minZoomFactor = 0.0f;
michael@0 1248 float maxZoomFactor = MAX_ZOOM;
michael@0 1249
michael@0 1250 ZoomConstraints constraints = mTarget.getZoomConstraints();
michael@0 1251
michael@0 1252 if (constraints.getMinZoom() > 0)
michael@0 1253 minZoomFactor = constraints.getMinZoom();
michael@0 1254 if (constraints.getMaxZoom() > 0)
michael@0 1255 maxZoomFactor = constraints.getMaxZoom();
michael@0 1256
michael@0 1257 if (newZoomFactor < minZoomFactor) {
michael@0 1258 // apply resistance when zooming past minZoomFactor,
michael@0 1259 // such that it asymptotically reaches minZoomFactor / 2.0
michael@0 1260 // but never exceeds that
michael@0 1261 final float rate = 0.5f; // controls how quickly we approach the limit
michael@0 1262 float excessZoom = minZoomFactor - newZoomFactor;
michael@0 1263 excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
michael@0 1264 newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
michael@0 1265 }
michael@0 1266
michael@0 1267 if (newZoomFactor > maxZoomFactor) {
michael@0 1268 // apply resistance when zooming past maxZoomFactor,
michael@0 1269 // such that it asymptotically reaches maxZoomFactor + 1.0
michael@0 1270 // but never exceeds that
michael@0 1271 float excessZoom = newZoomFactor - maxZoomFactor;
michael@0 1272 excessZoom = 1.0f - (float)Math.exp(-excessZoom);
michael@0 1273 newZoomFactor = maxZoomFactor + excessZoom;
michael@0 1274 }
michael@0 1275
michael@0 1276 return newZoomFactor;
michael@0 1277 }
michael@0 1278
michael@0 1279 @Override
michael@0 1280 public void onScaleEnd(SimpleScaleGestureDetector detector) {
michael@0 1281 if (mState == PanZoomState.ANIMATED_ZOOM)
michael@0 1282 return;
michael@0 1283
michael@0 1284 // switch back to the touching state
michael@0 1285 startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
michael@0 1286
michael@0 1287 // Force a viewport synchronisation
michael@0 1288 mTarget.forceRedraw(null);
michael@0 1289
michael@0 1290 PointF point = new PointF(detector.getFocusX(), detector.getFocusY());
michael@0 1291 GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_END, point, getMetrics().zoomFactor);
michael@0 1292
michael@0 1293 if (event == null) {
michael@0 1294 return;
michael@0 1295 }
michael@0 1296
michael@0 1297 GeckoAppShell.sendEventToGecko(event);
michael@0 1298 }
michael@0 1299
michael@0 1300 @Override
michael@0 1301 public boolean getRedrawHint() {
michael@0 1302 switch (mState) {
michael@0 1303 case PINCHING:
michael@0 1304 case ANIMATED_ZOOM:
michael@0 1305 case BOUNCE:
michael@0 1306 // don't redraw during these because the zoom is (or might be, in the case
michael@0 1307 // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
michael@0 1308 // display port area. we trigger a force-redraw upon exiting these states.
michael@0 1309 return false;
michael@0 1310 default:
michael@0 1311 // allow redrawing in other states
michael@0 1312 return true;
michael@0 1313 }
michael@0 1314 }
michael@0 1315
michael@0 1316 private void sendPointToGecko(String event, MotionEvent motionEvent) {
michael@0 1317 String json;
michael@0 1318 try {
michael@0 1319 PointF point = new PointF(motionEvent.getX(), motionEvent.getY());
michael@0 1320 point = mTarget.convertViewPointToLayerPoint(point);
michael@0 1321 if (point == null) {
michael@0 1322 return;
michael@0 1323 }
michael@0 1324 json = PointUtils.toJSON(point).toString();
michael@0 1325 } catch (Exception e) {
michael@0 1326 Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e);
michael@0 1327 return;
michael@0 1328 }
michael@0 1329
michael@0 1330 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json));
michael@0 1331 }
michael@0 1332
michael@0 1333 @Override
michael@0 1334 public boolean onDown(MotionEvent motionEvent) {
michael@0 1335 mMediumPress = false;
michael@0 1336 return false;
michael@0 1337 }
michael@0 1338
michael@0 1339 @Override
michael@0 1340 public void onShowPress(MotionEvent motionEvent) {
michael@0 1341 // If we get this, it will be followed either by a call to
michael@0 1342 // onSingleTapUp (if the user lifts their finger before the
michael@0 1343 // long-press timeout) or a call to onLongPress (if the user
michael@0 1344 // does not). In the former case, we want to make sure it is
michael@0 1345 // treated as a click. (Note that if this is called, we will
michael@0 1346 // not get a call to onDoubleTap).
michael@0 1347 mMediumPress = true;
michael@0 1348 }
michael@0 1349
michael@0 1350 @Override
michael@0 1351 public void onLongPress(MotionEvent motionEvent) {
michael@0 1352 sendPointToGecko("Gesture:LongPress", motionEvent);
michael@0 1353 }
michael@0 1354
michael@0 1355 @Override
michael@0 1356 public boolean onSingleTapUp(MotionEvent motionEvent) {
michael@0 1357 // When double-tapping is allowed, we have to wait to see if this is
michael@0 1358 // going to be a double-tap.
michael@0 1359 // However, if mMediumPress is true then we know there will be no
michael@0 1360 // double-tap so we treat this as a click.
michael@0 1361 if (mMediumPress || !mTarget.getZoomConstraints().getAllowDoubleTapZoom()) {
michael@0 1362 sendPointToGecko("Gesture:SingleTap", motionEvent);
michael@0 1363 }
michael@0 1364 // return false because we still want to get the ACTION_UP event that triggers this
michael@0 1365 return false;
michael@0 1366 }
michael@0 1367
michael@0 1368 @Override
michael@0 1369 public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
michael@0 1370 // When zooming is disabled, we handle this in onSingleTapUp.
michael@0 1371 if (mTarget.getZoomConstraints().getAllowDoubleTapZoom()) {
michael@0 1372 sendPointToGecko("Gesture:SingleTap", motionEvent);
michael@0 1373 }
michael@0 1374 return true;
michael@0 1375 }
michael@0 1376
michael@0 1377 @Override
michael@0 1378 public boolean onDoubleTap(MotionEvent motionEvent) {
michael@0 1379 if (mTarget.getZoomConstraints().getAllowDoubleTapZoom()) {
michael@0 1380 sendPointToGecko("Gesture:DoubleTap", motionEvent);
michael@0 1381 }
michael@0 1382 return true;
michael@0 1383 }
michael@0 1384
michael@0 1385 private void cancelTouch() {
michael@0 1386 GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
michael@0 1387 GeckoAppShell.sendEventToGecko(e);
michael@0 1388 }
michael@0 1389
michael@0 1390 /**
michael@0 1391 * Zoom to a specified rect IN CSS PIXELS.
michael@0 1392 *
michael@0 1393 * While we usually use device pixels, @zoomToRect must be specified in CSS
michael@0 1394 * pixels.
michael@0 1395 */
michael@0 1396 private ImmutableViewportMetrics getMetricsToZoomTo(RectF zoomToRect) {
michael@0 1397 final float startZoom = getMetrics().zoomFactor;
michael@0 1398
michael@0 1399 RectF viewport = getMetrics().getViewport();
michael@0 1400 // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
michael@0 1401 // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
michael@0 1402 // while enlarging make sure we enlarge equally on both sides to keep the target rect
michael@0 1403 // centered.
michael@0 1404 float targetRatio = viewport.width() / viewport.height();
michael@0 1405 float rectRatio = zoomToRect.width() / zoomToRect.height();
michael@0 1406 if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
michael@0 1407 // all good, do nothing
michael@0 1408 } else if (targetRatio < rectRatio) {
michael@0 1409 // need to increase zoomToRect height
michael@0 1410 float newHeight = zoomToRect.width() / targetRatio;
michael@0 1411 zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
michael@0 1412 zoomToRect.bottom = zoomToRect.top + newHeight;
michael@0 1413 } else { // targetRatio > rectRatio) {
michael@0 1414 // need to increase zoomToRect width
michael@0 1415 float newWidth = targetRatio * zoomToRect.height();
michael@0 1416 zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
michael@0 1417 zoomToRect.right = zoomToRect.left + newWidth;
michael@0 1418 }
michael@0 1419
michael@0 1420 float finalZoom = viewport.width() / zoomToRect.width();
michael@0 1421
michael@0 1422 ImmutableViewportMetrics finalMetrics = getMetrics();
michael@0 1423 finalMetrics = finalMetrics.setViewportOrigin(
michael@0 1424 zoomToRect.left * finalMetrics.zoomFactor,
michael@0 1425 zoomToRect.top * finalMetrics.zoomFactor);
michael@0 1426 finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
michael@0 1427
michael@0 1428 // 2. now run getValidViewportMetrics on it, so that the target viewport is
michael@0 1429 // clamped down to prevent overscroll, over-zoom, and other bad conditions.
michael@0 1430 finalMetrics = getValidViewportMetrics(finalMetrics);
michael@0 1431 return finalMetrics;
michael@0 1432 }
michael@0 1433
michael@0 1434 private boolean animatedZoomTo(RectF zoomToRect) {
michael@0 1435 bounce(getMetricsToZoomTo(zoomToRect), PanZoomState.ANIMATED_ZOOM);
michael@0 1436 return true;
michael@0 1437 }
michael@0 1438
michael@0 1439 /** This function must be called from the UI thread. */
michael@0 1440 @Override
michael@0 1441 public void abortPanning() {
michael@0 1442 checkMainThread();
michael@0 1443 bounce();
michael@0 1444 }
michael@0 1445
michael@0 1446 @Override
michael@0 1447 public void setOverScrollMode(int overscrollMode) {
michael@0 1448 mX.setOverScrollMode(overscrollMode);
michael@0 1449 mY.setOverScrollMode(overscrollMode);
michael@0 1450 }
michael@0 1451
michael@0 1452 @Override
michael@0 1453 public int getOverScrollMode() {
michael@0 1454 return mX.getOverScrollMode();
michael@0 1455 }
michael@0 1456
michael@0 1457 @Override
michael@0 1458 public void setOverscrollHandler(final Overscroll handler) {
michael@0 1459 mOverscroll = handler;
michael@0 1460 }
michael@0 1461 }

mercurial