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
;
26 * Handles the kinetic scrolling and zooming physics for a layer controller.
28 * Many ideas are from Joe Hewitt's Scrollability:
29 * https://github.com/joehewitt/scrollability/
31 class JavaPanZoomController
32 extends GestureDetector
.SimpleOnGestureListener
33 implements PanZoomController
, SimpleScaleGestureDetector
.SimpleScaleGestureListener
35 private static final String LOGTAG
= "GeckoPanZoomController";
37 // Animation stops if the velocity is below this value when overscrolled or panning.
38 private static final float STOPPED_THRESHOLD
= 4.0f
;
40 // Animation stops is the velocity is below this threshold when flinging.
41 private static final float FLING_STOPPED_THRESHOLD
= 0.1f
;
43 // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
44 // between the touch-down and touch-up of a click). In units of density-independent pixels.
45 private final float PAN_THRESHOLD
;
47 // Angle from axis within which we stay axis-locked
48 private static final double AXIS_LOCK_ANGLE
= Math
.PI
/ 6.0; // 30 degrees
50 // The maximum amount we allow you to zoom into a page
51 private static final float MAX_ZOOM
= 4.0f
;
53 // The threshold zoom factor of whether a double tap triggers zoom-in or zoom-out
54 private static final float DOUBLE_TAP_THRESHOLD
= 1.0f
;
56 // The maximum amount we would like to scroll with the mouse
57 private final float MAX_SCROLL
;
59 private enum PanZoomState
{
60 NOTHING
, /* no touch-start events received */
61 FLING
, /* all touches removed, but we're still scrolling page */
62 TOUCHING
, /* one touch-start event received */
63 PANNING_LOCKED
, /* touch-start followed by move (i.e. panning with axis lock) */
64 PANNING
, /* panning without axis lock */
65 PANNING_HOLD
, /* in panning, but not moving.
66 * similar to TOUCHING but after starting a pan */
67 PANNING_HOLD_LOCKED
, /* like PANNING_HOLD, but axis lock still in effect */
68 PINCHING
, /* nth touch-start, where n > 1. this mode allows pan and zoom */
69 ANIMATED_ZOOM
, /* animated zoom to a new rect */
70 BOUNCE
, /* in a bounce animation */
72 WAITING_LISTENERS
, /* a state halfway between NOTHING and TOUCHING - the user has
73 put a finger down, but we don't yet know if a touch listener has
74 prevented the default actions yet. we still need to abort animations. */
77 private final PanZoomTarget mTarget
;
78 private final SubdocumentScrollHelper mSubscroller
;
79 private final Axis mX
;
80 private final Axis mY
;
81 private final TouchEventHandler mTouchEventHandler
;
82 private Thread mMainThread
;
83 private LibreOfficeMainActivity mContext
;
85 /* The timer that handles flings or bounces. */
86 private Timer mAnimationTimer
;
87 /* The runnable being scheduled by the animation timer. */
88 private AnimationRunnable mAnimationRunnable
;
89 /* The zoom focus at the first zoom event (in page coordinates). */
90 private PointF mLastZoomFocus
;
91 /* The time the last motion event took place. */
92 private long mLastEventTime
;
93 /* Current state the pan/zoom UI is in. */
94 private PanZoomState mState
;
95 /* Whether or not to wait for a double-tap before dispatching a single-tap */
96 private boolean mWaitForDoubleTap
;
98 JavaPanZoomController(LibreOfficeMainActivity context
, PanZoomTarget target
, View view
) {
100 PAN_THRESHOLD
= 1/16f
* LOKitShell
.getDpi(view
.getContext());
101 MAX_SCROLL
= 0.075f
* LOKitShell
.getDpi(view
.getContext());
103 mSubscroller
= new SubdocumentScrollHelper();
104 mX
= new AxisX(mSubscroller
);
105 mY
= new AxisY(mSubscroller
);
106 mTouchEventHandler
= new TouchEventHandler(view
.getContext(), view
, this);
108 mMainThread
= mContext
.getMainLooper().getThread();
111 setState(PanZoomState
.NOTHING
);
114 public void destroy() {
115 mSubscroller
.destroy();
116 mTouchEventHandler
.destroy();
119 private static float easeOut(float t
) {
126 private void setState(PanZoomState state
) {
127 if (state
!= mState
) {
132 private ImmutableViewportMetrics
getMetrics() {
133 return mTarget
.getViewportMetrics();
136 // for debugging bug 713011; it can be taken out once that is resolved.
137 private void checkMainThread() {
138 if (mMainThread
!= Thread
.currentThread()) {
139 // log with full stack trace
140 Log
.e(LOGTAG
, "Uh-oh, we're running on the wrong thread!", new Exception());
144 /** This function MUST be called on the UI thread */
145 public boolean onMotionEvent(MotionEvent event
) {
146 if ((event
.getSource() & InputDevice
.SOURCE_CLASS_MASK
) == InputDevice
.SOURCE_CLASS_POINTER
147 && (event
.getAction() & MotionEvent
.ACTION_MASK
) == MotionEvent
.ACTION_SCROLL
) {
148 return handlePointerScroll(event
);
153 /** This function MUST be called on the UI thread */
154 public boolean onTouchEvent(MotionEvent event
) {
155 return mTouchEventHandler
.handleEvent(event
);
158 boolean handleEvent(MotionEvent event
) {
159 switch (event
.getAction() & MotionEvent
.ACTION_MASK
) {
160 case MotionEvent
.ACTION_DOWN
: return handleTouchStart(event
);
161 case MotionEvent
.ACTION_MOVE
: return handleTouchMove(event
);
162 case MotionEvent
.ACTION_UP
: return handleTouchEnd(event
);
163 case MotionEvent
.ACTION_CANCEL
: return handleTouchCancel(event
);
168 /** This function MUST be called on the UI thread */
169 public void notifyDefaultActionPrevented(boolean prevented
) {
170 mTouchEventHandler
.handleEventListenerAction(!prevented
);
173 /** This function must be called from the UI thread. */
174 public void abortAnimation() {
176 // this happens when gecko changes the viewport on us or if the device is rotated.
177 // if that's the case, abort any animation in progress and re-zoom so that the page
178 // snaps to edges. for other cases (where the user's finger(s) are down) don't do
187 // the zoom that's in progress likely makes no sense any more (such as if
188 // the screen orientation changed) so abort it
189 setState(PanZoomState
.NOTHING
);
192 // Don't do animations here; they're distracting and can cause flashes on page
194 synchronized (mTarget
.getLock()) {
195 mTarget
.setViewportMetrics(getValidViewportMetrics());
196 mTarget
.forceRedraw();
202 /** This function must be called on the UI thread. */
203 void startingNewEventBlock(MotionEvent event
, boolean waitingForTouchListeners
) {
205 mSubscroller
.cancel();
206 if (waitingForTouchListeners
&& (event
.getAction() & MotionEvent
.ACTION_MASK
) == MotionEvent
.ACTION_DOWN
) {
207 // this is the first touch point going down, so we enter the pending state
208 // setting the state will kill any animations in progress, possibly leaving
209 // the page in overscroll
210 setState(PanZoomState
.WAITING_LISTENERS
);
214 /** This function must be called on the UI thread. */
215 void preventedTouchFinished() {
217 if (mState
== PanZoomState
.WAITING_LISTENERS
) {
218 // if we enter here, we just finished a block of events whose default actions
219 // were prevented by touch listeners. Now there are no touch points left, so
220 // we need to reset our state and re-bounce because we might be in overscroll
225 /** This must be called on the UI thread. */
226 public void pageRectUpdated() {
227 if (mState
== PanZoomState
.NOTHING
) {
228 synchronized (mTarget
.getLock()) {
229 ImmutableViewportMetrics validated
= getValidViewportMetrics();
230 if (!getMetrics().fuzzyEquals(validated
)) {
231 // page size changed such that we are now in overscroll. snap to
232 // the nearest valid viewport
233 mTarget
.setViewportMetrics(validated
);
243 private boolean handleTouchStart(MotionEvent event
) {
244 // user is taking control of movement, so stop
245 // any auto-movement we have going
246 stopAnimationTimer();
250 // We just interrupted a double-tap animation, so force a redraw in
251 // case this touchstart is just a tap that doesn't end up triggering
253 mTarget
.forceRedraw();
258 case WAITING_LISTENERS
:
259 startTouch(event
.getX(0), event
.getY(0), event
.getEventTime());
265 case PANNING_HOLD_LOCKED
:
267 Log
.e(LOGTAG
, "Received impossible touch down while in " + mState
);
270 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchStart");
274 private boolean handleTouchMove(MotionEvent event
) {
275 if (mState
== PanZoomState
.PANNING_LOCKED
|| mState
== PanZoomState
.PANNING
) {
276 if (getVelocity() > 18.0f
) {
277 mContext
.hideSoftKeyboard();
284 case WAITING_LISTENERS
:
285 // should never happen
286 Log
.e(LOGTAG
, "Received impossible touch move while in " + mState
);
290 // may happen if user double-taps and drags without lifting after the
291 // second tap. ignore the move if this happens.
295 // Don't allow panning if there is an element in full-screen mode. See bug 775511.
296 if (mTarget
.isFullScreen() || panDistance(event
) < PAN_THRESHOLD
) {
300 startPanning(event
.getX(0), event
.getY(0), event
.getEventTime());
304 case PANNING_HOLD_LOCKED
:
305 setState(PanZoomState
.PANNING_LOCKED
);
312 setState(PanZoomState
.PANNING
);
319 // scale gesture listener will handle this
322 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchMove");
326 private boolean handleTouchEnd(MotionEvent event
) {
331 case WAITING_LISTENERS
:
332 // should never happen
333 Log
.e(LOGTAG
, "Received impossible touch end while in " + mState
);
337 // may happen if user double-taps and drags without lifting after the
338 // second tap. ignore if this happens.
342 // the switch into TOUCHING might have happened while the page was
343 // snapping back after overscroll. we need to finish the snap if that
351 case PANNING_HOLD_LOCKED
:
352 setState(PanZoomState
.FLING
);
357 setState(PanZoomState
.NOTHING
);
360 Log
.e(LOGTAG
, "Unhandled case " + mState
+ " in handleTouchEnd");
364 private boolean handleTouchCancel(MotionEvent event
) {
367 if (mState
== PanZoomState
.WAITING_LISTENERS
) {
368 // we might get a cancel event from the TouchEventHandler while in the
369 // WAITING_LISTENERS state if the touch listeners prevent-default the
370 // block of events. at this point being in WAITING_LISTENERS is equivalent
371 // to being in NOTHING with the exception of possibly being in overscroll.
372 // so here we don't want to do anything right now; the overscroll will be
373 // corrected in preventedTouchFinished().
377 // ensure we snap back if we're overscrolled
382 private boolean handlePointerScroll(MotionEvent event
) {
383 if (mState
== PanZoomState
.NOTHING
|| mState
== PanZoomState
.FLING
) {
384 float scrollX
= event
.getAxisValue(MotionEvent
.AXIS_HSCROLL
);
385 float scrollY
= event
.getAxisValue(MotionEvent
.AXIS_VSCROLL
);
387 scrollBy(scrollX
* MAX_SCROLL
, scrollY
* MAX_SCROLL
);
394 private void startTouch(float x
, float y
, long time
) {
397 setState(PanZoomState
.TOUCHING
);
398 mLastEventTime
= time
;
401 private void startPanning(float x
, float y
, long time
) {
402 float dx
= mX
.panDistance(x
);
403 float dy
= mY
.panDistance(y
);
404 double angle
= Math
.atan2(dy
, dx
); // range [-pi, pi]
405 angle
= Math
.abs(angle
); // range [0, pi]
407 // When the touch move breaks through the pan threshold, reposition the touch down origin
408 // so the page won't jump when we start panning.
411 mLastEventTime
= time
;
413 if (!mX
.scrollable() || !mY
.scrollable()) {
414 setState(PanZoomState
.PANNING
);
415 } else if (angle
< AXIS_LOCK_ANGLE
|| angle
> (Math
.PI
- AXIS_LOCK_ANGLE
)) {
416 mY
.setScrollingDisabled(true);
417 setState(PanZoomState
.PANNING_LOCKED
);
418 } else if (Math
.abs(angle
- (Math
.PI
/ 2)) < AXIS_LOCK_ANGLE
) {
419 mX
.setScrollingDisabled(true);
420 setState(PanZoomState
.PANNING_LOCKED
);
422 setState(PanZoomState
.PANNING
);
426 private float panDistance(MotionEvent move
) {
427 float dx
= mX
.panDistance(move
.getX(0));
428 float dy
= mY
.panDistance(move
.getY(0));
429 return (float) Math
.hypot(dx
, dy
);
432 private void track(float x
, float y
, long time
) {
433 float timeDelta
= (float)(time
- mLastEventTime
);
434 if (FloatUtils
.fuzzyEquals(timeDelta
, 0)) {
435 // probably a duplicate event, ignore it. using a zero timeDelta will mess
439 mLastEventTime
= time
;
441 mX
.updateWithTouchAt(x
, timeDelta
);
442 mY
.updateWithTouchAt(y
, timeDelta
);
445 private void track(MotionEvent event
) {
449 for (int i
= 0; i
< event
.getHistorySize(); i
++) {
450 track(event
.getHistoricalX(0, i
),
451 event
.getHistoricalY(0, i
),
452 event
.getHistoricalEventTime(i
));
454 track(event
.getX(0), event
.getY(0), event
.getEventTime());
457 if (mState
== PanZoomState
.PANNING
) {
458 setState(PanZoomState
.PANNING_HOLD
);
459 } else if (mState
== PanZoomState
.PANNING_LOCKED
) {
460 setState(PanZoomState
.PANNING_HOLD_LOCKED
);
462 // should never happen, but handle anyway for robustness
463 Log
.e(LOGTAG
, "Impossible case " + mState
+ " when stopped in track");
464 setState(PanZoomState
.PANNING_HOLD_LOCKED
);
473 private void scrollBy(float dx
, float dy
) {
474 ImmutableViewportMetrics scrolled
= getMetrics().offsetViewportBy(dx
, dy
);
475 mTarget
.setViewportMetrics(scrolled
);
478 private void fling() {
481 stopAnimationTimer();
483 boolean stopped
= stopped();
484 mX
.startFling(stopped
);
485 mY
.startFling(stopped
);
487 startAnimationTimer(new FlingRunnable());
490 /* Performs a bounce-back animation to the given viewport metrics. */
491 private void bounce(ImmutableViewportMetrics metrics
, PanZoomState state
) {
492 stopAnimationTimer();
494 ImmutableViewportMetrics bounceStartMetrics
= getMetrics();
495 if (bounceStartMetrics
.fuzzyEquals(metrics
)) {
496 setState(PanZoomState
.NOTHING
);
503 // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
504 // getRedrawHint() is returning false. This means we can safely call
505 // setAnimationTarget to set the new final display port and not have it get
506 // clobbered by display ports from intermediate animation frames.
507 mTarget
.setAnimationTarget(metrics
);
508 startAnimationTimer(new BounceRunnable(bounceStartMetrics
, metrics
));
511 /* Performs a bounce-back animation to the nearest valid viewport metrics. */
512 private void bounce() {
513 bounce(getValidViewportMetrics(), PanZoomState
.BOUNCE
);
516 /* Starts the fling or bounce animation. */
517 private void startAnimationTimer(final AnimationRunnable runnable
) {
518 if (mAnimationTimer
!= null) {
519 Log
.e(LOGTAG
, "Attempted to start a new fling without canceling the old one!");
520 stopAnimationTimer();
523 mAnimationTimer
= new Timer("Animation Timer");
524 mAnimationRunnable
= runnable
;
525 mAnimationTimer
.scheduleAtFixedRate(new TimerTask() {
527 public void run() { mTarget
.post(runnable
); }
528 }, 0, (int)Axis
.MS_PER_FRAME
);
531 /* Stops the fling or bounce animation. */
532 private void stopAnimationTimer() {
533 if (mAnimationTimer
!= null) {
534 mAnimationTimer
.cancel();
535 mAnimationTimer
= null;
537 if (mAnimationRunnable
!= null) {
538 mAnimationRunnable
.terminate();
539 mAnimationRunnable
= null;
543 private float getVelocity() {
544 float xvel
= mX
.getRealVelocity();
545 float yvel
= mY
.getRealVelocity();
546 return (float) Math
.sqrt(xvel
* xvel
+ yvel
* yvel
);
549 public PointF
getVelocityVector() {
550 return new PointF(mX
.getRealVelocity(), mY
.getRealVelocity());
553 private boolean stopped() {
554 return getVelocity() < STOPPED_THRESHOLD
;
557 private PointF
resetDisplacement() {
558 return new PointF(mX
.resetDisplacement(), mY
.resetDisplacement());
561 private void updatePosition() {
564 PointF displacement
= resetDisplacement();
565 if (FloatUtils
.fuzzyEquals(displacement
.x
, 0.0f
) && FloatUtils
.fuzzyEquals(displacement
.y
, 0.0f
)) {
568 if (! mSubscroller
.scrollBy(displacement
)) {
569 synchronized (mTarget
.getLock()) {
570 scrollBy(displacement
.x
, displacement
.y
);
575 private abstract class AnimationRunnable
implements Runnable
{
576 private boolean mAnimationTerminated
;
578 /* This should always run on the UI thread */
579 public final void run() {
581 * Since the animation timer queues this runnable on the UI thread, it
582 * is possible that even when the animation timer is cancelled, there
583 * are multiple instances of this queued, so we need to have another
584 * mechanism to abort. This is done by using the mAnimationTerminated flag.
586 if (mAnimationTerminated
) {
592 protected abstract void animateFrame();
594 /* This should always run on the UI thread */
595 final void terminate() {
596 mAnimationTerminated
= true;
600 /* The callback that performs the bounce animation. */
601 private class BounceRunnable
extends AnimationRunnable
{
602 /* The current frame of the bounce-back animation */
603 private int mBounceFrame
;
605 * The viewport metrics that represent the start and end of the bounce-back animation,
608 private ImmutableViewportMetrics mBounceStartMetrics
;
609 private ImmutableViewportMetrics mBounceEndMetrics
;
611 BounceRunnable(ImmutableViewportMetrics startMetrics
, ImmutableViewportMetrics endMetrics
) {
612 mBounceStartMetrics
= startMetrics
;
613 mBounceEndMetrics
= endMetrics
;
616 protected void animateFrame() {
618 * The pan/zoom controller might have signaled to us that it wants to abort the
619 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
622 if (!(mState
== PanZoomState
.BOUNCE
|| mState
== PanZoomState
.ANIMATED_ZOOM
)) {
627 /* Perform the next frame of the bounce-back animation. */
628 if (mBounceFrame
< (int)(256f
/Axis
.MS_PER_FRAME
)) {
633 /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
636 setState(PanZoomState
.NOTHING
);
639 /* Performs one frame of a bounce animation. */
640 private void advanceBounce() {
641 synchronized (mTarget
.getLock()) {
642 float t
= easeOut(mBounceFrame
* Axis
.MS_PER_FRAME
/ 256f
);
643 ImmutableViewportMetrics newMetrics
= mBounceStartMetrics
.interpolate(mBounceEndMetrics
, t
);
644 mTarget
.setViewportMetrics(newMetrics
);
649 /* Concludes a bounce animation and snaps the viewport into place. */
650 private void finishBounce() {
651 synchronized (mTarget
.getLock()) {
652 mTarget
.setViewportMetrics(mBounceEndMetrics
);
658 // The callback that performs the fling animation.
659 private class FlingRunnable
extends AnimationRunnable
{
660 protected void animateFrame() {
662 * The pan/zoom controller might have signaled to us that it wants to abort the
663 * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
666 if (mState
!= PanZoomState
.FLING
) {
671 /* Advance flings, if necessary. */
672 boolean flingingX
= mX
.advanceFling();
673 boolean flingingY
= mY
.advanceFling();
675 boolean overscrolled
= (mX
.overscrolled() || mY
.overscrolled());
677 /* If we're still flinging in any direction, update the origin. */
678 if (flingingX
|| flingingY
) {
682 * Check to see if we're still flinging with an appreciable velocity. The threshold is
683 * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
684 * coast smoothly to a stop when not. In other words, require a greater velocity to
685 * maintain the fling once we enter overscroll.
687 float threshold
= (overscrolled
&& !mSubscroller
.scrolling() ? STOPPED_THRESHOLD
: FLING_STOPPED_THRESHOLD
);
688 if (getVelocity() >= threshold
) {
689 mContext
.getDocumentOverlay().showPageNumberRect();
690 // we're still flinging
698 /* Perform a bounce-back animation if overscrolled. */
703 setState(PanZoomState
.NOTHING
);
708 private void finishAnimation() {
711 stopAnimationTimer();
713 mContext
.getDocumentOverlay().hidePageNumberRect();
715 // Force a viewport synchronisation
716 mTarget
.forceRedraw();
719 /* Returns the nearest viewport metrics with no overscroll visible. */
720 private ImmutableViewportMetrics
getValidViewportMetrics() {
721 return getValidViewportMetrics(getMetrics());
724 private ImmutableViewportMetrics
getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics
) {
725 /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
726 float zoomFactor
= viewportMetrics
.zoomFactor
;
727 RectF pageRect
= viewportMetrics
.getPageRect();
728 RectF viewport
= viewportMetrics
.getViewport();
730 float focusX
= viewport
.width() / 2.0f
;
731 float focusY
= viewport
.height() / 2.0f
;
733 float minZoomFactor
= 0.0f
;
734 float maxZoomFactor
= MAX_ZOOM
;
736 ZoomConstraints constraints
= mTarget
.getZoomConstraints();
737 if (null == constraints
) {
738 Log
.e(LOGTAG
, "ZoomConstraints not available - too impatient?");
739 return viewportMetrics
;
742 if (constraints
.getMinZoom() > 0)
743 minZoomFactor
= constraints
.getMinZoom();
744 if (constraints
.getMaxZoom() > 0)
745 maxZoomFactor
= constraints
.getMaxZoom();
747 maxZoomFactor
= Math
.max(maxZoomFactor
, minZoomFactor
);
749 if (zoomFactor
< minZoomFactor
) {
750 // if one (or both) of the page dimensions is smaller than the viewport,
751 // zoom using the top/left as the focus on that axis. this prevents the
752 // scenario where, if both dimensions are smaller than the viewport, but
753 // by different scale factors, we end up scrolled to the end on one axis
754 // after applying the scale
755 PointF center
= new PointF(focusX
, focusY
);
756 viewportMetrics
= viewportMetrics
.scaleTo(minZoomFactor
, center
);
757 } else if (zoomFactor
> maxZoomFactor
) {
758 PointF center
= new PointF(viewport
.width() / 2.0f
, viewport
.height() / 2.0f
);
759 viewportMetrics
= viewportMetrics
.scaleTo(maxZoomFactor
, center
);
762 /* Now we pan to the right origin. */
763 viewportMetrics
= viewportMetrics
.clamp();
765 viewportMetrics
= pushPageToCenterOfViewport(viewportMetrics
);
767 return viewportMetrics
;
770 private ImmutableViewportMetrics
pushPageToCenterOfViewport(ImmutableViewportMetrics viewportMetrics
) {
771 RectF pageRect
= viewportMetrics
.getPageRect();
772 RectF viewportRect
= viewportMetrics
.getViewport();
774 if (pageRect
.width() < viewportRect
.width()) {
775 float originX
= (viewportRect
.width() - pageRect
.width()) / 2.0f
;
776 viewportMetrics
= viewportMetrics
.setViewportOrigin(-originX
, viewportMetrics
.getOrigin().y
);
779 if (pageRect
.height() < viewportRect
.height()) {
780 float originY
= (viewportRect
.height() - pageRect
.height()) / 2.0f
;
781 viewportMetrics
= viewportMetrics
.setViewportOrigin(viewportMetrics
.getOrigin().x
, -originY
);
784 return viewportMetrics
;
787 private class AxisX
extends Axis
{
788 AxisX(SubdocumentScrollHelper subscroller
) { super(subscroller
); }
790 public float getOrigin() { return getMetrics().viewportRectLeft
; }
792 protected float getViewportLength() { return getMetrics().getWidth(); }
794 protected float getPageStart() { return getMetrics().pageRectLeft
; }
796 protected float getPageLength() { return getMetrics().getPageWidth(); }
799 private class AxisY
extends Axis
{
800 AxisY(SubdocumentScrollHelper subscroller
) { super(subscroller
); }
802 public float getOrigin() { return getMetrics().viewportRectTop
; }
804 protected float getViewportLength() { return getMetrics().getHeight(); }
806 protected float getPageStart() { return getMetrics().pageRectTop
; }
808 protected float getPageLength() { return getMetrics().getPageHeight(); }
815 public boolean onScaleBegin(SimpleScaleGestureDetector detector
) {
816 if (mState
== PanZoomState
.ANIMATED_ZOOM
)
819 if (null == mTarget
.getZoomConstraints())
822 setState(PanZoomState
.PINCHING
);
823 mLastZoomFocus
= new PointF(detector
.getFocusX(), detector
.getFocusY());
830 public boolean onScale(SimpleScaleGestureDetector detector
) {
831 if (mTarget
.isFullScreen())
834 if (mState
!= PanZoomState
.PINCHING
)
837 float prevSpan
= detector
.getPreviousSpan();
838 if (FloatUtils
.fuzzyEquals(prevSpan
, 0.0f
)) {
839 // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
843 float spanRatio
= detector
.getCurrentSpan() / prevSpan
;
845 synchronized (mTarget
.getLock()) {
846 float newZoomFactor
= getMetrics().zoomFactor
* spanRatio
;
847 float minZoomFactor
= 0.0f
; // deliberately set to zero to allow big zoom out effect
848 float maxZoomFactor
= MAX_ZOOM
;
850 ZoomConstraints constraints
= mTarget
.getZoomConstraints();
852 if (constraints
.getMaxZoom() > 0)
853 maxZoomFactor
= constraints
.getMaxZoom();
855 if (newZoomFactor
< minZoomFactor
) {
856 // apply resistance when zooming past minZoomFactor,
857 // such that it asymptotically reaches minZoomFactor / 2.0
858 // but never exceeds that
859 final float rate
= 0.5f
; // controls how quickly we approach the limit
860 float excessZoom
= minZoomFactor
- newZoomFactor
;
861 excessZoom
= 1.0f
- (float)Math
.exp(-excessZoom
* rate
);
862 newZoomFactor
= minZoomFactor
* (1.0f
- excessZoom
/ 2.0f
);
865 if (newZoomFactor
> maxZoomFactor
) {
866 // apply resistance when zooming past maxZoomFactor,
867 // such that it asymptotically reaches maxZoomFactor + 1.0
868 // but never exceeds that
869 float excessZoom
= newZoomFactor
- maxZoomFactor
;
870 excessZoom
= 1.0f
- (float)Math
.exp(-excessZoom
);
871 newZoomFactor
= maxZoomFactor
+ excessZoom
;
874 scrollBy(mLastZoomFocus
.x
- detector
.getFocusX(),
875 mLastZoomFocus
.y
- detector
.getFocusY());
876 PointF focus
= new PointF(detector
.getFocusX(), detector
.getFocusY());
877 scaleWithFocus(newZoomFactor
, focus
);
880 mLastZoomFocus
.set(detector
.getFocusX(), detector
.getFocusY());
886 public void onScaleEnd(SimpleScaleGestureDetector detector
) {
887 if (mState
== PanZoomState
.ANIMATED_ZOOM
)
890 // switch back to the touching state
891 startTouch(detector
.getFocusX(), detector
.getFocusY(), detector
.getEventTime());
893 // Force a viewport synchronisation
894 mTarget
.forceRedraw();
899 * Scales the viewport, keeping the given focus point in the same place before and after the
900 * scale operation. You must hold the monitor while calling this.
902 private void scaleWithFocus(float zoomFactor
, PointF focus
) {
903 ImmutableViewportMetrics viewportMetrics
= getMetrics();
904 viewportMetrics
= viewportMetrics
.scaleTo(zoomFactor
, focus
);
905 mTarget
.setViewportMetrics(viewportMetrics
);
908 public boolean getRedrawHint() {
913 // don't redraw during these because the zoom is (or might be, in the case
914 // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
915 // display port area. we trigger a force-redraw upon exiting these states.
918 // allow redrawing in other states
924 public boolean onDown(MotionEvent motionEvent
) {
925 mWaitForDoubleTap
= mTarget
.getZoomConstraints() != null;
930 public void onShowPress(MotionEvent motionEvent
) {
931 // If we get this, it will be followed either by a call to
932 // onSingleTapUp (if the user lifts their finger before the
933 // long-press timeout) or a call to onLongPress (if the user
934 // does not). In the former case, we want to make sure it is
935 // treated as a click. (Note that if this is called, we will
936 // not get a call to onDoubleTap).
937 mWaitForDoubleTap
= false;
940 private PointF
getMotionInDocumentCoordinates(MotionEvent motionEvent
) {
941 RectF viewport
= getValidViewportMetrics().getViewport();
942 PointF viewPoint
= new PointF(motionEvent
.getX(0), motionEvent
.getY(0));
943 return mTarget
.convertViewPointToLayerPoint(viewPoint
);
947 public void onLongPress(MotionEvent motionEvent
) {
948 LOKitShell
.sendTouchEvent("LongPress", getMotionInDocumentCoordinates(motionEvent
));
952 public boolean onScroll(MotionEvent e1
, MotionEvent e2
, float distanceX
, float distanceY
) {
953 mContext
.getDocumentOverlay().showPageNumberRect();
954 return super.onScroll(e1
, e2
, distanceX
, distanceY
);
958 public boolean onSingleTapUp(MotionEvent motionEvent
) {
959 // When double-tapping is allowed, we have to wait to see if this is
960 // going to be a double-tap.
961 if (!mWaitForDoubleTap
) {
962 LOKitShell
.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent
));
964 // return false because we still want to get the ACTION_UP event that triggers this
969 public boolean onSingleTapConfirmed(MotionEvent motionEvent
) {
970 // In cases where we don't wait for double-tap, we handle this in onSingleTapUp.
971 if (mWaitForDoubleTap
) {
972 LOKitShell
.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent
));
978 public boolean onDoubleTap(MotionEvent motionEvent
) {
979 if (null == mTarget
.getZoomConstraints()) {
982 // Double tap zooms in or out depending on the current zoom factor
983 PointF pointOfTap
= getMotionInDocumentCoordinates(motionEvent
);
984 ImmutableViewportMetrics metrics
= getMetrics();
985 float newZoom
= metrics
.getZoomFactor() >=
986 DOUBLE_TAP_THRESHOLD ? mTarget
.getZoomConstraints().getDefaultZoom() : DOUBLE_TAP_THRESHOLD
;
987 // calculate new top_left point from the point of tap
988 float ratio
= newZoom
/metrics
.getZoomFactor();
989 float newLeft
= pointOfTap
.x
- 1/ratio
* (pointOfTap
.x
- metrics
.getOrigin().x
/ metrics
.getZoomFactor());
990 float newTop
= pointOfTap
.y
- 1/ratio
* (pointOfTap
.y
- metrics
.getOrigin().y
/ metrics
.getZoomFactor());
991 // animate move to the new view
992 animatedMove(new PointF(newLeft
, newTop
), newZoom
);
994 LOKitShell
.sendTouchEvent("DoubleTap", pointOfTap
);
998 private void cancelTouch() {
999 //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
1000 //GeckoAppShell.sendEventToGecko(e);
1004 * Zoom to a specified rect IN CSS PIXELS.
1006 * While we usually use device pixels, zoomToRect must be specified in CSS
1009 boolean animatedZoomTo(RectF zoomToRect
) {
1010 final float startZoom
= getMetrics().zoomFactor
;
1012 RectF viewport
= getMetrics().getViewport();
1013 // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
1014 // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
1015 // while enlarging make sure we enlarge equally on both sides to keep the target rect
1017 float targetRatio
= viewport
.width() / viewport
.height();
1018 float rectRatio
= zoomToRect
.width() / zoomToRect
.height();
1019 if (FloatUtils
.fuzzyEquals(targetRatio
, rectRatio
)) {
1020 // all good, do nothing
1021 } else if (targetRatio
< rectRatio
) {
1022 // need to increase zoomToRect height
1023 float newHeight
= zoomToRect
.width() / targetRatio
;
1024 zoomToRect
.top
-= (newHeight
- zoomToRect
.height()) / 2;
1025 zoomToRect
.bottom
= zoomToRect
.top
+ newHeight
;
1026 } else { // targetRatio > rectRatio) {
1027 // need to increase zoomToRect width
1028 float newWidth
= targetRatio
* zoomToRect
.height();
1029 zoomToRect
.left
-= (newWidth
- zoomToRect
.width()) / 2;
1030 zoomToRect
.right
= zoomToRect
.left
+ newWidth
;
1033 float finalZoom
= viewport
.width() / zoomToRect
.width();
1035 ImmutableViewportMetrics finalMetrics
= getMetrics();
1036 finalMetrics
= finalMetrics
.setViewportOrigin(
1037 zoomToRect
.left
* finalMetrics
.zoomFactor
,
1038 zoomToRect
.top
* finalMetrics
.zoomFactor
);
1039 finalMetrics
= finalMetrics
.scaleTo(finalZoom
, new PointF(0.0f
, 0.0f
));
1041 // 2. now run getValidViewportMetrics on it, so that the target viewport is
1042 // clamped down to prevent overscroll, over-zoom, and other bad conditions.
1043 finalMetrics
= getValidViewportMetrics(finalMetrics
);
1045 bounce(finalMetrics
, PanZoomState
.ANIMATED_ZOOM
);
1050 * Move the viewport to the top-left point to and zoom to the desired
1051 * zoom factor. Input zoom factor can be null, in this case leave the zoom unchanged.
1053 boolean animatedMove(PointF topLeft
, Float zoom
) {
1054 RectF moveToRect
= getMetrics().getCssViewport();
1055 moveToRect
.offsetTo(topLeft
.x
, topLeft
.y
);
1057 ImmutableViewportMetrics finalMetrics
= getMetrics();
1059 finalMetrics
= finalMetrics
.setViewportOrigin(
1060 moveToRect
.left
* finalMetrics
.zoomFactor
,
1061 moveToRect
.top
* finalMetrics
.zoomFactor
);
1064 finalMetrics
= finalMetrics
.scaleTo(zoom
, new PointF(0.0f
, 0.0f
));
1066 finalMetrics
= getValidViewportMetrics(finalMetrics
);
1068 bounce(finalMetrics
, PanZoomState
.ANIMATED_ZOOM
);
1072 /** This function must be called from the UI thread. */
1073 public void abortPanning() {
1078 public void setOverScrollMode(int overscrollMode
) {
1079 mX
.setOverScrollMode(overscrollMode
);
1080 mY
.setOverScrollMode(overscrollMode
);
1083 public int getOverScrollMode() {
1084 return mX
.getOverScrollMode();