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