1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/JavaPanZoomController.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1461 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import org.mozilla.gecko.GeckoAppShell; 1.12 +import org.mozilla.gecko.GeckoEvent; 1.13 +import org.mozilla.gecko.PrefsHelper; 1.14 +import org.mozilla.gecko.Tab; 1.15 +import org.mozilla.gecko.Tabs; 1.16 +import org.mozilla.gecko.ZoomConstraints; 1.17 +import org.mozilla.gecko.EventDispatcher; 1.18 +import org.mozilla.gecko.util.FloatUtils; 1.19 +import org.mozilla.gecko.util.GamepadUtils; 1.20 +import org.mozilla.gecko.util.GeckoEventListener; 1.21 +import org.mozilla.gecko.util.ThreadUtils; 1.22 + 1.23 +import org.json.JSONObject; 1.24 + 1.25 +import android.graphics.PointF; 1.26 +import android.graphics.RectF; 1.27 +import android.os.Build; 1.28 +import android.util.FloatMath; 1.29 +import android.util.Log; 1.30 +import android.view.GestureDetector; 1.31 +import android.view.InputDevice; 1.32 +import android.view.KeyEvent; 1.33 +import android.view.MotionEvent; 1.34 +import android.view.View; 1.35 + 1.36 +/* 1.37 + * Handles the kinetic scrolling and zooming physics for a layer controller. 1.38 + * 1.39 + * Many ideas are from Joe Hewitt's Scrollability: 1.40 + * https://github.com/joehewitt/scrollability/ 1.41 + */ 1.42 +class JavaPanZoomController 1.43 + extends GestureDetector.SimpleOnGestureListener 1.44 + implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener 1.45 +{ 1.46 + private static final String LOGTAG = "GeckoPanZoomController"; 1.47 + 1.48 + private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect"; 1.49 + private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth"; 1.50 + private static String MESSAGE_TOUCH_LISTENER = "Tab:HasTouchListener"; 1.51 + 1.52 + // Animation stops if the velocity is below this value when overscrolled or panning. 1.53 + private static final float STOPPED_THRESHOLD = 4.0f; 1.54 + 1.55 + // Animation stops is the velocity is below this threshold when flinging. 1.56 + private static final float FLING_STOPPED_THRESHOLD = 0.1f; 1.57 + 1.58 + // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans 1.59 + // between the touch-down and touch-up of a click). In units of density-independent pixels. 1.60 + public static final float PAN_THRESHOLD = 1/16f * GeckoAppShell.getDpi(); 1.61 + 1.62 + // Angle from axis within which we stay axis-locked 1.63 + private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees 1.64 + 1.65 + // Axis-lock breakout angle 1.66 + private static final double AXIS_BREAKOUT_ANGLE = Math.PI / 8.0; 1.67 + 1.68 + // The distance the user has to pan before we consider breaking out of a locked axis 1.69 + public static final float AXIS_BREAKOUT_THRESHOLD = 1/32f * GeckoAppShell.getDpi(); 1.70 + 1.71 + // The maximum amount we allow you to zoom into a page 1.72 + private static final float MAX_ZOOM = 4.0f; 1.73 + 1.74 + // The maximum amount we would like to scroll with the mouse 1.75 + private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi(); 1.76 + 1.77 + // The maximum zoom factor adjustment per frame of the AUTONAV animation 1.78 + private static final float MAX_ZOOM_DELTA = 0.125f; 1.79 + 1.80 + // The duration of the bounce animation in ns 1.81 + private static final int BOUNCE_ANIMATION_DURATION = 250000000; 1.82 + 1.83 + private enum PanZoomState { 1.84 + NOTHING, /* no touch-start events received */ 1.85 + FLING, /* all touches removed, but we're still scrolling page */ 1.86 + TOUCHING, /* one touch-start event received */ 1.87 + PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis lock) X axis */ 1.88 + PANNING_LOCKED_Y, /* as above for Y axis */ 1.89 + PANNING, /* panning without axis lock */ 1.90 + PANNING_HOLD, /* in panning, but not moving. 1.91 + * similar to TOUCHING but after starting a pan */ 1.92 + PANNING_HOLD_LOCKED_X, /* like PANNING_HOLD, but axis lock still in effect for X axis */ 1.93 + PANNING_HOLD_LOCKED_Y, /* as above but for Y axis */ 1.94 + PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ 1.95 + ANIMATED_ZOOM, /* animated zoom to a new rect */ 1.96 + BOUNCE, /* in a bounce animation */ 1.97 + WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has 1.98 + put a finger down, but we don't yet know if a touch listener has 1.99 + prevented the default actions yet. we still need to abort animations. */ 1.100 + AUTONAV, /* We are scrolling using an AutonavRunnable animation. This is similar 1.101 + to the FLING state except that it must be stopped manually by the code that 1.102 + started it, and it's velocity can be updated while it's running. */ 1.103 + } 1.104 + 1.105 + private enum AxisLockMode { 1.106 + STANDARD, /* Default axis locking mode that doesn't break out until finger release */ 1.107 + FREE, /* No locking at all */ 1.108 + STICKY /* Break out with hysteresis so that it feels as free as possible whilst locking */ 1.109 + } 1.110 + 1.111 + private final PanZoomTarget mTarget; 1.112 + private final SubdocumentScrollHelper mSubscroller; 1.113 + private final Axis mX; 1.114 + private final Axis mY; 1.115 + private final TouchEventHandler mTouchEventHandler; 1.116 + private final EventDispatcher mEventDispatcher; 1.117 + 1.118 + /* The task that handles flings, autonav or bounces. */ 1.119 + private PanZoomRenderTask mAnimationRenderTask; 1.120 + /* The zoom focus at the first zoom event (in page coordinates). */ 1.121 + private PointF mLastZoomFocus; 1.122 + /* The time the last motion event took place. */ 1.123 + private long mLastEventTime; 1.124 + /* Current state the pan/zoom UI is in. */ 1.125 + private PanZoomState mState; 1.126 + /* The per-frame zoom delta for the currently-running AUTONAV animation. */ 1.127 + private float mAutonavZoomDelta; 1.128 + /* The user selected panning mode */ 1.129 + private AxisLockMode mMode; 1.130 + /* A medium-length tap/press is happening */ 1.131 + private boolean mMediumPress; 1.132 + /* Used to change the scrollY direction */ 1.133 + private boolean mNegateWheelScrollY; 1.134 + /* Whether the current event has been default-prevented. */ 1.135 + private boolean mDefaultPrevented; 1.136 + 1.137 + // Handler to be notified when overscroll occurs 1.138 + private Overscroll mOverscroll; 1.139 + 1.140 + public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) { 1.141 + mTarget = target; 1.142 + mSubscroller = new SubdocumentScrollHelper(eventDispatcher); 1.143 + mX = new AxisX(mSubscroller); 1.144 + mY = new AxisY(mSubscroller); 1.145 + mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this); 1.146 + 1.147 + checkMainThread(); 1.148 + 1.149 + setState(PanZoomState.NOTHING); 1.150 + 1.151 + mEventDispatcher = eventDispatcher; 1.152 + registerEventListener(MESSAGE_ZOOM_RECT); 1.153 + registerEventListener(MESSAGE_ZOOM_PAGE); 1.154 + registerEventListener(MESSAGE_TOUCH_LISTENER); 1.155 + 1.156 + mMode = AxisLockMode.STANDARD; 1.157 + 1.158 + String[] prefs = { "ui.scrolling.axis_lock_mode", 1.159 + "ui.scrolling.negate_wheel_scrollY", 1.160 + "ui.scrolling.gamepad_dead_zone" }; 1.161 + mNegateWheelScrollY = false; 1.162 + PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() { 1.163 + @Override public void prefValue(String pref, String value) { 1.164 + if (pref.equals("ui.scrolling.axis_lock_mode")) { 1.165 + if (value.equals("standard")) { 1.166 + mMode = AxisLockMode.STANDARD; 1.167 + } else if (value.equals("free")) { 1.168 + mMode = AxisLockMode.FREE; 1.169 + } else { 1.170 + mMode = AxisLockMode.STICKY; 1.171 + } 1.172 + } 1.173 + } 1.174 + 1.175 + @Override public void prefValue(String pref, int value) { 1.176 + if (pref.equals("ui.scrolling.gamepad_dead_zone")) { 1.177 + GamepadUtils.overrideDeadZoneThreshold((float)value / 1000f); 1.178 + } 1.179 + } 1.180 + 1.181 + @Override public void prefValue(String pref, boolean value) { 1.182 + if (pref.equals("ui.scrolling.negate_wheel_scrollY")) { 1.183 + mNegateWheelScrollY = value; 1.184 + } 1.185 + } 1.186 + 1.187 + @Override 1.188 + public boolean isObserver() { 1.189 + return true; 1.190 + } 1.191 + 1.192 + }); 1.193 + 1.194 + Axis.initPrefs(); 1.195 + } 1.196 + 1.197 + @Override 1.198 + public void destroy() { 1.199 + unregisterEventListener(MESSAGE_ZOOM_RECT); 1.200 + unregisterEventListener(MESSAGE_ZOOM_PAGE); 1.201 + unregisterEventListener(MESSAGE_TOUCH_LISTENER); 1.202 + mSubscroller.destroy(); 1.203 + mTouchEventHandler.destroy(); 1.204 + } 1.205 + 1.206 + private final static float easeOut(float t) { 1.207 + // ease-out approx. 1.208 + // -(t-1)^2+1 1.209 + t = t-1; 1.210 + return -t*t+1; 1.211 + } 1.212 + 1.213 + private void registerEventListener(String event) { 1.214 + mEventDispatcher.registerEventListener(event, this); 1.215 + } 1.216 + 1.217 + private void unregisterEventListener(String event) { 1.218 + mEventDispatcher.unregisterEventListener(event, this); 1.219 + } 1.220 + 1.221 + private void setState(PanZoomState state) { 1.222 + if (state != mState) { 1.223 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PanZoom:StateChange", state.toString())); 1.224 + mState = state; 1.225 + 1.226 + // Let the target know we've finished with it (for now) 1.227 + if (state == PanZoomState.NOTHING) { 1.228 + mTarget.panZoomStopped(); 1.229 + } 1.230 + } 1.231 + } 1.232 + 1.233 + private ImmutableViewportMetrics getMetrics() { 1.234 + return mTarget.getViewportMetrics(); 1.235 + } 1.236 + 1.237 + private void checkMainThread() { 1.238 + if (!ThreadUtils.isOnUiThread()) { 1.239 + // log with full stack trace 1.240 + Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception()); 1.241 + } 1.242 + } 1.243 + 1.244 + @Override 1.245 + public void handleMessage(String event, JSONObject message) { 1.246 + try { 1.247 + if (MESSAGE_ZOOM_RECT.equals(event)) { 1.248 + float x = (float)message.getDouble("x"); 1.249 + float y = (float)message.getDouble("y"); 1.250 + final RectF zoomRect = new RectF(x, y, 1.251 + x + (float)message.getDouble("w"), 1.252 + y + (float)message.getDouble("h")); 1.253 + if (message.optBoolean("animate", true)) { 1.254 + mTarget.post(new Runnable() { 1.255 + @Override 1.256 + public void run() { 1.257 + animatedZoomTo(zoomRect); 1.258 + } 1.259 + }); 1.260 + } else { 1.261 + mTarget.setViewportMetrics(getMetricsToZoomTo(zoomRect)); 1.262 + } 1.263 + } else if (MESSAGE_ZOOM_PAGE.equals(event)) { 1.264 + ImmutableViewportMetrics metrics = getMetrics(); 1.265 + RectF cssPageRect = metrics.getCssPageRect(); 1.266 + 1.267 + RectF viewableRect = metrics.getCssViewport(); 1.268 + float y = viewableRect.top; 1.269 + // attempt to keep zoom keep focused on the center of the viewport 1.270 + float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width(); 1.271 + float dh = viewableRect.height() - newHeight; // increase in the height 1.272 + final RectF r = new RectF(0.0f, 1.273 + y + dh/2, 1.274 + cssPageRect.width(), 1.275 + y + dh/2 + newHeight); 1.276 + if (message.optBoolean("animate", true)) { 1.277 + mTarget.post(new Runnable() { 1.278 + @Override 1.279 + public void run() { 1.280 + animatedZoomTo(r); 1.281 + } 1.282 + }); 1.283 + } else { 1.284 + mTarget.setViewportMetrics(getMetricsToZoomTo(r)); 1.285 + } 1.286 + } else if (MESSAGE_TOUCH_LISTENER.equals(event)) { 1.287 + int tabId = message.getInt("tabID"); 1.288 + final Tab tab = Tabs.getInstance().getTab(tabId); 1.289 + tab.setHasTouchListeners(true); 1.290 + mTarget.post(new Runnable() { 1.291 + @Override 1.292 + public void run() { 1.293 + if (Tabs.getInstance().isSelectedTab(tab)) 1.294 + mTouchEventHandler.setWaitForTouchListeners(true); 1.295 + } 1.296 + }); 1.297 + } 1.298 + } catch (Exception e) { 1.299 + Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); 1.300 + } 1.301 + } 1.302 + 1.303 + /** This function MUST be called on the UI thread */ 1.304 + @Override 1.305 + public boolean onKeyEvent(KeyEvent event) { 1.306 + if (Build.VERSION.SDK_INT <= 11) { 1.307 + return false; 1.308 + } 1.309 + 1.310 + if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD 1.311 + && event.getAction() == KeyEvent.ACTION_DOWN) { 1.312 + 1.313 + switch (event.getKeyCode()) { 1.314 + case KeyEvent.KEYCODE_ZOOM_IN: 1.315 + return animatedScale(0.2f); 1.316 + case KeyEvent.KEYCODE_ZOOM_OUT: 1.317 + return animatedScale(-0.2f); 1.318 + } 1.319 + } 1.320 + return false; 1.321 + } 1.322 + 1.323 + /** This function MUST be called on the UI thread */ 1.324 + @Override 1.325 + public boolean onMotionEvent(MotionEvent event) { 1.326 + if (Build.VERSION.SDK_INT <= 11) { 1.327 + return false; 1.328 + } 1.329 + 1.330 + switch (event.getSource() & InputDevice.SOURCE_CLASS_MASK) { 1.331 + case InputDevice.SOURCE_CLASS_POINTER: 1.332 + switch (event.getAction() & MotionEvent.ACTION_MASK) { 1.333 + case MotionEvent.ACTION_SCROLL: return handlePointerScroll(event); 1.334 + } 1.335 + break; 1.336 + case InputDevice.SOURCE_CLASS_JOYSTICK: 1.337 + switch (event.getAction() & MotionEvent.ACTION_MASK) { 1.338 + case MotionEvent.ACTION_MOVE: return handleJoystickNav(event); 1.339 + } 1.340 + break; 1.341 + } 1.342 + return false; 1.343 + } 1.344 + 1.345 + /** This function MUST be called on the UI thread */ 1.346 + @Override 1.347 + public boolean onTouchEvent(MotionEvent event) { 1.348 + return mTouchEventHandler.handleEvent(event); 1.349 + } 1.350 + 1.351 + boolean handleEvent(MotionEvent event, boolean defaultPrevented) { 1.352 + mDefaultPrevented = defaultPrevented; 1.353 + 1.354 + switch (event.getAction() & MotionEvent.ACTION_MASK) { 1.355 + case MotionEvent.ACTION_DOWN: return handleTouchStart(event); 1.356 + case MotionEvent.ACTION_MOVE: return handleTouchMove(event); 1.357 + case MotionEvent.ACTION_UP: return handleTouchEnd(event); 1.358 + case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event); 1.359 + } 1.360 + return false; 1.361 + } 1.362 + 1.363 + /** This function MUST be called on the UI thread */ 1.364 + @Override 1.365 + public void notifyDefaultActionPrevented(boolean prevented) { 1.366 + mTouchEventHandler.handleEventListenerAction(!prevented); 1.367 + } 1.368 + 1.369 + /** This function must be called from the UI thread. */ 1.370 + @Override 1.371 + public void abortAnimation() { 1.372 + checkMainThread(); 1.373 + // this happens when gecko changes the viewport on us or if the device is rotated. 1.374 + // if that's the case, abort any animation in progress and re-zoom so that the page 1.375 + // snaps to edges. for other cases (where the user's finger(s) are down) don't do 1.376 + // anything special. 1.377 + switch (mState) { 1.378 + case FLING: 1.379 + mX.stopFling(); 1.380 + mY.stopFling(); 1.381 + // fall through 1.382 + case BOUNCE: 1.383 + case ANIMATED_ZOOM: 1.384 + // the zoom that's in progress likely makes no sense any more (such as if 1.385 + // the screen orientation changed) so abort it 1.386 + setState(PanZoomState.NOTHING); 1.387 + // fall through 1.388 + case NOTHING: 1.389 + // Don't do animations here; they're distracting and can cause flashes on page 1.390 + // transitions. 1.391 + synchronized (mTarget.getLock()) { 1.392 + mTarget.setViewportMetrics(getValidViewportMetrics()); 1.393 + mTarget.forceRedraw(null); 1.394 + } 1.395 + break; 1.396 + } 1.397 + } 1.398 + 1.399 + /** This function must be called on the UI thread. */ 1.400 + public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) { 1.401 + checkMainThread(); 1.402 + mSubscroller.cancel(); 1.403 + if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 1.404 + // this is the first touch point going down, so we enter the pending state 1.405 + // seting the state will kill any animations in progress, possibly leaving 1.406 + // the page in overscroll 1.407 + setState(PanZoomState.WAITING_LISTENERS); 1.408 + } 1.409 + } 1.410 + 1.411 + /** This must be called on the UI thread. */ 1.412 + @Override 1.413 + public void pageRectUpdated() { 1.414 + if (mState == PanZoomState.NOTHING) { 1.415 + synchronized (mTarget.getLock()) { 1.416 + ImmutableViewportMetrics validated = getValidViewportMetrics(); 1.417 + if (!getMetrics().fuzzyEquals(validated)) { 1.418 + // page size changed such that we are now in overscroll. snap to the 1.419 + // the nearest valid viewport 1.420 + mTarget.setViewportMetrics(validated); 1.421 + } 1.422 + } 1.423 + } 1.424 + } 1.425 + 1.426 + /* 1.427 + * Panning/scrolling 1.428 + */ 1.429 + 1.430 + private boolean handleTouchStart(MotionEvent event) { 1.431 + // user is taking control of movement, so stop 1.432 + // any auto-movement we have going 1.433 + stopAnimationTask(); 1.434 + 1.435 + switch (mState) { 1.436 + case ANIMATED_ZOOM: 1.437 + // We just interrupted a double-tap animation, so force a redraw in 1.438 + // case this touchstart is just a tap that doesn't end up triggering 1.439 + // a redraw 1.440 + mTarget.forceRedraw(null); 1.441 + // fall through 1.442 + case FLING: 1.443 + case AUTONAV: 1.444 + case BOUNCE: 1.445 + case NOTHING: 1.446 + case WAITING_LISTENERS: 1.447 + startTouch(event.getX(0), event.getY(0), event.getEventTime()); 1.448 + return false; 1.449 + case TOUCHING: 1.450 + case PANNING: 1.451 + case PANNING_LOCKED_X: 1.452 + case PANNING_LOCKED_Y: 1.453 + case PANNING_HOLD: 1.454 + case PANNING_HOLD_LOCKED_X: 1.455 + case PANNING_HOLD_LOCKED_Y: 1.456 + case PINCHING: 1.457 + Log.e(LOGTAG, "Received impossible touch down while in " + mState); 1.458 + return false; 1.459 + } 1.460 + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart"); 1.461 + return false; 1.462 + } 1.463 + 1.464 + private boolean handleTouchMove(MotionEvent event) { 1.465 + 1.466 + switch (mState) { 1.467 + case FLING: 1.468 + case AUTONAV: 1.469 + case BOUNCE: 1.470 + case WAITING_LISTENERS: 1.471 + // should never happen 1.472 + Log.e(LOGTAG, "Received impossible touch move while in " + mState); 1.473 + // fall through 1.474 + case ANIMATED_ZOOM: 1.475 + case NOTHING: 1.476 + // may happen if user double-taps and drags without lifting after the 1.477 + // second tap. ignore the move if this happens. 1.478 + return false; 1.479 + 1.480 + case TOUCHING: 1.481 + // Don't allow panning if there is an element in full-screen mode. See bug 775511. 1.482 + if ((mTarget.isFullScreen() && !mSubscroller.scrolling()) || panDistance(event) < PAN_THRESHOLD) { 1.483 + return false; 1.484 + } 1.485 + cancelTouch(); 1.486 + startPanning(event.getX(0), event.getY(0), event.getEventTime()); 1.487 + track(event); 1.488 + return true; 1.489 + 1.490 + case PANNING_HOLD_LOCKED_X: 1.491 + setState(PanZoomState.PANNING_LOCKED_X); 1.492 + track(event); 1.493 + return true; 1.494 + case PANNING_HOLD_LOCKED_Y: 1.495 + setState(PanZoomState.PANNING_LOCKED_Y); 1.496 + // fall through 1.497 + case PANNING_LOCKED_X: 1.498 + case PANNING_LOCKED_Y: 1.499 + track(event); 1.500 + return true; 1.501 + 1.502 + case PANNING_HOLD: 1.503 + setState(PanZoomState.PANNING); 1.504 + // fall through 1.505 + case PANNING: 1.506 + track(event); 1.507 + return true; 1.508 + 1.509 + case PINCHING: 1.510 + // scale gesture listener will handle this 1.511 + return false; 1.512 + } 1.513 + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove"); 1.514 + return false; 1.515 + } 1.516 + 1.517 + private boolean handleTouchEnd(MotionEvent event) { 1.518 + 1.519 + switch (mState) { 1.520 + case FLING: 1.521 + case AUTONAV: 1.522 + case BOUNCE: 1.523 + case ANIMATED_ZOOM: 1.524 + case NOTHING: 1.525 + // may happen if user double-taps and drags without lifting after the 1.526 + // second tap. ignore if this happens. 1.527 + return false; 1.528 + 1.529 + case WAITING_LISTENERS: 1.530 + if (!mDefaultPrevented) { 1.531 + // should never happen 1.532 + Log.e(LOGTAG, "Received impossible touch end while in " + mState); 1.533 + } 1.534 + // fall through 1.535 + case TOUCHING: 1.536 + // the switch into TOUCHING might have happened while the page was 1.537 + // snapping back after overscroll. we need to finish the snap if that 1.538 + // was the case 1.539 + bounce(); 1.540 + return false; 1.541 + 1.542 + case PANNING: 1.543 + case PANNING_LOCKED_X: 1.544 + case PANNING_LOCKED_Y: 1.545 + case PANNING_HOLD: 1.546 + case PANNING_HOLD_LOCKED_X: 1.547 + case PANNING_HOLD_LOCKED_Y: 1.548 + setState(PanZoomState.FLING); 1.549 + fling(); 1.550 + return true; 1.551 + 1.552 + case PINCHING: 1.553 + setState(PanZoomState.NOTHING); 1.554 + return true; 1.555 + } 1.556 + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd"); 1.557 + return false; 1.558 + } 1.559 + 1.560 + private boolean handleTouchCancel(MotionEvent event) { 1.561 + cancelTouch(); 1.562 + 1.563 + // ensure we snap back if we're overscrolled 1.564 + bounce(); 1.565 + return false; 1.566 + } 1.567 + 1.568 + private boolean handlePointerScroll(MotionEvent event) { 1.569 + if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) { 1.570 + float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1.571 + float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1.572 + if (mNegateWheelScrollY) { 1.573 + scrollY *= -1.0; 1.574 + } 1.575 + scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL); 1.576 + bounce(); 1.577 + return true; 1.578 + } 1.579 + return false; 1.580 + } 1.581 + 1.582 + private float filterDeadZone(MotionEvent event, int axis) { 1.583 + return (GamepadUtils.isValueInDeadZone(event, axis) ? 0 : event.getAxisValue(axis)); 1.584 + } 1.585 + 1.586 + private float normalizeJoystickScroll(MotionEvent event, int axis) { 1.587 + return filterDeadZone(event, axis) * MAX_SCROLL; 1.588 + } 1.589 + 1.590 + private float normalizeJoystickZoom(MotionEvent event, int axis) { 1.591 + // negate MAX_ZOOM_DELTA so that pushing up on the stick zooms in 1.592 + return filterDeadZone(event, axis) * -MAX_ZOOM_DELTA; 1.593 + } 1.594 + 1.595 + // Since this event is a position-based event rather than a motion-based event, we need to 1.596 + // set up an AUTONAV animation to keep scrolling even while we don't get events. 1.597 + private boolean handleJoystickNav(MotionEvent event) { 1.598 + float velocityX = normalizeJoystickScroll(event, MotionEvent.AXIS_X); 1.599 + float velocityY = normalizeJoystickScroll(event, MotionEvent.AXIS_Y); 1.600 + float zoomDelta = normalizeJoystickZoom(event, MotionEvent.AXIS_RZ); 1.601 + 1.602 + if (velocityX == 0 && velocityY == 0 && zoomDelta == 0) { 1.603 + if (mState == PanZoomState.AUTONAV) { 1.604 + bounce(); // if not needed, this will automatically go to state NOTHING 1.605 + return true; 1.606 + } 1.607 + return false; 1.608 + } 1.609 + 1.610 + if (mState == PanZoomState.NOTHING) { 1.611 + setState(PanZoomState.AUTONAV); 1.612 + startAnimationRenderTask(new AutonavRenderTask()); 1.613 + } 1.614 + if (mState == PanZoomState.AUTONAV) { 1.615 + mX.setAutoscrollVelocity(velocityX); 1.616 + mY.setAutoscrollVelocity(velocityY); 1.617 + mAutonavZoomDelta = zoomDelta; 1.618 + return true; 1.619 + } 1.620 + return false; 1.621 + } 1.622 + 1.623 + private void startTouch(float x, float y, long time) { 1.624 + mX.startTouch(x); 1.625 + mY.startTouch(y); 1.626 + setState(PanZoomState.TOUCHING); 1.627 + mLastEventTime = time; 1.628 + } 1.629 + 1.630 + private void startPanning(float x, float y, long time) { 1.631 + float dx = mX.panDistance(x); 1.632 + float dy = mY.panDistance(y); 1.633 + double angle = Math.atan2(dy, dx); // range [-pi, pi] 1.634 + angle = Math.abs(angle); // range [0, pi] 1.635 + 1.636 + // When the touch move breaks through the pan threshold, reposition the touch down origin 1.637 + // so the page won't jump when we start panning. 1.638 + mX.startTouch(x); 1.639 + mY.startTouch(y); 1.640 + mLastEventTime = time; 1.641 + 1.642 + if (mMode == AxisLockMode.STANDARD || mMode == AxisLockMode.STICKY) { 1.643 + if (!mX.scrollable() || !mY.scrollable()) { 1.644 + setState(PanZoomState.PANNING); 1.645 + } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) { 1.646 + mY.setScrollingDisabled(true); 1.647 + setState(PanZoomState.PANNING_LOCKED_X); 1.648 + } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) { 1.649 + mX.setScrollingDisabled(true); 1.650 + setState(PanZoomState.PANNING_LOCKED_Y); 1.651 + } else { 1.652 + setState(PanZoomState.PANNING); 1.653 + } 1.654 + } else if (mMode == AxisLockMode.FREE) { 1.655 + setState(PanZoomState.PANNING); 1.656 + } 1.657 + } 1.658 + 1.659 + private float panDistance(MotionEvent move) { 1.660 + float dx = mX.panDistance(move.getX(0)); 1.661 + float dy = mY.panDistance(move.getY(0)); 1.662 + return FloatMath.sqrt(dx * dx + dy * dy); 1.663 + } 1.664 + 1.665 + private void track(float x, float y, long time) { 1.666 + float timeDelta = (float)(time - mLastEventTime); 1.667 + if (FloatUtils.fuzzyEquals(timeDelta, 0)) { 1.668 + // probably a duplicate event, ignore it. using a zero timeDelta will mess 1.669 + // up our velocity 1.670 + return; 1.671 + } 1.672 + mLastEventTime = time; 1.673 + 1.674 + 1.675 + // if we're axis-locked check if the user is trying to scroll away from the lock 1.676 + if (mMode == AxisLockMode.STICKY) { 1.677 + float dx = mX.panDistance(x); 1.678 + float dy = mY.panDistance(y); 1.679 + double angle = Math.atan2(dy, dx); // range [-pi, pi] 1.680 + angle = Math.abs(angle); // range [0, pi] 1.681 + 1.682 + if (Math.abs(dx) > AXIS_BREAKOUT_THRESHOLD || Math.abs(dy) > AXIS_BREAKOUT_THRESHOLD) { 1.683 + if (mState == PanZoomState.PANNING_LOCKED_X) { 1.684 + if (angle > AXIS_BREAKOUT_ANGLE && angle < (Math.PI - AXIS_BREAKOUT_ANGLE)) { 1.685 + mY.setScrollingDisabled(false); 1.686 + setState(PanZoomState.PANNING); 1.687 + } 1.688 + } else if (mState == PanZoomState.PANNING_LOCKED_Y) { 1.689 + if (Math.abs(angle - (Math.PI / 2)) > AXIS_BREAKOUT_ANGLE) { 1.690 + mX.setScrollingDisabled(false); 1.691 + setState(PanZoomState.PANNING); 1.692 + } 1.693 + } 1.694 + } 1.695 + } 1.696 + 1.697 + mX.updateWithTouchAt(x, timeDelta); 1.698 + mY.updateWithTouchAt(y, timeDelta); 1.699 + } 1.700 + 1.701 + private void track(MotionEvent event) { 1.702 + mX.saveTouchPos(); 1.703 + mY.saveTouchPos(); 1.704 + 1.705 + for (int i = 0; i < event.getHistorySize(); i++) { 1.706 + track(event.getHistoricalX(0, i), 1.707 + event.getHistoricalY(0, i), 1.708 + event.getHistoricalEventTime(i)); 1.709 + } 1.710 + track(event.getX(0), event.getY(0), event.getEventTime()); 1.711 + 1.712 + if (stopped()) { 1.713 + if (mState == PanZoomState.PANNING) { 1.714 + setState(PanZoomState.PANNING_HOLD); 1.715 + } else if (mState == PanZoomState.PANNING_LOCKED_X) { 1.716 + setState(PanZoomState.PANNING_HOLD_LOCKED_X); 1.717 + } else if (mState == PanZoomState.PANNING_LOCKED_Y) { 1.718 + setState(PanZoomState.PANNING_HOLD_LOCKED_Y); 1.719 + } else { 1.720 + // should never happen, but handle anyway for robustness 1.721 + Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track"); 1.722 + setState(PanZoomState.PANNING_HOLD); 1.723 + } 1.724 + } 1.725 + 1.726 + mX.startPan(); 1.727 + mY.startPan(); 1.728 + updatePosition(); 1.729 + } 1.730 + 1.731 + private void scrollBy(float dx, float dy) { 1.732 + mTarget.scrollBy(dx, dy); 1.733 + } 1.734 + 1.735 + private void fling() { 1.736 + updatePosition(); 1.737 + 1.738 + stopAnimationTask(); 1.739 + 1.740 + boolean stopped = stopped(); 1.741 + mX.startFling(stopped); 1.742 + mY.startFling(stopped); 1.743 + 1.744 + startAnimationRenderTask(new FlingRenderTask()); 1.745 + } 1.746 + 1.747 + /* Performs a bounce-back animation to the given viewport metrics. */ 1.748 + private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) { 1.749 + stopAnimationTask(); 1.750 + 1.751 + ImmutableViewportMetrics bounceStartMetrics = getMetrics(); 1.752 + if (bounceStartMetrics.fuzzyEquals(metrics)) { 1.753 + setState(PanZoomState.NOTHING); 1.754 + return; 1.755 + } 1.756 + 1.757 + setState(state); 1.758 + 1.759 + // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so 1.760 + // getRedrawHint() is returning false. This means we can safely call 1.761 + // setAnimationTarget to set the new final display port and not have it get 1.762 + // clobbered by display ports from intermediate animation frames. 1.763 + mTarget.setAnimationTarget(metrics); 1.764 + startAnimationRenderTask(new BounceRenderTask(bounceStartMetrics, metrics)); 1.765 + } 1.766 + 1.767 + /* Performs a bounce-back animation to the nearest valid viewport metrics. */ 1.768 + private void bounce() { 1.769 + bounce(getValidViewportMetrics(), PanZoomState.BOUNCE); 1.770 + } 1.771 + 1.772 + /* Starts the fling or bounce animation. */ 1.773 + private void startAnimationRenderTask(final PanZoomRenderTask task) { 1.774 + if (mAnimationRenderTask != null) { 1.775 + Log.e(LOGTAG, "Attempted to start a new task without canceling the old one!"); 1.776 + stopAnimationTask(); 1.777 + } 1.778 + 1.779 + mAnimationRenderTask = task; 1.780 + mTarget.postRenderTask(mAnimationRenderTask); 1.781 + } 1.782 + 1.783 + /* Stops the fling or bounce animation. */ 1.784 + private void stopAnimationTask() { 1.785 + if (mAnimationRenderTask != null) { 1.786 + mAnimationRenderTask.terminate(); 1.787 + mTarget.removeRenderTask(mAnimationRenderTask); 1.788 + mAnimationRenderTask = null; 1.789 + } 1.790 + } 1.791 + 1.792 + private float getVelocity() { 1.793 + float xvel = mX.getRealVelocity(); 1.794 + float yvel = mY.getRealVelocity(); 1.795 + return FloatMath.sqrt(xvel * xvel + yvel * yvel); 1.796 + } 1.797 + 1.798 + @Override 1.799 + public PointF getVelocityVector() { 1.800 + return new PointF(mX.getRealVelocity(), mY.getRealVelocity()); 1.801 + } 1.802 + 1.803 + private boolean stopped() { 1.804 + return getVelocity() < STOPPED_THRESHOLD; 1.805 + } 1.806 + 1.807 + PointF resetDisplacement() { 1.808 + return new PointF(mX.resetDisplacement(), mY.resetDisplacement()); 1.809 + } 1.810 + 1.811 + private void updatePosition() { 1.812 + mX.displace(); 1.813 + mY.displace(); 1.814 + PointF displacement = resetDisplacement(); 1.815 + if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) { 1.816 + return; 1.817 + } 1.818 + if (mDefaultPrevented || mSubscroller.scrollBy(displacement)) { 1.819 + synchronized (mTarget.getLock()) { 1.820 + mTarget.scrollMarginsBy(displacement.x, displacement.y); 1.821 + } 1.822 + } else { 1.823 + synchronized (mTarget.getLock()) { 1.824 + scrollBy(displacement.x, displacement.y); 1.825 + } 1.826 + } 1.827 + } 1.828 + 1.829 + /** 1.830 + * This class is an implementation of RenderTask which enforces its implementor to run in the UI thread. 1.831 + * 1.832 + */ 1.833 + private abstract class PanZoomRenderTask extends RenderTask { 1.834 + 1.835 + /** 1.836 + * the time when the current frame was started in ns. 1.837 + */ 1.838 + protected long mCurrentFrameStartTime; 1.839 + /** 1.840 + * The current frame duration in ns. 1.841 + */ 1.842 + protected long mLastFrameTimeDelta; 1.843 + 1.844 + private final Runnable mRunnable = new Runnable() { 1.845 + @Override 1.846 + public final void run() { 1.847 + if (mContinueAnimation) { 1.848 + animateFrame(); 1.849 + } 1.850 + } 1.851 + }; 1.852 + 1.853 + private boolean mContinueAnimation = true; 1.854 + 1.855 + public PanZoomRenderTask() { 1.856 + super(false); 1.857 + } 1.858 + 1.859 + @Override 1.860 + protected final boolean internalRun(long timeDelta, long currentFrameStartTime) { 1.861 + 1.862 + mCurrentFrameStartTime = currentFrameStartTime; 1.863 + mLastFrameTimeDelta = timeDelta; 1.864 + 1.865 + mTarget.post(mRunnable); 1.866 + return mContinueAnimation; 1.867 + } 1.868 + 1.869 + /** 1.870 + * The method subclasses must override. This method is run on the UI thread thanks to internalRun 1.871 + */ 1.872 + protected abstract void animateFrame(); 1.873 + 1.874 + /** 1.875 + * Terminate the animation. 1.876 + */ 1.877 + public void terminate() { 1.878 + mContinueAnimation = false; 1.879 + } 1.880 + } 1.881 + 1.882 + private class AutonavRenderTask extends PanZoomRenderTask { 1.883 + public AutonavRenderTask() { 1.884 + super(); 1.885 + } 1.886 + 1.887 + @Override 1.888 + protected void animateFrame() { 1.889 + if (mState != PanZoomState.AUTONAV) { 1.890 + finishAnimation(); 1.891 + return; 1.892 + } 1.893 + 1.894 + updatePosition(); 1.895 + synchronized (mTarget.getLock()) { 1.896 + mTarget.setViewportMetrics(applyZoomDelta(getMetrics(), mAutonavZoomDelta)); 1.897 + } 1.898 + } 1.899 + } 1.900 + 1.901 + /* The task that performs the bounce animation. */ 1.902 + private class BounceRenderTask extends PanZoomRenderTask { 1.903 + 1.904 + /* 1.905 + * The viewport metrics that represent the start and end of the bounce-back animation, 1.906 + * respectively. 1.907 + */ 1.908 + private ImmutableViewportMetrics mBounceStartMetrics; 1.909 + private ImmutableViewportMetrics mBounceEndMetrics; 1.910 + // How long ago this bounce was started in ns. 1.911 + private long mBounceDuration; 1.912 + 1.913 + BounceRenderTask(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) { 1.914 + super(); 1.915 + mBounceStartMetrics = startMetrics; 1.916 + mBounceEndMetrics = endMetrics; 1.917 + } 1.918 + 1.919 + @Override 1.920 + protected void animateFrame() { 1.921 + /* 1.922 + * The pan/zoom controller might have signaled to us that it wants to abort the 1.923 + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail 1.924 + * out. 1.925 + */ 1.926 + if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) { 1.927 + finishAnimation(); 1.928 + return; 1.929 + } 1.930 + 1.931 + /* Perform the next frame of the bounce-back animation. */ 1.932 + mBounceDuration = mCurrentFrameStartTime - getStartTime(); 1.933 + if (mBounceDuration < BOUNCE_ANIMATION_DURATION) { 1.934 + advanceBounce(); 1.935 + return; 1.936 + } 1.937 + 1.938 + /* Finally, if there's nothing else to do, complete the animation and go to sleep. */ 1.939 + finishBounce(); 1.940 + finishAnimation(); 1.941 + setState(PanZoomState.NOTHING); 1.942 + } 1.943 + 1.944 + /* Performs one frame of a bounce animation. */ 1.945 + private void advanceBounce() { 1.946 + synchronized (mTarget.getLock()) { 1.947 + float t = easeOut((float)mBounceDuration / BOUNCE_ANIMATION_DURATION); 1.948 + ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); 1.949 + mTarget.setViewportMetrics(newMetrics); 1.950 + } 1.951 + } 1.952 + 1.953 + /* Concludes a bounce animation and snaps the viewport into place. */ 1.954 + private void finishBounce() { 1.955 + synchronized (mTarget.getLock()) { 1.956 + mTarget.setViewportMetrics(mBounceEndMetrics); 1.957 + } 1.958 + } 1.959 + } 1.960 + 1.961 + // The callback that performs the fling animation. 1.962 + private class FlingRenderTask extends PanZoomRenderTask { 1.963 + 1.964 + public FlingRenderTask() { 1.965 + super(); 1.966 + } 1.967 + 1.968 + @Override 1.969 + protected void animateFrame() { 1.970 + /* 1.971 + * The pan/zoom controller might have signaled to us that it wants to abort the 1.972 + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail 1.973 + * out. 1.974 + */ 1.975 + if (mState != PanZoomState.FLING) { 1.976 + finishAnimation(); 1.977 + return; 1.978 + } 1.979 + 1.980 + /* Advance flings, if necessary. */ 1.981 + boolean flingingX = mX.advanceFling(mLastFrameTimeDelta); 1.982 + boolean flingingY = mY.advanceFling(mLastFrameTimeDelta); 1.983 + 1.984 + boolean overscrolled = (mX.overscrolled() || mY.overscrolled()); 1.985 + 1.986 + /* If we're still flinging in any direction, update the origin. */ 1.987 + if (flingingX || flingingY) { 1.988 + updatePosition(); 1.989 + 1.990 + /* 1.991 + * Check to see if we're still flinging with an appreciable velocity. The threshold is 1.992 + * higher in the case of overscroll, so we bounce back eagerly when overscrolling but 1.993 + * coast smoothly to a stop when not. In other words, require a greater velocity to 1.994 + * maintain the fling once we enter overscroll. 1.995 + */ 1.996 + float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD); 1.997 + if (getVelocity() >= threshold) { 1.998 + // we're still flinging 1.999 + return; 1.1000 + } 1.1001 + 1.1002 + mX.stopFling(); 1.1003 + mY.stopFling(); 1.1004 + } 1.1005 + 1.1006 + /* Perform a bounce-back animation if overscrolled. */ 1.1007 + if (overscrolled) { 1.1008 + bounce(); 1.1009 + } else { 1.1010 + finishAnimation(); 1.1011 + setState(PanZoomState.NOTHING); 1.1012 + } 1.1013 + } 1.1014 + } 1.1015 + 1.1016 + private void finishAnimation() { 1.1017 + checkMainThread(); 1.1018 + 1.1019 + stopAnimationTask(); 1.1020 + 1.1021 + // Force a viewport synchronisation 1.1022 + mTarget.forceRedraw(null); 1.1023 + } 1.1024 + 1.1025 + /* Returns the nearest viewport metrics with no overscroll visible. */ 1.1026 + private ImmutableViewportMetrics getValidViewportMetrics() { 1.1027 + return getValidViewportMetrics(getMetrics()); 1.1028 + } 1.1029 + 1.1030 + private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) { 1.1031 + /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */ 1.1032 + float zoomFactor = viewportMetrics.zoomFactor; 1.1033 + RectF pageRect = viewportMetrics.getPageRect(); 1.1034 + RectF viewport = viewportMetrics.getViewport(); 1.1035 + 1.1036 + float focusX = viewport.width() / 2.0f; 1.1037 + float focusY = viewport.height() / 2.0f; 1.1038 + 1.1039 + float minZoomFactor = 0.0f; 1.1040 + float maxZoomFactor = MAX_ZOOM; 1.1041 + 1.1042 + ZoomConstraints constraints = mTarget.getZoomConstraints(); 1.1043 + 1.1044 + if (constraints.getMinZoom() > 0) 1.1045 + minZoomFactor = constraints.getMinZoom(); 1.1046 + if (constraints.getMaxZoom() > 0) 1.1047 + maxZoomFactor = constraints.getMaxZoom(); 1.1048 + 1.1049 + if (!constraints.getAllowZoom()) { 1.1050 + // If allowZoom is false, clamp to the default zoom level. 1.1051 + maxZoomFactor = minZoomFactor = constraints.getDefaultZoom(); 1.1052 + } 1.1053 + 1.1054 + // Ensure minZoomFactor keeps the page at least as big as the viewport. 1.1055 + if (pageRect.width() > 0) { 1.1056 + float pageWidth = pageRect.width() + 1.1057 + viewportMetrics.marginLeft + 1.1058 + viewportMetrics.marginRight; 1.1059 + float scaleFactor = viewport.width() / pageWidth; 1.1060 + minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); 1.1061 + if (viewport.width() > pageWidth) 1.1062 + focusX = 0.0f; 1.1063 + } 1.1064 + if (pageRect.height() > 0) { 1.1065 + float pageHeight = pageRect.height() + 1.1066 + viewportMetrics.marginTop + 1.1067 + viewportMetrics.marginBottom; 1.1068 + float scaleFactor = viewport.height() / pageHeight; 1.1069 + minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); 1.1070 + if (viewport.height() > pageHeight) 1.1071 + focusY = 0.0f; 1.1072 + } 1.1073 + 1.1074 + maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor); 1.1075 + 1.1076 + if (zoomFactor < minZoomFactor) { 1.1077 + // if one (or both) of the page dimensions is smaller than the viewport, 1.1078 + // zoom using the top/left as the focus on that axis. this prevents the 1.1079 + // scenario where, if both dimensions are smaller than the viewport, but 1.1080 + // by different scale factors, we end up scrolled to the end on one axis 1.1081 + // after applying the scale 1.1082 + PointF center = new PointF(focusX, focusY); 1.1083 + viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center); 1.1084 + } else if (zoomFactor > maxZoomFactor) { 1.1085 + PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f); 1.1086 + viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center); 1.1087 + } 1.1088 + 1.1089 + /* Now we pan to the right origin. */ 1.1090 + viewportMetrics = viewportMetrics.clampWithMargins(); 1.1091 + 1.1092 + return viewportMetrics; 1.1093 + } 1.1094 + 1.1095 + private class AxisX extends Axis { 1.1096 + AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); } 1.1097 + @Override 1.1098 + public float getOrigin() { return getMetrics().viewportRectLeft; } 1.1099 + @Override 1.1100 + protected float getViewportLength() { return getMetrics().getWidth(); } 1.1101 + @Override 1.1102 + protected float getPageStart() { return getMetrics().pageRectLeft; } 1.1103 + @Override 1.1104 + protected float getMarginStart() { return mTarget.getMaxMargins().left - getMetrics().marginLeft; } 1.1105 + @Override 1.1106 + protected float getMarginEnd() { return mTarget.getMaxMargins().right - getMetrics().marginRight; } 1.1107 + @Override 1.1108 + protected float getPageLength() { return getMetrics().getPageWidthWithMargins(); } 1.1109 + @Override 1.1110 + protected boolean marginsHidden() { 1.1111 + ImmutableViewportMetrics metrics = getMetrics(); 1.1112 + RectF maxMargins = mTarget.getMaxMargins(); 1.1113 + return (metrics.marginLeft < maxMargins.left || metrics.marginRight < maxMargins.right); 1.1114 + } 1.1115 + @Override 1.1116 + protected void overscrollFling(final float velocity) { 1.1117 + if (mOverscroll != null) { 1.1118 + mOverscroll.setVelocity(velocity, Overscroll.Axis.X); 1.1119 + } 1.1120 + } 1.1121 + @Override 1.1122 + protected void overscrollPan(final float distance) { 1.1123 + if (mOverscroll != null) { 1.1124 + mOverscroll.setDistance(distance, Overscroll.Axis.X); 1.1125 + } 1.1126 + } 1.1127 + } 1.1128 + 1.1129 + private class AxisY extends Axis { 1.1130 + AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); } 1.1131 + @Override 1.1132 + public float getOrigin() { return getMetrics().viewportRectTop; } 1.1133 + @Override 1.1134 + protected float getViewportLength() { return getMetrics().getHeight(); } 1.1135 + @Override 1.1136 + protected float getPageStart() { return getMetrics().pageRectTop; } 1.1137 + @Override 1.1138 + protected float getPageLength() { return getMetrics().getPageHeightWithMargins(); } 1.1139 + @Override 1.1140 + protected float getMarginStart() { return mTarget.getMaxMargins().top - getMetrics().marginTop; } 1.1141 + @Override 1.1142 + protected float getMarginEnd() { return mTarget.getMaxMargins().bottom - getMetrics().marginBottom; } 1.1143 + @Override 1.1144 + protected boolean marginsHidden() { 1.1145 + ImmutableViewportMetrics metrics = getMetrics(); 1.1146 + RectF maxMargins = mTarget.getMaxMargins(); 1.1147 + return (metrics.marginTop < maxMargins.top || metrics.marginBottom < maxMargins.bottom); 1.1148 + } 1.1149 + @Override 1.1150 + protected void overscrollFling(final float velocity) { 1.1151 + if (mOverscroll != null) { 1.1152 + mOverscroll.setVelocity(velocity, Overscroll.Axis.Y); 1.1153 + } 1.1154 + } 1.1155 + @Override 1.1156 + protected void overscrollPan(final float distance) { 1.1157 + if (mOverscroll != null) { 1.1158 + mOverscroll.setDistance(distance, Overscroll.Axis.Y); 1.1159 + } 1.1160 + } 1.1161 + } 1.1162 + 1.1163 + /* 1.1164 + * Zooming 1.1165 + */ 1.1166 + @Override 1.1167 + public boolean onScaleBegin(SimpleScaleGestureDetector detector) { 1.1168 + if (mState == PanZoomState.ANIMATED_ZOOM) 1.1169 + return false; 1.1170 + 1.1171 + if (!mTarget.getZoomConstraints().getAllowZoom()) 1.1172 + return false; 1.1173 + 1.1174 + setState(PanZoomState.PINCHING); 1.1175 + mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); 1.1176 + cancelTouch(); 1.1177 + 1.1178 + GeckoAppShell.sendEventToGecko(GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_START, mLastZoomFocus, getMetrics().zoomFactor)); 1.1179 + 1.1180 + return true; 1.1181 + } 1.1182 + 1.1183 + @Override 1.1184 + public boolean onScale(SimpleScaleGestureDetector detector) { 1.1185 + if (mTarget.isFullScreen()) 1.1186 + return false; 1.1187 + 1.1188 + if (mState != PanZoomState.PINCHING) 1.1189 + return false; 1.1190 + 1.1191 + float prevSpan = detector.getPreviousSpan(); 1.1192 + if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) { 1.1193 + // let's eat this one to avoid setting the new zoom to infinity (bug 711453) 1.1194 + return true; 1.1195 + } 1.1196 + 1.1197 + synchronized (mTarget.getLock()) { 1.1198 + float zoomFactor = getAdjustedZoomFactor(detector.getCurrentSpan() / prevSpan); 1.1199 + scrollBy(mLastZoomFocus.x - detector.getFocusX(), 1.1200 + mLastZoomFocus.y - detector.getFocusY()); 1.1201 + mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY()); 1.1202 + ImmutableViewportMetrics target = getMetrics().scaleTo(zoomFactor, mLastZoomFocus); 1.1203 + 1.1204 + // If overscroll is diabled, prevent zooming outside the normal document pans. 1.1205 + if (mX.getOverScrollMode() == View.OVER_SCROLL_NEVER || mY.getOverScrollMode() == View.OVER_SCROLL_NEVER) { 1.1206 + target = getValidViewportMetrics(target); 1.1207 + } 1.1208 + mTarget.setViewportMetrics(target); 1.1209 + } 1.1210 + 1.1211 + GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY, mLastZoomFocus, getMetrics().zoomFactor); 1.1212 + GeckoAppShell.sendEventToGecko(event); 1.1213 + 1.1214 + return true; 1.1215 + } 1.1216 + 1.1217 + private ImmutableViewportMetrics applyZoomDelta(ImmutableViewportMetrics metrics, float zoomDelta) { 1.1218 + float oldZoom = metrics.zoomFactor; 1.1219 + float newZoom = oldZoom + zoomDelta; 1.1220 + float adjustedZoom = getAdjustedZoomFactor(newZoom / oldZoom); 1.1221 + // since we don't have a particular focus to zoom to, just use the center 1.1222 + PointF center = new PointF(metrics.getWidth() / 2.0f, metrics.getHeight() / 2.0f); 1.1223 + metrics = metrics.scaleTo(adjustedZoom, center); 1.1224 + return metrics; 1.1225 + } 1.1226 + 1.1227 + private boolean animatedScale(float zoomDelta) { 1.1228 + if (mState != PanZoomState.NOTHING && mState != PanZoomState.BOUNCE) { 1.1229 + return false; 1.1230 + } 1.1231 + synchronized (mTarget.getLock()) { 1.1232 + ImmutableViewportMetrics metrics = applyZoomDelta(getMetrics(), zoomDelta); 1.1233 + bounce(getValidViewportMetrics(metrics), PanZoomState.BOUNCE); 1.1234 + } 1.1235 + return true; 1.1236 + } 1.1237 + 1.1238 + private float getAdjustedZoomFactor(float zoomRatio) { 1.1239 + /* 1.1240 + * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom 1.1241 + * factor toward 1.0. 1.1242 + */ 1.1243 + float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true)); 1.1244 + if (zoomRatio > 1.0f) 1.1245 + zoomRatio = 1.0f + (zoomRatio - 1.0f) * resistance; 1.1246 + else 1.1247 + zoomRatio = 1.0f - (1.0f - zoomRatio) * resistance; 1.1248 + 1.1249 + float newZoomFactor = getMetrics().zoomFactor * zoomRatio; 1.1250 + float minZoomFactor = 0.0f; 1.1251 + float maxZoomFactor = MAX_ZOOM; 1.1252 + 1.1253 + ZoomConstraints constraints = mTarget.getZoomConstraints(); 1.1254 + 1.1255 + if (constraints.getMinZoom() > 0) 1.1256 + minZoomFactor = constraints.getMinZoom(); 1.1257 + if (constraints.getMaxZoom() > 0) 1.1258 + maxZoomFactor = constraints.getMaxZoom(); 1.1259 + 1.1260 + if (newZoomFactor < minZoomFactor) { 1.1261 + // apply resistance when zooming past minZoomFactor, 1.1262 + // such that it asymptotically reaches minZoomFactor / 2.0 1.1263 + // but never exceeds that 1.1264 + final float rate = 0.5f; // controls how quickly we approach the limit 1.1265 + float excessZoom = minZoomFactor - newZoomFactor; 1.1266 + excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate); 1.1267 + newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f); 1.1268 + } 1.1269 + 1.1270 + if (newZoomFactor > maxZoomFactor) { 1.1271 + // apply resistance when zooming past maxZoomFactor, 1.1272 + // such that it asymptotically reaches maxZoomFactor + 1.0 1.1273 + // but never exceeds that 1.1274 + float excessZoom = newZoomFactor - maxZoomFactor; 1.1275 + excessZoom = 1.0f - (float)Math.exp(-excessZoom); 1.1276 + newZoomFactor = maxZoomFactor + excessZoom; 1.1277 + } 1.1278 + 1.1279 + return newZoomFactor; 1.1280 + } 1.1281 + 1.1282 + @Override 1.1283 + public void onScaleEnd(SimpleScaleGestureDetector detector) { 1.1284 + if (mState == PanZoomState.ANIMATED_ZOOM) 1.1285 + return; 1.1286 + 1.1287 + // switch back to the touching state 1.1288 + startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime()); 1.1289 + 1.1290 + // Force a viewport synchronisation 1.1291 + mTarget.forceRedraw(null); 1.1292 + 1.1293 + PointF point = new PointF(detector.getFocusX(), detector.getFocusY()); 1.1294 + GeckoEvent event = GeckoEvent.createNativeGestureEvent(GeckoEvent.ACTION_MAGNIFY_END, point, getMetrics().zoomFactor); 1.1295 + 1.1296 + if (event == null) { 1.1297 + return; 1.1298 + } 1.1299 + 1.1300 + GeckoAppShell.sendEventToGecko(event); 1.1301 + } 1.1302 + 1.1303 + @Override 1.1304 + public boolean getRedrawHint() { 1.1305 + switch (mState) { 1.1306 + case PINCHING: 1.1307 + case ANIMATED_ZOOM: 1.1308 + case BOUNCE: 1.1309 + // don't redraw during these because the zoom is (or might be, in the case 1.1310 + // of BOUNCE) be changing rapidly and gecko will have to redraw the entire 1.1311 + // display port area. we trigger a force-redraw upon exiting these states. 1.1312 + return false; 1.1313 + default: 1.1314 + // allow redrawing in other states 1.1315 + return true; 1.1316 + } 1.1317 + } 1.1318 + 1.1319 + private void sendPointToGecko(String event, MotionEvent motionEvent) { 1.1320 + String json; 1.1321 + try { 1.1322 + PointF point = new PointF(motionEvent.getX(), motionEvent.getY()); 1.1323 + point = mTarget.convertViewPointToLayerPoint(point); 1.1324 + if (point == null) { 1.1325 + return; 1.1326 + } 1.1327 + json = PointUtils.toJSON(point).toString(); 1.1328 + } catch (Exception e) { 1.1329 + Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e); 1.1330 + return; 1.1331 + } 1.1332 + 1.1333 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json)); 1.1334 + } 1.1335 + 1.1336 + @Override 1.1337 + public boolean onDown(MotionEvent motionEvent) { 1.1338 + mMediumPress = false; 1.1339 + return false; 1.1340 + } 1.1341 + 1.1342 + @Override 1.1343 + public void onShowPress(MotionEvent motionEvent) { 1.1344 + // If we get this, it will be followed either by a call to 1.1345 + // onSingleTapUp (if the user lifts their finger before the 1.1346 + // long-press timeout) or a call to onLongPress (if the user 1.1347 + // does not). In the former case, we want to make sure it is 1.1348 + // treated as a click. (Note that if this is called, we will 1.1349 + // not get a call to onDoubleTap). 1.1350 + mMediumPress = true; 1.1351 + } 1.1352 + 1.1353 + @Override 1.1354 + public void onLongPress(MotionEvent motionEvent) { 1.1355 + sendPointToGecko("Gesture:LongPress", motionEvent); 1.1356 + } 1.1357 + 1.1358 + @Override 1.1359 + public boolean onSingleTapUp(MotionEvent motionEvent) { 1.1360 + // When double-tapping is allowed, we have to wait to see if this is 1.1361 + // going to be a double-tap. 1.1362 + // However, if mMediumPress is true then we know there will be no 1.1363 + // double-tap so we treat this as a click. 1.1364 + if (mMediumPress || !mTarget.getZoomConstraints().getAllowDoubleTapZoom()) { 1.1365 + sendPointToGecko("Gesture:SingleTap", motionEvent); 1.1366 + } 1.1367 + // return false because we still want to get the ACTION_UP event that triggers this 1.1368 + return false; 1.1369 + } 1.1370 + 1.1371 + @Override 1.1372 + public boolean onSingleTapConfirmed(MotionEvent motionEvent) { 1.1373 + // When zooming is disabled, we handle this in onSingleTapUp. 1.1374 + if (mTarget.getZoomConstraints().getAllowDoubleTapZoom()) { 1.1375 + sendPointToGecko("Gesture:SingleTap", motionEvent); 1.1376 + } 1.1377 + return true; 1.1378 + } 1.1379 + 1.1380 + @Override 1.1381 + public boolean onDoubleTap(MotionEvent motionEvent) { 1.1382 + if (mTarget.getZoomConstraints().getAllowDoubleTapZoom()) { 1.1383 + sendPointToGecko("Gesture:DoubleTap", motionEvent); 1.1384 + } 1.1385 + return true; 1.1386 + } 1.1387 + 1.1388 + private void cancelTouch() { 1.1389 + GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", ""); 1.1390 + GeckoAppShell.sendEventToGecko(e); 1.1391 + } 1.1392 + 1.1393 + /** 1.1394 + * Zoom to a specified rect IN CSS PIXELS. 1.1395 + * 1.1396 + * While we usually use device pixels, @zoomToRect must be specified in CSS 1.1397 + * pixels. 1.1398 + */ 1.1399 + private ImmutableViewportMetrics getMetricsToZoomTo(RectF zoomToRect) { 1.1400 + final float startZoom = getMetrics().zoomFactor; 1.1401 + 1.1402 + RectF viewport = getMetrics().getViewport(); 1.1403 + // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport, 1.1404 + // enlarging as necessary (if it gets too big, it will get shrunk in the next step). 1.1405 + // while enlarging make sure we enlarge equally on both sides to keep the target rect 1.1406 + // centered. 1.1407 + float targetRatio = viewport.width() / viewport.height(); 1.1408 + float rectRatio = zoomToRect.width() / zoomToRect.height(); 1.1409 + if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) { 1.1410 + // all good, do nothing 1.1411 + } else if (targetRatio < rectRatio) { 1.1412 + // need to increase zoomToRect height 1.1413 + float newHeight = zoomToRect.width() / targetRatio; 1.1414 + zoomToRect.top -= (newHeight - zoomToRect.height()) / 2; 1.1415 + zoomToRect.bottom = zoomToRect.top + newHeight; 1.1416 + } else { // targetRatio > rectRatio) { 1.1417 + // need to increase zoomToRect width 1.1418 + float newWidth = targetRatio * zoomToRect.height(); 1.1419 + zoomToRect.left -= (newWidth - zoomToRect.width()) / 2; 1.1420 + zoomToRect.right = zoomToRect.left + newWidth; 1.1421 + } 1.1422 + 1.1423 + float finalZoom = viewport.width() / zoomToRect.width(); 1.1424 + 1.1425 + ImmutableViewportMetrics finalMetrics = getMetrics(); 1.1426 + finalMetrics = finalMetrics.setViewportOrigin( 1.1427 + zoomToRect.left * finalMetrics.zoomFactor, 1.1428 + zoomToRect.top * finalMetrics.zoomFactor); 1.1429 + finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f)); 1.1430 + 1.1431 + // 2. now run getValidViewportMetrics on it, so that the target viewport is 1.1432 + // clamped down to prevent overscroll, over-zoom, and other bad conditions. 1.1433 + finalMetrics = getValidViewportMetrics(finalMetrics); 1.1434 + return finalMetrics; 1.1435 + } 1.1436 + 1.1437 + private boolean animatedZoomTo(RectF zoomToRect) { 1.1438 + bounce(getMetricsToZoomTo(zoomToRect), PanZoomState.ANIMATED_ZOOM); 1.1439 + return true; 1.1440 + } 1.1441 + 1.1442 + /** This function must be called from the UI thread. */ 1.1443 + @Override 1.1444 + public void abortPanning() { 1.1445 + checkMainThread(); 1.1446 + bounce(); 1.1447 + } 1.1448 + 1.1449 + @Override 1.1450 + public void setOverScrollMode(int overscrollMode) { 1.1451 + mX.setOverScrollMode(overscrollMode); 1.1452 + mY.setOverScrollMode(overscrollMode); 1.1453 + } 1.1454 + 1.1455 + @Override 1.1456 + public int getOverScrollMode() { 1.1457 + return mX.getOverScrollMode(); 1.1458 + } 1.1459 + 1.1460 + @Override 1.1461 + public void setOverscrollHandler(final Overscroll handler) { 1.1462 + mOverscroll = handler; 1.1463 + } 1.1464 +}