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