LibreOffice Module android (master) 1
JavaPanZoomController.java
Go to the documentation of this file.
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
6package org.mozilla.gecko.gfx;
7
8import android.graphics.PointF;
9import android.graphics.RectF;
10import android.os.Build;
11import android.util.Log;
12import android.view.GestureDetector;
13import android.view.InputDevice;
14import android.view.MotionEvent;
15import android.view.View;
16
21
22import java.util.Timer;
23import java.util.TimerTask;
24
25/*
26 * Handles the kinetic scrolling and zooming physics for a layer controller.
27 *
28 * Many ideas are from Joe Hewitt's Scrollability:
29 * https://github.com/joehewitt/scrollability/
30 */
31class JavaPanZoomController
32 extends GestureDetector.SimpleOnGestureListener
33 implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener
34{
35 private static final String LOGTAG = "GeckoPanZoomController";
36
37 // Animation stops if the velocity is below this value when overscrolled or panning.
38 private static final float STOPPED_THRESHOLD = 4.0f;
39
40 // Animation stops is the velocity is below this threshold when flinging.
41 private static final float FLING_STOPPED_THRESHOLD = 0.1f;
42
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;
46
47 // Angle from axis within which we stay axis-locked
48 private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
49
50 // The maximum amount we allow you to zoom into a page
51 private static final float MAX_ZOOM = 4.0f;
52
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;
55
56 // The maximum amount we would like to scroll with the mouse
57 private final float MAX_SCROLL;
58
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 */
71
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. */
75 }
76
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;
84
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;
97
98 JavaPanZoomController(LibreOfficeMainActivity context, PanZoomTarget target, View view) {
99 mContext = context;
100 PAN_THRESHOLD = 1/16f * LOKitShell.getDpi(view.getContext());
101 MAX_SCROLL = 0.075f * LOKitShell.getDpi(view.getContext());
102 mTarget = target;
103 mSubscroller = new SubdocumentScrollHelper();
104 mX = new AxisX(mSubscroller);
105 mY = new AxisY(mSubscroller);
106 mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this);
107
108 mMainThread = mContext.getMainLooper().getThread();
109 checkMainThread();
110
111 setState(PanZoomState.NOTHING);
112 }
113
114 public void destroy() {
115 mSubscroller.destroy();
116 mTouchEventHandler.destroy();
117 }
118
119 private static float easeOut(float t) {
120 // ease-out approx.
121 // -(t-1)^2+1
122 t = t-1;
123 return -t*t+1;
124 }
125
126 private void setState(PanZoomState state) {
127 if (state != mState) {
128 mState = state;
129 }
130 }
131
132 private ImmutableViewportMetrics getMetrics() {
133 return mTarget.getViewportMetrics();
134 }
135
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());
141 }
142 }
143
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);
149 }
150 return false;
151 }
152
154 public boolean onTouchEvent(MotionEvent event) {
155 return mTouchEventHandler.handleEvent(event);
156 }
157
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);
164 }
165 return false;
166 }
167
169 public void notifyDefaultActionPrevented(boolean prevented) {
170 mTouchEventHandler.handleEventListenerAction(!prevented);
171 }
172
174 public void abortAnimation() {
175 checkMainThread();
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
179 // anything special.
180 switch (mState) {
181 case FLING:
182 mX.stopFling();
183 mY.stopFling();
184 // fall through
185 case BOUNCE:
186 case ANIMATED_ZOOM:
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);
190 // fall through
191 case NOTHING:
192 // Don't do animations here; they're distracting and can cause flashes on page
193 // transitions.
194 synchronized (mTarget.getLock()) {
195 mTarget.setViewportMetrics(getValidViewportMetrics());
196 mTarget.forceRedraw();
197 }
198 break;
199 }
200 }
201
203 void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
204 checkMainThread();
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);
211 }
212 }
213
215 void preventedTouchFinished() {
216 checkMainThread();
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
221 bounce();
222 }
223 }
224
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);
234 }
235 }
236 }
237 }
238
239 /*
240 * Panning/scrolling
241 */
242
243 private boolean handleTouchStart(MotionEvent event) {
244 // user is taking control of movement, so stop
245 // any auto-movement we have going
246 stopAnimationTimer();
247
248 switch (mState) {
249 case ANIMATED_ZOOM:
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
252 // a redraw
253 mTarget.forceRedraw();
254 // fall through
255 case FLING:
256 case BOUNCE:
257 case NOTHING:
258 case WAITING_LISTENERS:
259 startTouch(event.getX(0), event.getY(0), event.getEventTime());
260 return false;
261 case TOUCHING:
262 case PANNING:
263 case PANNING_LOCKED:
264 case PANNING_HOLD:
265 case PANNING_HOLD_LOCKED:
266 case PINCHING:
267 Log.e(LOGTAG, "Received impossible touch down while in " + mState);
268 return false;
269 }
270 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart");
271 return false;
272 }
273
274 private boolean handleTouchMove(MotionEvent event) {
275 if (mState == PanZoomState.PANNING_LOCKED || mState == PanZoomState.PANNING) {
276 if (getVelocity() > 18.0f) {
277 mContext.hideSoftKeyboard();
278 }
279 }
280
281 switch (mState) {
282 case FLING:
283 case BOUNCE:
284 case WAITING_LISTENERS:
285 // should never happen
286 Log.e(LOGTAG, "Received impossible touch move while in " + mState);
287 // fall through
288 case ANIMATED_ZOOM:
289 case NOTHING:
290 // may happen if user double-taps and drags without lifting after the
291 // second tap. ignore the move if this happens.
292 return false;
293
294 case TOUCHING:
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) {
297 return false;
298 }
299 cancelTouch();
300 startPanning(event.getX(0), event.getY(0), event.getEventTime());
301 track(event);
302 return true;
303
304 case PANNING_HOLD_LOCKED:
305 setState(PanZoomState.PANNING_LOCKED);
306 // fall through
307 case PANNING_LOCKED:
308 track(event);
309 return true;
310
311 case PANNING_HOLD:
312 setState(PanZoomState.PANNING);
313 // fall through
314 case PANNING:
315 track(event);
316 return true;
317
318 case PINCHING:
319 // scale gesture listener will handle this
320 return false;
321 }
322 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove");
323 return false;
324 }
325
326 private boolean handleTouchEnd(MotionEvent event) {
327
328 switch (mState) {
329 case FLING:
330 case BOUNCE:
331 case WAITING_LISTENERS:
332 // should never happen
333 Log.e(LOGTAG, "Received impossible touch end while in " + mState);
334 // fall through
335 case ANIMATED_ZOOM:
336 case NOTHING:
337 // may happen if user double-taps and drags without lifting after the
338 // second tap. ignore if this happens.
339 return false;
340
341 case TOUCHING:
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
344 // was the case
345 bounce();
346 return false;
347
348 case PANNING:
349 case PANNING_LOCKED:
350 case PANNING_HOLD:
351 case PANNING_HOLD_LOCKED:
352 setState(PanZoomState.FLING);
353 fling();
354 return true;
355
356 case PINCHING:
357 setState(PanZoomState.NOTHING);
358 return true;
359 }
360 Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd");
361 return false;
362 }
363
364 private boolean handleTouchCancel(MotionEvent event) {
365 cancelTouch();
366
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().
374 return false;
375 }
376
377 // ensure we snap back if we're overscrolled
378 bounce();
379 return false;
380 }
381
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);
386
387 scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
388 bounce();
389 return true;
390 }
391 return false;
392 }
393
394 private void startTouch(float x, float y, long time) {
395 mX.startTouch(x);
396 mY.startTouch(y);
397 setState(PanZoomState.TOUCHING);
398 mLastEventTime = time;
399 }
400
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]
406
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.
409 mX.startTouch(x);
410 mY.startTouch(y);
411 mLastEventTime = time;
412
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);
421 } else {
422 setState(PanZoomState.PANNING);
423 }
424 }
425
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);
430 }
431
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
436 // up our velocity
437 return;
438 }
439 mLastEventTime = time;
440
441 mX.updateWithTouchAt(x, timeDelta);
442 mY.updateWithTouchAt(y, timeDelta);
443 }
444
445 private void track(MotionEvent event) {
446 mX.saveTouchPos();
447 mY.saveTouchPos();
448
449 for (int i = 0; i < event.getHistorySize(); i++) {
450 track(event.getHistoricalX(0, i),
451 event.getHistoricalY(0, i),
452 event.getHistoricalEventTime(i));
453 }
454 track(event.getX(0), event.getY(0), event.getEventTime());
455
456 if (stopped()) {
457 if (mState == PanZoomState.PANNING) {
458 setState(PanZoomState.PANNING_HOLD);
459 } else if (mState == PanZoomState.PANNING_LOCKED) {
460 setState(PanZoomState.PANNING_HOLD_LOCKED);
461 } else {
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);
465 }
466 }
467
468 mX.startPan();
469 mY.startPan();
470 updatePosition();
471 }
472
473 private void scrollBy(float dx, float dy) {
474 ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy);
475 mTarget.setViewportMetrics(scrolled);
476 }
477
478 private void fling() {
479 updatePosition();
480
481 stopAnimationTimer();
482
483 boolean stopped = stopped();
484 mX.startFling(stopped);
485 mY.startFling(stopped);
486
487 startAnimationTimer(new FlingRunnable());
488 }
489
490 /* Performs a bounce-back animation to the given viewport metrics. */
491 private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
492 stopAnimationTimer();
493
494 ImmutableViewportMetrics bounceStartMetrics = getMetrics();
495 if (bounceStartMetrics.fuzzyEquals(metrics)) {
496 setState(PanZoomState.NOTHING);
497 finishAnimation();
498 return;
499 }
500
501 setState(state);
502
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));
509 }
510
511 /* Performs a bounce-back animation to the nearest valid viewport metrics. */
512 private void bounce() {
513 bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
514 }
515
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();
521 }
522
523 mAnimationTimer = new Timer("Animation Timer");
524 mAnimationRunnable = runnable;
525 mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
526 @Override
527 public void run() { mTarget.post(runnable); }
528 }, 0, (int)Axis.MS_PER_FRAME);
529 }
530
531 /* Stops the fling or bounce animation. */
532 private void stopAnimationTimer() {
533 if (mAnimationTimer != null) {
534 mAnimationTimer.cancel();
535 mAnimationTimer = null;
536 }
537 if (mAnimationRunnable != null) {
538 mAnimationRunnable.terminate();
539 mAnimationRunnable = null;
540 }
541 }
542
543 private float getVelocity() {
544 float xvel = mX.getRealVelocity();
545 float yvel = mY.getRealVelocity();
546 return (float) Math.sqrt(xvel * xvel + yvel * yvel);
547 }
548
549 public PointF getVelocityVector() {
550 return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
551 }
552
553 private boolean stopped() {
554 return getVelocity() < STOPPED_THRESHOLD;
555 }
556
557 private PointF resetDisplacement() {
558 return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
559 }
560
561 private void updatePosition() {
562 mX.displace();
563 mY.displace();
564 PointF displacement = resetDisplacement();
565 if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
566 return;
567 }
568 if (! mSubscroller.scrollBy(displacement)) {
569 synchronized (mTarget.getLock()) {
570 scrollBy(displacement.x, displacement.y);
571 }
572 }
573 }
574
575 private abstract class AnimationRunnable implements Runnable {
576 private boolean mAnimationTerminated;
577
578 /* This should always run on the UI thread */
579 public final void run() {
580 /*
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.
585 */
587 return;
588 }
589 animateFrame();
590 }
591
592 protected abstract void animateFrame();
593
594 /* This should always run on the UI thread */
595 final void terminate() {
597 }
598 }
599
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;
604 /*
605 * The viewport metrics that represent the start and end of the bounce-back animation,
606 * respectively.
607 */
610
612 mBounceStartMetrics = startMetrics;
613 mBounceEndMetrics = endMetrics;
614 }
615
616 protected void animateFrame() {
617 /*
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
620 * out.
621 */
622 if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
623 finishAnimation();
624 return;
625 }
626
627 /* Perform the next frame of the bounce-back animation. */
628 if (mBounceFrame < (int)(256f/Axis.MS_PER_FRAME)) {
630 return;
631 }
632
633 /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
634 finishBounce();
635 finishAnimation();
636 setState(PanZoomState.NOTHING);
637 }
638
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);
644 mTarget.setViewportMetrics(newMetrics);
645 mBounceFrame++;
646 }
647 }
648
649 /* Concludes a bounce animation and snaps the viewport into place. */
650 private void finishBounce() {
651 synchronized (mTarget.getLock()) {
653 mBounceFrame = -1;
654 }
655 }
656 }
657
658 // The callback that performs the fling animation.
659 private class FlingRunnable extends AnimationRunnable {
660 protected void animateFrame() {
661 /*
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
664 * out.
665 */
666 if (mState != PanZoomState.FLING) {
667 finishAnimation();
668 return;
669 }
670
671 /* Advance flings, if necessary. */
672 boolean flingingX = mX.advanceFling();
673 boolean flingingY = mY.advanceFling();
674
675 boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
676
677 /* If we're still flinging in any direction, update the origin. */
678 if (flingingX || flingingY) {
679 updatePosition();
680
681 /*
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.
686 */
687 float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
688 if (getVelocity() >= threshold) {
690 // we're still flinging
691 return;
692 }
693
694 mX.stopFling();
695 mY.stopFling();
696 }
697
698 /* Perform a bounce-back animation if overscrolled. */
699 if (overscrolled) {
700 bounce();
701 } else {
702 finishAnimation();
703 setState(PanZoomState.NOTHING);
704 }
705 }
706 }
707
708 private void finishAnimation() {
709 checkMainThread();
710
711 stopAnimationTimer();
712
714
715 // Force a viewport synchronisation
716 mTarget.forceRedraw();
717 }
718
719 /* Returns the nearest viewport metrics with no overscroll visible. */
720 private ImmutableViewportMetrics getValidViewportMetrics() {
721 return getValidViewportMetrics(getMetrics());
722 }
723
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();
729
730 float focusX = viewport.width() / 2.0f;
731 float focusY = viewport.height() / 2.0f;
732
733 float minZoomFactor = 0.0f;
734 float maxZoomFactor = MAX_ZOOM;
735
736 ZoomConstraints constraints = mTarget.getZoomConstraints();
737 if (null == constraints) {
738 Log.e(LOGTAG, "ZoomConstraints not available - too impatient?");
739 return viewportMetrics;
740
741 }
742 if (constraints.getMinZoom() > 0)
743 minZoomFactor = constraints.getMinZoom();
744 if (constraints.getMaxZoom() > 0)
745 maxZoomFactor = constraints.getMaxZoom();
746
747 maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
748
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);
760 }
761
762 /* Now we pan to the right origin. */
763 viewportMetrics = viewportMetrics.clamp();
764
765 viewportMetrics = pushPageToCenterOfViewport(viewportMetrics);
766
767 return viewportMetrics;
768 }
769
770 private ImmutableViewportMetrics pushPageToCenterOfViewport(ImmutableViewportMetrics viewportMetrics) {
771 RectF pageRect = viewportMetrics.getPageRect();
772 RectF viewportRect = viewportMetrics.getViewport();
773
774 if (pageRect.width() < viewportRect.width()) {
775 float originX = (viewportRect.width() - pageRect.width()) / 2.0f;
776 viewportMetrics = viewportMetrics.setViewportOrigin(-originX, viewportMetrics.getOrigin().y);
777 }
778
779 if (pageRect.height() < viewportRect.height()) {
780 float originY = (viewportRect.height() - pageRect.height()) / 2.0f;
781 viewportMetrics = viewportMetrics.setViewportOrigin(viewportMetrics.getOrigin().x, -originY);
782 }
783
784 return viewportMetrics;
785 }
786
787 private class AxisX extends Axis {
788 AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
789 @Override
790 public float getOrigin() { return getMetrics().viewportRectLeft; }
791 @Override
792 protected float getViewportLength() { return getMetrics().getWidth(); }
793 @Override
794 protected float getPageStart() { return getMetrics().pageRectLeft; }
795 @Override
796 protected float getPageLength() { return getMetrics().getPageWidth(); }
797 }
798
799 private class AxisY extends Axis {
800 AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
801 @Override
802 public float getOrigin() { return getMetrics().viewportRectTop; }
803 @Override
804 protected float getViewportLength() { return getMetrics().getHeight(); }
805 @Override
806 protected float getPageStart() { return getMetrics().pageRectTop; }
807 @Override
808 protected float getPageLength() { return getMetrics().getPageHeight(); }
809 }
810
811 /*
812 * Zooming
813 */
814 @Override
815 public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
816 if (mState == PanZoomState.ANIMATED_ZOOM)
817 return false;
818
819 if (null == mTarget.getZoomConstraints())
820 return false;
821
822 setState(PanZoomState.PINCHING);
823 mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
824 cancelTouch();
825
826 return true;
827 }
828
829 @Override
830 public boolean onScale(SimpleScaleGestureDetector detector) {
831 if (mTarget.isFullScreen())
832 return false;
833
834 if (mState != PanZoomState.PINCHING)
835 return false;
836
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)
840 return true;
841 }
842
843 float spanRatio = detector.getCurrentSpan() / prevSpan;
844
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;
849
850 ZoomConstraints constraints = mTarget.getZoomConstraints();
851
852 if (constraints.getMaxZoom() > 0)
853 maxZoomFactor = constraints.getMaxZoom();
854
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);
863 }
864
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;
872 }
873
874 scrollBy(mLastZoomFocus.x - detector.getFocusX(),
875 mLastZoomFocus.y - detector.getFocusY());
876 PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
877 scaleWithFocus(newZoomFactor, focus);
878 }
879
880 mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
881
882 return true;
883 }
884
885 @Override
886 public void onScaleEnd(SimpleScaleGestureDetector detector) {
887 if (mState == PanZoomState.ANIMATED_ZOOM)
888 return;
889
890 // switch back to the touching state
891 startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
892
893 // Force a viewport synchronisation
894 mTarget.forceRedraw();
895
896 }
897
902 private void scaleWithFocus(float zoomFactor, PointF focus) {
903 ImmutableViewportMetrics viewportMetrics = getMetrics();
904 viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus);
905 mTarget.setViewportMetrics(viewportMetrics);
906 }
907
908 public boolean getRedrawHint() {
909 switch (mState) {
910 case PINCHING:
911 case ANIMATED_ZOOM:
912 case BOUNCE:
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.
916 return false;
917 default:
918 // allow redrawing in other states
919 return true;
920 }
921 }
922
923 @Override
924 public boolean onDown(MotionEvent motionEvent) {
925 mWaitForDoubleTap = mTarget.getZoomConstraints() != null;
926 return false;
927 }
928
929 @Override
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;
938 }
939
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);
944 }
945
946 @Override
947 public void onLongPress(MotionEvent motionEvent) {
948 LOKitShell.sendTouchEvent("LongPress", getMotionInDocumentCoordinates(motionEvent));
949 }
950
951 @Override
952 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
954 return super.onScroll(e1, e2, distanceX, distanceY);
955 }
956
957 @Override
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));
963 }
964 // return false because we still want to get the ACTION_UP event that triggers this
965 return false;
966 }
967
968 @Override
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));
973 }
974 return true;
975 }
976
977 @Override
978 public boolean onDoubleTap(MotionEvent motionEvent) {
979 if (null == mTarget.getZoomConstraints()) {
980 return true;
981 }
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);
993
994 LOKitShell.sendTouchEvent("DoubleTap", pointOfTap);
995 return true;
996 }
997
998 private void cancelTouch() {
999 //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
1000 //GeckoAppShell.sendEventToGecko(e);
1001 }
1002
1009 boolean animatedZoomTo(RectF zoomToRect) {
1010 final float startZoom = getMetrics().zoomFactor;
1011
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
1016 // centered.
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;
1031 }
1032
1033 float finalZoom = viewport.width() / zoomToRect.width();
1034
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));
1040
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);
1044
1045 bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
1046 return true;
1047 }
1048
1053 boolean animatedMove(PointF topLeft, Float zoom) {
1054 RectF moveToRect = getMetrics().getCssViewport();
1055 moveToRect.offsetTo(topLeft.x, topLeft.y);
1056
1057 ImmutableViewportMetrics finalMetrics = getMetrics();
1058
1059 finalMetrics = finalMetrics.setViewportOrigin(
1060 moveToRect.left * finalMetrics.zoomFactor,
1061 moveToRect.top * finalMetrics.zoomFactor);
1062
1063 if (zoom != null) {
1064 finalMetrics = finalMetrics.scaleTo(zoom, new PointF(0.0f, 0.0f));
1065 }
1066 finalMetrics = getValidViewportMetrics(finalMetrics);
1067
1068 bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
1069 return true;
1070 }
1071
1073 public void abortPanning() {
1074 checkMainThread();
1075 bounce();
1076 }
1077
1078 public void setOverScrollMode(int overscrollMode) {
1079 mX.setOverScrollMode(overscrollMode);
1080 mY.setOverScrollMode(overscrollMode);
1081 }
1082
1083 public int getOverScrollMode() {
1084 return mX.getOverScrollMode();
1085 }
1086}
XPropertyListType t
#define LOGTAG
Common static LOKit functions, functions to send events.
Definition: LOKitShell.java:26
static float getDpi(Context context)
Definition: LOKitShell.java:27
Main activity of the LibreOffice App.
void hideSoftKeyboard()
Hides software keyboard on UI thread.
void showPageNumberRect()
Show the page number rectangle on the overlay.
void hidePageNumberRect()
Hide the page number rectangle on the overlay.
ImmutableViewportMetrics are used to store the viewport metrics in way that we can access a version o...
ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t)
A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
float getFocusY()
Returns the Y coordinate of the focus location (the midpoint of the two fingers).
float getFocusX()
Returns the X coordinate of the focus location (the midpoint of the two fingers).
This class handles incoming touch events from the user and sends them to listeners in Gecko and/or pe...
void handleEventListenerAction(boolean allowDefaultAction)
This function is how gecko sends us a default-prevented notification.
void forceRedraw()
This triggers an (asynchronous) viewport update/redraw.
void setViewportMetrics(ImmutableViewportMetrics viewport)
ZoomConstraints getZoomConstraints()
void setAnimationTarget(ImmutableViewportMetrics viewport)
boolean post(Runnable action)
PointF convertViewPointToLayerPoint(PointF viewPoint)
ImmutableViewportMetrics getViewportMetrics()
def run(arg=None, arg2=-1)
@ Exception
int i
#define MAX_ZOOM
const wchar_t *typedef int(__stdcall *DllNativeUnregProc)(int