6package org.mozilla.gecko.gfx;
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;
22import java.util.Timer;
23import java.util.TimerTask;
31class JavaPanZoomController
32 extends GestureDetector.SimpleOnGestureListener
33 implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener
35 private static final String LOGTAG =
"GeckoPanZoomController";
38 private static final float STOPPED_THRESHOLD = 4.0f;
41 private static final float FLING_STOPPED_THRESHOLD = 0.1f;
45 private final float PAN_THRESHOLD;
48 private static final double AXIS_LOCK_ANGLE =
Math.PI / 6.0;
51 private static final float MAX_ZOOM = 4.0f;
54 private static final float DOUBLE_TAP_THRESHOLD = 1.0f;
57 private final float MAX_SCROLL;
78 private final SubdocumentScrollHelper mSubscroller;
79 private final Axis mX;
80 private final Axis mY;
82 private Thread mMainThread;
86 private Timer mAnimationTimer;
90 private PointF mLastZoomFocus;
92 private long mLastEventTime;
96 private boolean mWaitForDoubleTap;
103 mSubscroller =
new SubdocumentScrollHelper();
104 mX =
new AxisX(mSubscroller);
105 mY =
new AxisY(mSubscroller);
108 mMainThread = mContext.getMainLooper().getThread();
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() {
137 private void checkMainThread() {
138 if (mMainThread != Thread.currentThread()) {
140 Log.e(LOGTAG,
"Uh-oh, we're running on the wrong thread!",
new Exception());
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);
154 public boolean onTouchEvent(MotionEvent 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);
169 public void notifyDefaultActionPrevented(
boolean prevented) {
174 public void abortAnimation() {
189 setState(PanZoomState.NOTHING);
194 synchronized (mTarget.
getLock()) {
203 void startingNewEventBlock(MotionEvent event,
boolean waitingForTouchListeners) {
205 mSubscroller.cancel();
206 if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
210 setState(PanZoomState.WAITING_LISTENERS);
215 void preventedTouchFinished() {
217 if (mState == PanZoomState.WAITING_LISTENERS) {
226 public void pageRectUpdated() {
227 if (mState == PanZoomState.NOTHING) {
228 synchronized (mTarget.
getLock()) {
229 ImmutableViewportMetrics validated = getValidViewportMetrics();
230 if (!getMetrics().fuzzyEquals(validated)) {
243 private boolean handleTouchStart(MotionEvent event) {
246 stopAnimationTimer();
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) {
284 case WAITING_LISTENERS:
286 Log.e(LOGTAG,
"Received impossible touch move while in " + mState);
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);
322 Log.e(LOGTAG,
"Unhandled case " + mState +
" in handleTouchMove");
326 private boolean handleTouchEnd(MotionEvent event) {
331 case WAITING_LISTENERS:
333 Log.e(LOGTAG,
"Received impossible touch end while in " + mState);
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) {
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);
405 angle =
Math.abs(angle);
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)) {
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);
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);
478 private void fling() {
481 stopAnimationTimer();
483 boolean stopped = stopped();
484 mX.startFling(stopped);
485 mY.startFling(stopped);
487 startAnimationTimer(
new FlingRunnable());
491 private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
492 stopAnimationTimer();
494 ImmutableViewportMetrics bounceStartMetrics = getMetrics();
495 if (bounceStartMetrics.fuzzyEquals(metrics)) {
496 setState(PanZoomState.NOTHING);
508 startAnimationTimer(
new BounceRunnable(bounceStartMetrics, metrics));
512 private void bounce() {
513 bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
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);
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);
595 final void terminate() {
641 synchronized (mTarget.
getLock()) {
651 synchronized (mTarget.
getLock()) {
672 boolean flingingX = mX.advanceFling();
673 boolean flingingY = mY.advanceFling();
675 boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
678 if (flingingX || flingingY) {
687 float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
688 if (getVelocity() >= threshold) {
708 private void finishAnimation() {
711 stopAnimationTimer();
721 return getValidViewportMetrics(getMetrics());
724 private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
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;
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) {
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);
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;
788 AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
790 public float getOrigin() {
return getMetrics().viewportRectLeft; }
800 AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
802 public float getOrigin() {
return getMetrics().viewportRectTop; }
816 if (mState == PanZoomState.ANIMATED_ZOOM)
822 setState(PanZoomState.PINCHING);
830 public boolean onScale(SimpleScaleGestureDetector detector) {
834 if (mState != PanZoomState.PINCHING)
837 float prevSpan = detector.getPreviousSpan();
838 if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
843 float spanRatio = detector.getCurrentSpan() / prevSpan;
845 synchronized (mTarget.
getLock()) {
846 float newZoomFactor = getMetrics().zoomFactor * spanRatio;
847 float minZoomFactor = 0.0f;
852 if (constraints.getMaxZoom() > 0)
855 if (newZoomFactor < minZoomFactor) {
859 final float rate = 0.5f;
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) {
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)
891 startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
902 private void scaleWithFocus(
float zoomFactor, PointF focus) {
903 ImmutableViewportMetrics viewportMetrics = getMetrics();
904 viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus);
908 public boolean getRedrawHint() {
924 public boolean onDown(MotionEvent motionEvent) {
930 public void onShowPress(MotionEvent motionEvent) {
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));
947 public void onLongPress(MotionEvent motionEvent) {
948 LOKitShell.sendTouchEvent(
"LongPress", getMotionInDocumentCoordinates(motionEvent));
952 public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX,
float distanceY) {
954 return super.onScroll(e1, e2, distanceX, distanceY);
958 public boolean onSingleTapUp(MotionEvent motionEvent) {
961 if (!mWaitForDoubleTap) {
962 LOKitShell.sendTouchEvent(
"SingleTap", getMotionInDocumentCoordinates(motionEvent));
969 public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
971 if (mWaitForDoubleTap) {
972 LOKitShell.sendTouchEvent(
"SingleTap", getMotionInDocumentCoordinates(motionEvent));
978 public boolean onDoubleTap(MotionEvent motionEvent) {
983 PointF pointOfTap = getMotionInDocumentCoordinates(motionEvent);
984 ImmutableViewportMetrics metrics = getMetrics();
985 float newZoom = metrics.getZoomFactor() >=
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());
992 animatedMove(
new PointF(newLeft, newTop), newZoom);
994 LOKitShell.sendTouchEvent(
"DoubleTap", pointOfTap);
998 private void cancelTouch() {
1009 boolean animatedZoomTo(RectF zoomToRect) {
1010 final float startZoom = getMetrics().zoomFactor;
1012 RectF viewport = getMetrics().getViewport();
1017 float targetRatio = viewport.width() / viewport.height();
1018 float rectRatio = zoomToRect.width() / zoomToRect.height();
1019 if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
1021 }
else if (targetRatio < rectRatio) {
1023 float newHeight = zoomToRect.width() / targetRatio;
1024 zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
1025 zoomToRect.bottom = zoomToRect.top + newHeight;
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));
1043 finalMetrics = getValidViewportMetrics(finalMetrics);
1045 bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
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);
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();
Common static LOKit functions, functions to send events.
static float getDpi(Context context)
Main activity of the LibreOffice App.
DocumentOverlay getDocumentOverlay()
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.
final float getDefaultZoom()
ImmutableViewportMetrics are used to store the viewport metrics in way that we can access a version o...
ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t)
boolean mAnimationTerminated
abstract void animateFrame()
float getViewportLength()
float getViewportLength()
ImmutableViewportMetrics mBounceStartMetrics
ImmutableViewportMetrics mBounceEndMetrics
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.
boolean handleEvent(MotionEvent event)
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)
const wchar_t *typedef int(__stdcall *DllNativeUnregProc)(int