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