1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org
.mozilla
.gecko
.gfx
;
8 import android
.graphics
.PointF
;
9 import android
.graphics
.RectF
;
10 import android
.os
.Build
;
11 import android
.util
.Log
;
12 import android
.view
.GestureDetector
;
13 import android
.view
.InputDevice
;
14 import android
.view
.MotionEvent
;
15 import android
.view
.View
;
17 import org
.libreoffice
.LOKitShell
;
18 import org
.libreoffice
.LibreOfficeMainActivity
;
19 import org
.mozilla
.gecko
.ZoomConstraints
;
20 import org
.mozilla
.gecko
.util
.FloatUtils
;
22 import java
.util
.Timer
;
23 import java
.util
.TimerTask
;
24 import java
.lang
.StrictMath
;
27 * Handles the kinetic scrolling and zooming physics for a layer controller.
29 * Many ideas are from Joe Hewitt's Scrollability:
30 * https://github.com/joehewitt/scrollability/
32 class JavaPanZoomController
33 extends GestureDetector
.SimpleOnGestureListener
34 implements PanZoomController
, SimpleScaleGestureDetector
.SimpleScaleGestureListener
36 private static final String LOGTAG
= "GeckoPanZoomController";
38 // Animation stops if the velocity is below this value when overscrolled or panning.
39 private static final float STOPPED_THRESHOLD
= 4.0f
;
41 // Animation stops is the velocity is below this threshold when flinging.
42 private static final float FLING_STOPPED_THRESHOLD
= 0.1f
;
44 // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
45 // between the touch-down and touch-up of a click). In units of density-independent pixels.
46 private final float PAN_THRESHOLD
;
48 // Angle from axis within which we stay axis-locked
49 private static final double AXIS_LOCK_ANGLE
= Math
.PI
/ 6.0; // 30 degrees
51 // The maximum amount we allow you to zoom into a page
52 private static final float MAX_ZOOM
= 4.0f
;
54 // The threshold zoom factor of whether a double tap triggers zoom-in or zoom-out
55 private static final float DOUBLE_TAP_THRESHOLD
= 1.0f
;
57 // The maximum amount we would like to scroll with the mouse
58 private final float MAX_SCROLL
;
60 private enum PanZoomState
{
61 NOTHING
, /* no touch-start events received */
62 FLING
, /* all touches removed, but we're still scrolling page */
63 TOUCHING
, /* one touch-start event received */
64 PANNING_LOCKED
, /* touch-start followed by move (i.e. panning with axis lock) */
65 PANNING
, /* panning without axis lock */
66 PANNING_HOLD
, /* in panning, but not moving.
67 * similar to TOUCHING but after starting a pan */
68 PANNING_HOLD_LOCKED
, /* like PANNING_HOLD, but axis lock still in effect */
69 PINCHING
, /* nth touch-start, where n > 1. this mode allows pan and zoom */
70 ANIMATED_ZOOM
, /* animated zoom to a new rect */
71 BOUNCE
, /* in a bounce animation */
73 WAITING_LISTENERS
, /* a state halfway between NOTHING and TOUCHING - the user has
74 put a finger down, but we don't yet know if a touch listener has
75 prevented the default actions yet. we still need to abort animations. */
78 private final PanZoomTarget mTarget
;
79 private final SubdocumentScrollHelper mSubscroller
;
80 private final Axis mX
;
81 private final Axis mY
;
82 private final TouchEventHandler mTouchEventHandler
;
83 private Thread mMainThread
;
84 private LibreOfficeMainActivity mContext
;
86 /* The timer that handles flings or bounces. */
87 private Timer mAnimationTimer
;
88 /* The runnable being scheduled by the animation timer. */
89 private AnimationRunnable mAnimationRunnable
;
90 /* The zoom focus at the first zoom event (in page coordinates). */
91 private PointF mLastZoomFocus
;
92 /* The time the last motion event took place. */
93 private long mLastEventTime
;
94 /* Current state the pan/zoom UI is in. */
95 private PanZoomState mState
;
96 /* Whether or not to wait for a double-tap before dispatching a single-tap */
97 private boolean mWaitForDoubleTap
;
99 JavaPanZoomController(LibreOfficeMainActivity context
, PanZoomTarget target
, View view
) {
101 PAN_THRESHOLD
= 1/16f
* LOKitShell
.getDpi(view
.getContext());
102 MAX_SCROLL
= 0.075f
* LOKitShell
.getDpi(view
.getContext());
104 mSubscroller
= new SubdocumentScrollHelper();
105 mX
= new AxisX(mSubscroller
);
106 mY
= new AxisY(mSubscroller
);
107 mTouchEventHandler
= new TouchEventHandler(view
.getContext(), view
, this);
109 mMainThread
= mContext
.getMainLooper().getThread();
112 setState(PanZoomState
.NOTHING
);
115 public void destroy() {
116 mSubscroller
.destroy();
117 mTouchEventHandler
.destroy();
120 private static float easeOut(float t
) {
127 private void setState(PanZoomState state
) {
128 if (state
!= mState
) {
133 private ImmutableViewportMetrics
getMetrics() {
134 return mTarget
.getViewportMetrics();
137 // for debugging bug 713011; it can be taken out once that is resolved.
138 private void checkMainThread() {
139 if (mMainThread
!= Thread
.currentThread()) {
140 // log with full stack trace
141 Log
.e(LOGTAG
, "Uh-oh, we're running on the wrong thread!", new Exception());
145 /** This function MUST be called on the UI thread */
146 public boolean onMotionEvent(MotionEvent event
) {
147 if ((event
.getSource() & InputDevice
.SOURCE_CLASS_MASK
) == InputDevice
.SOURCE_CLASS_POINTER
148 && (event
.getAction() & MotionEvent
.ACTION_MASK
) == MotionEvent
.ACTION_SCROLL
) {
149 return handlePointerScroll(event
);
154 /** This function MUST be called on the UI thread */
155 public boolean onTouchEvent(MotionEvent event
) {
156 return mTouchEventHandler
.handleEvent(event
);
159 boolean handleEvent(MotionEvent event
) {
160 switch (event
.getAction() & MotionEvent
.ACTION_MASK
) {
161 case MotionEvent
.ACTION_DOWN
: return handleTouchStart(event
);
162 case MotionEvent
.ACTION_MOVE
: return handleTouchMove(event
);
163 case MotionEvent
.ACTION_UP
: return handleTouchEnd(event
);
164 case MotionEvent
.ACTION_CANCEL
: return handleTouchCancel(event
);
169 /** This function MUST be called on the UI thread */
170 public void notifyDefaultActionPrevented(boolean prevented
) {
171 mTouchEventHandler
.handleEventListenerAction(!prevented
);
174 /** This function must be called from the UI thread. */
175 public void abortAnimation() {
177 // this happens when gecko changes the viewport on us or if the device is rotated.
178 // if that's the case, abort any animation in progress and re-zoom so that the page
179 // snaps to edges. for other cases (where the user's finger(s) are down) don't do
188 // the zoom that's in progress likely makes no sense any more (such as if
189 // the screen orientation changed) so abort it
190 setState(PanZoomState
.NOTHING
);
193 // Don't do animations here; they're distracting and can cause flashes on page
195 synchronized (mTarget
.getLock()) {
196 mTarget
.setViewportMetrics(getValidViewportMetrics());
197 mTarget
.forceRedraw();
203 /** This function must be called on the UI thread. */
204 void startingNewEventBlock(MotionEvent event
, boolean waitingForTouchListeners
) {
206 mSubscroller
.cancel();
207 if (waitingForTouchListeners
&& (event
.getAction() & MotionEvent
.ACTION_MASK
) == MotionEvent
.ACTION_DOWN
) {
208 // this is the first touch point going down, so we enter the pending state
209 // setting the state will kill any animations in progress, possibly leaving
210 // the page in overscroll
211 setState(PanZoomState
.WAITING_LISTENERS
);
215 /** This function must be called on the UI thread. */
216 void preventedTouchFinished() {
218 if (mState
== PanZoomState
.WAITING_LISTENERS
) {
219 // if we enter here, we just finished a block of events whose default actions
220 // were prevented by touch listeners. Now there are no touch points left, so
221 // we need to reset our state and re-bounce because we might be in overscroll
226 /** This must be called on the UI thread. */
227 public void pageRectUpdated() {
228 if (mState
== PanZoomState
.NOTHING
) {
229 synchronized (mTarget
.getLock()) {
230 ImmutableViewportMetrics validated
= getValidViewportMetrics();
231 if (!getMetrics().fuzzyEquals(validated
)) {
232 // page size changed such that we are now in overscroll. snap to
233 // the nearest valid viewport
234 mTarget
.setViewportMetrics(validated
);
244 private boolean handleTouchStart(MotionEvent event
) {
245 // user is taking control of movement, so stop
246 // any auto-movement we have going
247 stopAnimationTimer();
251 // We just interrupted a double-tap animation, so force a redraw in
252 // case this touchstart is just a tap that doesn't end up triggering
254 mTarget
.forceRedraw();
259 case WAITING_LISTENERS
:
260 startTouch(event
.getX(0), event
.getY(0), event
.getEventTime());
266 case PANNING_HOLD_LOCKED
:
268 Log
.e(LOGTAG
, "Received impossible touch down while in " + mState
);
271 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchStart");
275 private boolean handleTouchMove(MotionEvent event
) {
276 if (mState
== PanZoomState
.PANNING_LOCKED
|| mState
== PanZoomState
.PANNING
) {
277 if (getVelocity() > 18.0f
) {
278 mContext
.hideSoftKeyboard();
285 case WAITING_LISTENERS
:
286 // should never happen
287 Log
.e(LOGTAG
, "Received impossible touch move while in " + mState
);
291 // may happen if user double-taps and drags without lifting after the
292 // second tap. ignore the move if this happens.
296 // Don't allow panning if there is an element in full-screen mode. See bug 775511.
297 if (mTarget
.isFullScreen() || panDistance(event
) < PAN_THRESHOLD
) {
301 startPanning(event
.getX(0), event
.getY(0), event
.getEventTime());
305 case PANNING_HOLD_LOCKED
:
306 setState(PanZoomState
.PANNING_LOCKED
);
313 setState(PanZoomState
.PANNING
);
320 // scale gesture listener will handle this
323 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchMove");
327 private boolean handleTouchEnd(MotionEvent event
) {
332 case WAITING_LISTENERS
:
333 // should never happen
334 Log
.e(LOGTAG
, "Received impossible touch end while in " + mState
);
338 // may happen if user double-taps and drags without lifting after the
339 // second tap. ignore if this happens.
343 // the switch into TOUCHING might have happened while the page was
344 // snapping back after overscroll. we need to finish the snap if that
352 case PANNING_HOLD_LOCKED
:
353 setState(PanZoomState
.FLING
);
358 setState(PanZoomState
.NOTHING
);
361 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchEnd");
365 private boolean handleTouchCancel(MotionEvent event
) {
368 if (mState
== PanZoomState
.WAITING_LISTENERS
) {
369 // we might get a cancel event from the TouchEventHandler while in the
370 // WAITING_LISTENERS state if the touch listeners prevent-default the
371 // block of events. at this point being in WAITING_LISTENERS is equivalent
372 // to being in NOTHING with the exception of possibly being in overscroll.
373 // so here we don't want to do anything right now; the overscroll will be
374 // corrected in preventedTouchFinished().
378 // ensure we snap back if we're overscrolled
383 private boolean handlePointerScroll(MotionEvent event
) {
384 if (mState
== PanZoomState
.NOTHING
|| mState
== PanZoomState
.FLING
) {
385 float scrollX
= event
.getAxisValue(MotionEvent
.AXIS_HSCROLL
);
386 float scrollY
= event
.getAxisValue(MotionEvent
.AXIS_VSCROLL
);
388 scrollBy(scrollX
* MAX_SCROLL
, scrollY
* MAX_SCROLL
);
395 private void startTouch(float x
, float y
, long time
) {
398 setState(PanZoomState
.TOUCHING
);
399 mLastEventTime
= time
;
402 private void startPanning(float x
, float y
, long time
) {
403 float dx
= mX
.panDistance(x
);
404 float dy
= mY
.panDistance(y
);
405 double angle
= Math
.atan2(dy
, dx
); // range [-pi, pi]
406 angle
= Math
.abs(angle
); // range [0, pi]
408 // When the touch move breaks through the pan threshold, reposition the touch down origin
409 // so the page won't jump when we start panning.
412 mLastEventTime
= time
;
414 if (!mX
.scrollable() || !mY
.scrollable()) {
415 setState(PanZoomState
.PANNING
);
416 } else if (angle
< AXIS_LOCK_ANGLE
|| angle
> (Math
.PI
- AXIS_LOCK_ANGLE
)) {
417 mY
.setScrollingDisabled(true);
418 setState(PanZoomState
.PANNING_LOCKED
);
419 } else if (Math
.abs(angle
- (Math
.PI
/ 2)) < AXIS_LOCK_ANGLE
) {
420 mX
.setScrollingDisabled(true);
421 setState(PanZoomState
.PANNING_LOCKED
);
423 setState(PanZoomState
.PANNING
);
427 private float panDistance(MotionEvent move
) {
428 float dx
= mX
.panDistance(move
.getX(0));
429 float dy
= mY
.panDistance(move
.getY(0));
430 return (float) Math
.hypot(dx
, dy
);
433 private void track(float x
, float y
, long time
) {
434 float timeDelta
= (float)(time
- mLastEventTime
);
435 if (FloatUtils
.fuzzyEquals(timeDelta
, 0)) {
436 // probably a duplicate event, ignore it. using a zero timeDelta will mess
440 mLastEventTime
= time
;
442 mX
.updateWithTouchAt(x
, timeDelta
);
443 mY
.updateWithTouchAt(y
, timeDelta
);
446 private void track(MotionEvent event
) {
450 for (int i
= 0; i
< event
.getHistorySize(); i
++) {
451 track(event
.getHistoricalX(0, i
),
452 event
.getHistoricalY(0, i
),
453 event
.getHistoricalEventTime(i
));
455 track(event
.getX(0), event
.getY(0), event
.getEventTime());
458 if (mState
== PanZoomState
.PANNING
) {
459 setState(PanZoomState
.PANNING_HOLD
);
460 } else if (mState
== PanZoomState
.PANNING_LOCKED
) {
461 setState(PanZoomState
.PANNING_HOLD_LOCKED
);
463 // should never happen, but handle anyway for robustness
464 Log
.e(LOGTAG
, "Impossible case " + mState
+ " when stopped in track");
465 setState(PanZoomState
.PANNING_HOLD_LOCKED
);
474 private void scrollBy(float dx
, float dy
) {
475 ImmutableViewportMetrics scrolled
= getMetrics().offsetViewportBy(dx
, dy
);
476 mTarget
.setViewportMetrics(scrolled
);
479 private void fling() {
482 stopAnimationTimer();
484 boolean stopped
= stopped();
485 mX
.startFling(stopped
);
486 mY
.startFling(stopped
);
488 startAnimationTimer(new FlingRunnable());
491 /* Performs a bounce-back animation to the given viewport metrics. */
492 private void bounce(ImmutableViewportMetrics metrics
, PanZoomState state
) {
493 stopAnimationTimer();
495 ImmutableViewportMetrics bounceStartMetrics
= getMetrics();
496 if (bounceStartMetrics
.fuzzyEquals(metrics
)) {
497 setState(PanZoomState
.NOTHING
);
504 // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
505 // getRedrawHint() is returning false. This means we can safely call
506 // setAnimationTarget to set the new final display port and not have it get
507 // clobbered by display ports from intermediate animation frames.
508 mTarget
.setAnimationTarget(metrics
);
509 startAnimationTimer(new BounceRunnable(bounceStartMetrics
, metrics
));
512 /* Performs a bounce-back animation to the nearest valid viewport metrics. */
513 private void bounce() {
514 bounce(getValidViewportMetrics(), PanZoomState
.BOUNCE
);
517 /* Starts the fling or bounce animation. */
518 private void startAnimationTimer(final AnimationRunnable runnable
) {
519 if (mAnimationTimer
!= null) {
520 Log
.e(LOGTAG
, "Attempted to start a new fling without canceling the old one!");
521 stopAnimationTimer();
524 mAnimationTimer
= new Timer("Animation Timer");
525 mAnimationRunnable
= runnable
;
526 mAnimationTimer
.scheduleAtFixedRate(new TimerTask() {
528 public void run() { mTarget
.post(runnable
); }
529 }, 0, (int)Axis
.MS_PER_FRAME
);
532 /* Stops the fling or bounce animation. */
533 private void stopAnimationTimer() {
534 if (mAnimationTimer
!= null) {
535 mAnimationTimer
.cancel();
536 mAnimationTimer
= null;
538 if (mAnimationRunnable
!= null) {
539 mAnimationRunnable
.terminate();
540 mAnimationRunnable
= null;
544 private float getVelocity() {
545 float xvel
= mX
.getRealVelocity();
546 float yvel
= mY
.getRealVelocity();
547 return (float) StrictMath
.hypot(xvel
, yvel
);
550 public PointF
getVelocityVector() {
551 return new PointF(mX
.getRealVelocity(), mY
.getRealVelocity());
554 private boolean stopped() {
555 return getVelocity() < STOPPED_THRESHOLD
;
558 private PointF
resetDisplacement() {
559 return new PointF(mX
.resetDisplacement(), mY
.resetDisplacement());
562 private void updatePosition() {
565 PointF displacement
= resetDisplacement();
566 if (FloatUtils
.fuzzyEquals(displacement
.x
, 0.0f
) && FloatUtils
.fuzzyEquals(displacement
.y
, 0.0f
)) {
569 if (! mSubscroller
.scrollBy(displacement
)) {
570 synchronized (mTarget
.getLock()) {
571 scrollBy(displacement
.x
, displacement
.y
);
576 private abstract class AnimationRunnable
implements Runnable
{
577 private boolean mAnimationTerminated
;
579 /* This should always run on the UI thread */
580 public final void run() {
582 * Since the animation timer queues this runnable on the UI thread, it
583 * is possible that even when the animation timer is cancelled, there
584 * are multiple instances of this queued, so we need to have another
585 * mechanism to abort. This is done by using the mAnimationTerminated flag.
587 if (mAnimationTerminated
) {
593 protected abstract void animateFrame();
595 /* This should always run on the UI thread */
596 final void terminate() {
597 mAnimationTerminated
= true;
601 /* The callback that performs the bounce animation. */
602 private class BounceRunnable
extends AnimationRunnable
{
603 /* The current frame of the bounce-back animation */
604 private int mBounceFrame
;
606 * The viewport metrics that represent the start and end of the bounce-back animation,
609 private ImmutableViewportMetrics mBounceStartMetrics
;
610 private ImmutableViewportMetrics mBounceEndMetrics
;
612 BounceRunnable(ImmutableViewportMetrics startMetrics
, ImmutableViewportMetrics endMetrics
) {
613 mBounceStartMetrics
= startMetrics
;
614 mBounceEndMetrics
= endMetrics
;
617 protected void animateFrame() {
619 * The pan/zoom controller might have signaled to us that it wants to abort the
620 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
623 if (!(mState
== PanZoomState
.BOUNCE
|| mState
== PanZoomState
.ANIMATED_ZOOM
)) {
628 /* Perform the next frame of the bounce-back animation. */
629 if (mBounceFrame
< (int)(256f
/Axis
.MS_PER_FRAME
)) {
634 /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
637 setState(PanZoomState
.NOTHING
);
640 /* Performs one frame of a bounce animation. */
641 private void advanceBounce() {
642 synchronized (mTarget
.getLock()) {
643 float t
= easeOut(mBounceFrame
* Axis
.MS_PER_FRAME
/ 256f
);
644 ImmutableViewportMetrics newMetrics
= mBounceStartMetrics
.interpolate(mBounceEndMetrics
, t
);
645 mTarget
.setViewportMetrics(newMetrics
);
650 /* Concludes a bounce animation and snaps the viewport into place. */
651 private void finishBounce() {
652 synchronized (mTarget
.getLock()) {
653 mTarget
.setViewportMetrics(mBounceEndMetrics
);
659 // The callback that performs the fling animation.
660 private class FlingRunnable
extends AnimationRunnable
{
661 protected void animateFrame() {
663 * The pan/zoom controller might have signaled to us that it wants to abort the
664 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
667 if (mState
!= PanZoomState
.FLING
) {
672 /* Advance flings, if necessary. */
673 boolean flingingX
= mX
.advanceFling();
674 boolean flingingY
= mY
.advanceFling();
676 boolean overscrolled
= (mX
.overscrolled() || mY
.overscrolled());
678 /* If we're still flinging in any direction, update the origin. */
679 if (flingingX
|| flingingY
) {
683 * Check to see if we're still flinging with an appreciable velocity. The threshold is
684 * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
685 * coast smoothly to a stop when not. In other words, require a greater velocity to
686 * maintain the fling once we enter overscroll.
688 float threshold
= (overscrolled
&& !mSubscroller
.scrolling() ? STOPPED_THRESHOLD
: FLING_STOPPED_THRESHOLD
);
689 if (getVelocity() >= threshold
) {
690 mContext
.getDocumentOverlay().showPageNumberRect();
691 // we're still flinging
699 /* Perform a bounce-back animation if overscrolled. */
704 setState(PanZoomState
.NOTHING
);
709 private void finishAnimation() {
712 stopAnimationTimer();
714 mContext
.getDocumentOverlay().hidePageNumberRect();
716 // Force a viewport synchronisation
717 mTarget
.forceRedraw();
720 /* Returns the nearest viewport metrics with no overscroll visible. */
721 private ImmutableViewportMetrics
getValidViewportMetrics() {
722 return getValidViewportMetrics(getMetrics());
725 private ImmutableViewportMetrics
getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics
) {
726 /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
727 float zoomFactor
= viewportMetrics
.zoomFactor
;
728 RectF pageRect
= viewportMetrics
.getPageRect();
729 RectF viewport
= viewportMetrics
.getViewport();
731 float focusX
= viewport
.width() / 2.0f
;
732 float focusY
= viewport
.height() / 2.0f
;
734 float minZoomFactor
= 0.0f
;
735 float maxZoomFactor
= MAX_ZOOM
;
737 ZoomConstraints constraints
= mTarget
.getZoomConstraints();
738 if (null == constraints
) {
739 Log
.e(LOGTAG
, "ZoomConstraints not available - too impatient?");
740 return viewportMetrics
;
743 if (constraints
.getMinZoom() > 0)
744 minZoomFactor
= constraints
.getMinZoom();
745 if (constraints
.getMaxZoom() > 0)
746 maxZoomFactor
= constraints
.getMaxZoom();
748 maxZoomFactor
= Math
.max(maxZoomFactor
, minZoomFactor
);
750 if (zoomFactor
< minZoomFactor
) {
751 // if one (or both) of the page dimensions is smaller than the viewport,
752 // zoom using the top/left as the focus on that axis. this prevents the
753 // scenario where, if both dimensions are smaller than the viewport, but
754 // by different scale factors, we end up scrolled to the end on one axis
755 // after applying the scale
756 PointF center
= new PointF(focusX
, focusY
);
757 viewportMetrics
= viewportMetrics
.scaleTo(minZoomFactor
, center
);
758 } else if (zoomFactor
> maxZoomFactor
) {
759 PointF center
= new PointF(viewport
.width() / 2.0f
, viewport
.height() / 2.0f
);
760 viewportMetrics
= viewportMetrics
.scaleTo(maxZoomFactor
, center
);
763 /* Now we pan to the right origin. */
764 viewportMetrics
= viewportMetrics
.clamp();
766 viewportMetrics
= pushPageToCenterOfViewport(viewportMetrics
);
768 return viewportMetrics
;
771 private ImmutableViewportMetrics
pushPageToCenterOfViewport(ImmutableViewportMetrics viewportMetrics
) {
772 RectF pageRect
= viewportMetrics
.getPageRect();
773 RectF viewportRect
= viewportMetrics
.getViewport();
775 if (pageRect
.width() < viewportRect
.width()) {
776 float originX
= (viewportRect
.width() - pageRect
.width()) / 2.0f
;
777 viewportMetrics
= viewportMetrics
.setViewportOrigin(-originX
, viewportMetrics
.getOrigin().y
);
780 if (pageRect
.height() < viewportRect
.height()) {
781 float originY
= (viewportRect
.height() - pageRect
.height()) / 2.0f
;
782 viewportMetrics
= viewportMetrics
.setViewportOrigin(viewportMetrics
.getOrigin().x
, -originY
);
785 return viewportMetrics
;
788 private class AxisX
extends Axis
{
789 AxisX(SubdocumentScrollHelper subscroller
) { super(subscroller
); }
791 public float getOrigin() { return getMetrics().viewportRectLeft
; }
793 protected float getViewportLength() { return getMetrics().getWidth(); }
795 protected float getPageStart() { return getMetrics().pageRectLeft
; }
797 protected float getPageLength() { return getMetrics().getPageWidth(); }
800 private class AxisY
extends Axis
{
801 AxisY(SubdocumentScrollHelper subscroller
) { super(subscroller
); }
803 public float getOrigin() { return getMetrics().viewportRectTop
; }
805 protected float getViewportLength() { return getMetrics().getHeight(); }
807 protected float getPageStart() { return getMetrics().pageRectTop
; }
809 protected float getPageLength() { return getMetrics().getPageHeight(); }
816 public boolean onScaleBegin(SimpleScaleGestureDetector detector
) {
817 if (mState
== PanZoomState
.ANIMATED_ZOOM
)
820 if (null == mTarget
.getZoomConstraints())
823 setState(PanZoomState
.PINCHING
);
824 mLastZoomFocus
= new PointF(detector
.getFocusX(), detector
.getFocusY());
831 public boolean onScale(SimpleScaleGestureDetector detector
) {
832 if (mTarget
.isFullScreen())
835 if (mState
!= PanZoomState
.PINCHING
)
838 float prevSpan
= detector
.getPreviousSpan();
839 if (FloatUtils
.fuzzyEquals(prevSpan
, 0.0f
)) {
840 // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
844 float spanRatio
= detector
.getCurrentSpan() / prevSpan
;
846 synchronized (mTarget
.getLock()) {
847 float newZoomFactor
= getMetrics().zoomFactor
* spanRatio
;
848 float minZoomFactor
= 0.0f
; // deliberately set to zero to allow big zoom out effect
849 float maxZoomFactor
= MAX_ZOOM
;
851 ZoomConstraints constraints
= mTarget
.getZoomConstraints();
853 if (constraints
.getMaxZoom() > 0)
854 maxZoomFactor
= constraints
.getMaxZoom();
856 if (newZoomFactor
< minZoomFactor
) {
857 // apply resistance when zooming past minZoomFactor,
858 // such that it asymptotically reaches minZoomFactor / 2.0
859 // but never exceeds that
860 final float rate
= 0.5f
; // controls how quickly we approach the limit
861 float excessZoom
= minZoomFactor
- newZoomFactor
;
862 excessZoom
= 1.0f
- (float)Math
.exp(-excessZoom
* rate
);
863 newZoomFactor
= minZoomFactor
* (1.0f
- excessZoom
/ 2.0f
);
866 if (newZoomFactor
> maxZoomFactor
) {
867 // apply resistance when zooming past maxZoomFactor,
868 // such that it asymptotically reaches maxZoomFactor + 1.0
869 // but never exceeds that
870 float excessZoom
= newZoomFactor
- maxZoomFactor
;
871 excessZoom
= 1.0f
- (float)Math
.exp(-excessZoom
);
872 newZoomFactor
= maxZoomFactor
+ excessZoom
;
875 scrollBy(mLastZoomFocus
.x
- detector
.getFocusX(),
876 mLastZoomFocus
.y
- detector
.getFocusY());
877 PointF focus
= new PointF(detector
.getFocusX(), detector
.getFocusY());
878 scaleWithFocus(newZoomFactor
, focus
);
881 mLastZoomFocus
.set(detector
.getFocusX(), detector
.getFocusY());
887 public void onScaleEnd(SimpleScaleGestureDetector detector
) {
888 if (mState
== PanZoomState
.ANIMATED_ZOOM
)
891 // switch back to the touching state
892 startTouch(detector
.getFocusX(), detector
.getFocusY(), detector
.getEventTime());
894 // Force a viewport synchronisation
895 mTarget
.forceRedraw();
900 * Scales the viewport, keeping the given focus point in the same place before and after the
901 * scale operation. You must hold the monitor while calling this.
903 private void scaleWithFocus(float zoomFactor
, PointF focus
) {
904 ImmutableViewportMetrics viewportMetrics
= getMetrics();
905 viewportMetrics
= viewportMetrics
.scaleTo(zoomFactor
, focus
);
906 mTarget
.setViewportMetrics(viewportMetrics
);
909 public boolean getRedrawHint() {
914 // don't redraw during these because the zoom is (or might be, in the case
915 // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
916 // display port area. we trigger a force-redraw upon exiting these states.
919 // allow redrawing in other states
925 public boolean onDown(MotionEvent motionEvent
) {
926 mWaitForDoubleTap
= mTarget
.getZoomConstraints() != null;
931 public void onShowPress(MotionEvent motionEvent
) {
932 // If we get this, it will be followed either by a call to
933 // onSingleTapUp (if the user lifts their finger before the
934 // long-press timeout) or a call to onLongPress (if the user
935 // does not). In the former case, we want to make sure it is
936 // treated as a click. (Note that if this is called, we will
937 // not get a call to onDoubleTap).
938 mWaitForDoubleTap
= false;
941 private PointF
getMotionInDocumentCoordinates(MotionEvent motionEvent
) {
942 RectF viewport
= getValidViewportMetrics().getViewport();
943 PointF viewPoint
= new PointF(motionEvent
.getX(0), motionEvent
.getY(0));
944 return mTarget
.convertViewPointToLayerPoint(viewPoint
);
948 public void onLongPress(MotionEvent motionEvent
) {
949 LOKitShell
.sendTouchEvent("LongPress", getMotionInDocumentCoordinates(motionEvent
));
953 public boolean onScroll(MotionEvent e1
, MotionEvent e2
, float distanceX
, float distanceY
) {
954 mContext
.getDocumentOverlay().showPageNumberRect();
955 return super.onScroll(e1
, e2
, distanceX
, distanceY
);
959 public boolean onSingleTapUp(MotionEvent motionEvent
) {
960 // When double-tapping is allowed, we have to wait to see if this is
961 // going to be a double-tap.
962 if (!mWaitForDoubleTap
) {
963 LOKitShell
.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent
));
965 // return false because we still want to get the ACTION_UP event that triggers this
970 public boolean onSingleTapConfirmed(MotionEvent motionEvent
) {
971 // In cases where we don't wait for double-tap, we handle this in onSingleTapUp.
972 if (mWaitForDoubleTap
) {
973 LOKitShell
.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent
));
979 public boolean onDoubleTap(MotionEvent motionEvent
) {
980 if (null == mTarget
.getZoomConstraints()) {
983 // Double tap zooms in or out depending on the current zoom factor
984 PointF pointOfTap
= getMotionInDocumentCoordinates(motionEvent
);
985 ImmutableViewportMetrics metrics
= getMetrics();
986 float newZoom
= metrics
.getZoomFactor() >=
987 DOUBLE_TAP_THRESHOLD ? mTarget
.getZoomConstraints().getDefaultZoom() : DOUBLE_TAP_THRESHOLD
;
988 // calculate new top_left point from the point of tap
989 float ratio
= newZoom
/metrics
.getZoomFactor();
990 float newLeft
= pointOfTap
.x
- 1/ratio
* (pointOfTap
.x
- metrics
.getOrigin().x
/ metrics
.getZoomFactor());
991 float newTop
= pointOfTap
.y
- 1/ratio
* (pointOfTap
.y
- metrics
.getOrigin().y
/ metrics
.getZoomFactor());
992 // animate move to the new view
993 animatedMove(new PointF(newLeft
, newTop
), newZoom
);
995 LOKitShell
.sendTouchEvent("DoubleTap", pointOfTap
);
999 private void cancelTouch() {
1000 //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
1001 //GeckoAppShell.sendEventToGecko(e);
1005 * Zoom to a specified rect IN CSS PIXELS.
1007 * While we usually use device pixels, zoomToRect must be specified in CSS
1010 boolean animatedZoomTo(RectF zoomToRect
) {
1011 final float startZoom
= getMetrics().zoomFactor
;
1013 RectF viewport
= getMetrics().getViewport();
1014 // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
1015 // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
1016 // while enlarging make sure we enlarge equally on both sides to keep the target rect
1018 float targetRatio
= viewport
.width() / viewport
.height();
1019 float rectRatio
= zoomToRect
.width() / zoomToRect
.height();
1020 if (FloatUtils
.fuzzyEquals(targetRatio
, rectRatio
)) {
1021 // all good, do nothing
1022 } else if (targetRatio
< rectRatio
) {
1023 // need to increase zoomToRect height
1024 float newHeight
= zoomToRect
.width() / targetRatio
;
1025 zoomToRect
.top
-= (newHeight
- zoomToRect
.height()) / 2;
1026 zoomToRect
.bottom
= zoomToRect
.top
+ newHeight
;
1027 } else { // targetRatio > rectRatio) {
1028 // need to increase zoomToRect width
1029 float newWidth
= targetRatio
* zoomToRect
.height();
1030 zoomToRect
.left
-= (newWidth
- zoomToRect
.width()) / 2;
1031 zoomToRect
.right
= zoomToRect
.left
+ newWidth
;
1034 float finalZoom
= viewport
.width() / zoomToRect
.width();
1036 ImmutableViewportMetrics finalMetrics
= getMetrics();
1037 finalMetrics
= finalMetrics
.setViewportOrigin(
1038 zoomToRect
.left
* finalMetrics
.zoomFactor
,
1039 zoomToRect
.top
* finalMetrics
.zoomFactor
);
1040 finalMetrics
= finalMetrics
.scaleTo(finalZoom
, new PointF(0.0f
, 0.0f
));
1042 // 2. now run getValidViewportMetrics on it, so that the target viewport is
1043 // clamped down to prevent overscroll, over-zoom, and other bad conditions.
1044 finalMetrics
= getValidViewportMetrics(finalMetrics
);
1046 bounce(finalMetrics
, PanZoomState
.ANIMATED_ZOOM
);
1051 * Move the viewport to the top-left point to and zoom to the desired
1052 * zoom factor. Input zoom factor can be null, in this case leave the zoom unchanged.
1054 boolean animatedMove(PointF topLeft
, Float zoom
) {
1055 RectF moveToRect
= getMetrics().getCssViewport();
1056 moveToRect
.offsetTo(topLeft
.x
, topLeft
.y
);
1058 ImmutableViewportMetrics finalMetrics
= getMetrics();
1060 finalMetrics
= finalMetrics
.setViewportOrigin(
1061 moveToRect
.left
* finalMetrics
.zoomFactor
,
1062 moveToRect
.top
* finalMetrics
.zoomFactor
);
1065 finalMetrics
= finalMetrics
.scaleTo(zoom
, new PointF(0.0f
, 0.0f
));
1067 finalMetrics
= getValidViewportMetrics(finalMetrics
);
1069 bounce(finalMetrics
, PanZoomState
.ANIMATED_ZOOM
);
1073 /** This function must be called from the UI thread. */
1074 public void abortPanning() {
1079 public void setOverScrollMode(int overscrollMode
) {
1080 mX
.setOverScrollMode(overscrollMode
);
1081 mY
.setOverScrollMode(overscrollMode
);
1084 public int getOverScrollMode() {
1085 return mX
.getOverScrollMode();