mobile/android/base/gfx/JavaPanZoomController.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial