Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 293c360f authored by Derek Sollenberger's avatar Derek Sollenberger
Browse files

Move pinch-to-zoom logic into ZoomManager.

This CL is one in a series of CL's that moves the zoom logic
from WebView into ZoomManager.

Change-Id: I9980dd78dbc3345d465d4f39afcfd2f025f45bcb
http://b/2671604
parent 5e4d9a04
Loading
Loading
Loading
Loading
+80 −162
Original line number Diff line number Diff line
@@ -520,11 +520,6 @@ public class WebView extends AbsoluteLayout
    private static final int MOTIONLESS_IGNORE          = 3;
    private int mHeldMotionless;

    // whether support multi-touch
    private boolean mSupportMultiTouch;
    // use the framework's ScaleGestureDetector to handle multi-touch
    private ScaleGestureDetector mScaleDetector;

    // An instance for injecting accessibility in WebViews with disabled
    // JavaScript or ones for which no accessibility script exists
    private AccessibilityInjector mAccessibilityInjector;
@@ -873,16 +868,7 @@ public class WebView extends AbsoluteLayout
    }

    void updateMultiTouchSupport(Context context) {
        WebSettings settings = getSettings();
        mSupportMultiTouch = context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
                && settings.supportZoom() && settings.getBuiltInZoomControls();
        if (mSupportMultiTouch && (mScaleDetector == null)) {
            mScaleDetector = new ScaleGestureDetector(context,
                    new ScaleDetectorListener());
        } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
            mScaleDetector = null;
        }
        mZoomManager.updateMultiTouchSupport(context);
    }

    private void init() {
@@ -2125,7 +2111,7 @@ public class WebView extends AbsoluteLayout
    private Rect mLastGlobalRect;

    Rect sendOurVisibleRect() {
        if (mZoomManager.mPreviewZoomOnly) return mLastVisibleRectSent;
        if (mZoomManager.isPreventingWebkitUpdates()) return mLastVisibleRectSent;

        Rect rect = new Rect();
        calcOurContentVisibleRect(rect);
@@ -2217,7 +2203,7 @@ public class WebView extends AbsoluteLayout
     * @return true if new values were sent
     */
    boolean sendViewSizeZoom(boolean force) {
        if (mZoomManager.mPreviewZoomOnly) return false;
        if (mZoomManager.isPreventingWebkitUpdates()) return false;

        int viewWidth = getViewWidth();
        int newWidth = Math.round(viewWidth * mZoomManager.mInvActualScale);
@@ -2240,7 +2226,8 @@ public class WebView extends AbsoluteLayout
            data.mHeight = newHeight;
            data.mTextWrapWidth = Math.round(viewWidth / mZoomManager.mTextWrapScale);
            data.mScale = mZoomManager.mActualScale;
            data.mIgnoreHeight = mZoomManager.isZoomAnimating() && !mHeightCanMeasure;
            data.mIgnoreHeight = mZoomManager.isFixedLengthAnimationInProgress()
                    && !mHeightCanMeasure;
            data.mAnchorX = mAnchorX;
            data.mAnchorY = mAnchorY;
            mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data);
@@ -3219,15 +3206,6 @@ public class WebView extends AbsoluteLayout
        }
    }

    /**
     * Need to adjust the WebTextView after a change in zoom, since mActualScale
     * has changed.  This is especially important for password fields, which are
     * drawn by the WebTextView, since it conveys more information than what
     * webkit draws.  Thus we need to reposition it to show in the correct
     * place.
     */
    private boolean mNeedToAdjustWebTextView;

    private boolean didUpdateTextViewBounds(boolean allowIntersect) {
        Rect contentBounds = nativeFocusCandidateNodeBounds();
        Rect vBox = contentToViewRect(contentBounds);
@@ -3265,6 +3243,32 @@ public class WebView extends AbsoluteLayout
        canvas.setDrawFilter(null);
    }

    private void onZoomAnimationStart() {
        // If it is in password mode, turn it off so it does not draw misplaced.
        if (inEditingMode() && nativeFocusCandidateIsPassword()) {
            mWebTextView.setInPassword(false);
        }
    }

    private void onZoomAnimationEnd() {
        // adjust the edit text view if needed
        if (inEditingMode() && didUpdateTextViewBounds(false) && nativeFocusCandidateIsPassword()) {
            // If it is a password field, start drawing the WebTextView once
            // again.
            mWebTextView.setInPassword(true);
        }
    }

    void onFixedLengthZoomAnimationStart() {
        WebViewCore.pauseUpdatePicture(getWebViewCore());
        onZoomAnimationStart();
    }

    void onFixedLengthZoomAnimationEnd() {
        WebViewCore.resumeUpdatePicture(mWebViewCore);
        onZoomAnimationEnd();
    }

    private void drawCoreAndCursorRing(Canvas canvas, int color,
        boolean drawCursorRing) {
        if (mDrawHistory) {
@@ -3273,7 +3277,7 @@ public class WebView extends AbsoluteLayout
            return;
        }

        boolean animateZoom = mZoomManager.isZoomAnimating();
        boolean animateZoom = mZoomManager.isFixedLengthAnimationInProgress();
        boolean animateScroll = ((!mScroller.isFinished()
                || mVelocityTracker != null)
                && (mTouchMode != TOUCH_DRAG_MODE ||
@@ -3292,39 +3296,7 @@ public class WebView extends AbsoluteLayout
            }
        }
        if (animateZoom) {
            final float[] zoomValues = mZoomManager.animateZoom();
            final boolean isStillAnimating = mZoomManager.isZoomAnimating();

            if (isStillAnimating) {
                invalidate();
            } else {
                WebViewCore.resumeUpdatePicture(mWebViewCore);
                // call invalidate() again to draw with the final filters
                invalidate();
                if (mNeedToAdjustWebTextView) {
                    mNeedToAdjustWebTextView = false;
                    if (didUpdateTextViewBounds(false)
                            && nativeFocusCandidateIsPassword()) {
                        // If it is a password field, start drawing the
                        // WebTextView once again.
                        mWebTextView.setInPassword(true);
                    }
                }
            }

            canvas.translate(zoomValues[0], zoomValues[1]);
            canvas.scale(zoomValues[2], zoomValues[2]);

            if (inEditingMode() && !mNeedToAdjustWebTextView && isStillAnimating) {
                // The WebTextView is up.  Keep track of this so we can adjust
                // its size and placement when we finish zooming
                mNeedToAdjustWebTextView = true;
                // If it is in password mode, turn it off so it does not draw
                // misplaced.
                if (nativeFocusCandidateIsPassword()) {
                    mWebTextView.setInPassword(false);
                }
            }
            mZoomManager.animateZoom(canvas);
        } else {
            canvas.scale(mZoomManager.mActualScale, mZoomManager.mActualScale);
        }
@@ -3339,7 +3311,7 @@ public class WebView extends AbsoluteLayout
            invalidate();
        }
        mWebViewCore.drawContentPicture(canvas, color,
                (animateZoom || mZoomManager.mPreviewZoomOnly || UIAnimationsRunning),
                (mZoomManager.isZoomAnimating() || UIAnimationsRunning),
                animateScroll);
        if (mNativeClass == 0) return;
        // decide which adornments to draw
@@ -3352,7 +3324,7 @@ public class WebView extends AbsoluteLayout
            }
        } else if (mShiftIsPressed
                && !nativePageShouldHandleShiftAndArrows()) {
            if (!animateZoom && !mZoomManager.mPreviewZoomOnly) {
            if (!mZoomManager.isZoomAnimating()) {
                extras = DRAW_EXTRAS_SELECTION;
                nativeSetSelectionRegion(mTouchSelection || mExtendSelection);
                nativeSetSelectionPointer(!mTouchSelection, mZoomManager.mInvActualScale,
@@ -4425,79 +4397,6 @@ public class WebView extends AbsoluteLayout
    private DragTracker mDragTracker;
    private DragTrackerHandler mDragTrackerHandler;

    private class ScaleDetectorListener implements
            ScaleGestureDetector.OnScaleGestureListener {

        public boolean onScaleBegin(ScaleGestureDetector detector) {
            // cancel the single touch handling
            cancelTouch();
            mZoomManager.dismissZoomPicker();
            // reset the zoom overview mode so that the page won't auto grow
            mZoomManager.mInZoomOverview = false;
            // If it is in password mode, turn it off so it does not draw
            // misplaced.
            if (inEditingMode() && nativeFocusCandidateIsPassword()) {
                mWebTextView.setInPassword(false);
            }

            mViewManager.startZoom();

            return true;
        }

        public void onScaleEnd(ScaleGestureDetector detector) {
            if (mZoomManager.mPreviewZoomOnly) {
                mZoomManager.mPreviewZoomOnly = false;
                mAnchorX = viewToContentX((int) mZoomManager.mZoomCenterX + mScrollX);
                mAnchorY = viewToContentY((int) mZoomManager.mZoomCenterY + mScrollY);
                // don't reflow when zoom in; when zoom out, do reflow if the
                // new scale is almost minimum scale;
                boolean reflowNow = !mZoomManager.canZoomOut()
                        || (mZoomManager.mActualScale <= 0.8 * mZoomManager.mTextWrapScale);
                // force zoom after mPreviewZoomOnly is set to false so that the
                // new view size will be passed to the WebKit
                mZoomManager.refreshZoomScale(reflowNow);
                // call invalidate() to draw without zoom filter
                invalidate();
            }
            // adjust the edit text view if needed
            if (inEditingMode() && didUpdateTextViewBounds(false)
                    && nativeFocusCandidateIsPassword()) {
                // If it is a password field, start drawing the
                // WebTextView once again.
                mWebTextView.setInPassword(true);
            }
            // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as it
            // may trigger the unwanted click, can't use TOUCH_DRAG_MODE as it
            // may trigger the unwanted fling.
            mTouchMode = TOUCH_PINCH_DRAG;
            mConfirmMove = true;
            startTouch(detector.getFocusX(), detector.getFocusY(),
                    mLastTouchTime);

            mViewManager.endZoom();
        }

        public boolean onScale(ScaleGestureDetector detector) {
            float scale = (float) (Math.round(detector.getScaleFactor()
                    * mZoomManager.mActualScale * 100) / 100.0);
            if (mZoomManager.willScaleTriggerZoom(scale)) {
                mZoomManager.mPreviewZoomOnly = true;
                // limit the scale change per step
                if (scale > mZoomManager.mActualScale) {
                    scale = Math.min(scale, mZoomManager.mActualScale * 1.25f);
                } else {
                    scale = Math.max(scale, mZoomManager.mActualScale * 0.8f);
                }
                mZoomManager.setZoomCenter(detector.getFocusX(), detector.getFocusY());
                mZoomManager.setZoomScale(scale, false);
                invalidate();
                return true;
            }
            return false;
        }
    }

    private boolean hitFocusedPlugin(int contentX, int contentY) {
        if (DebugFlags.WEB_VIEW) {
            Log.v(LOGTAG, "nativeFocusIsPlugin()=" + nativeFocusIsPlugin());
@@ -4519,6 +4418,22 @@ public class WebView extends AbsoluteLayout
        return mFullScreenHolder != null;
    }

    void onPinchToZoomAnimationStart() {
        // cancel the single touch handling
        cancelTouch();
        onZoomAnimationStart();
    }

    void onPinchToZoomAnimationEnd(ScaleGestureDetector detector) {
        onZoomAnimationEnd();
        // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as
        // it may trigger the unwanted click, can't use TOUCH_DRAG_MODE
        // as it may trigger the unwanted fling.
        mTouchMode = TOUCH_PINCH_DRAG;
        mConfirmMove = true;
        startTouch(detector.getFocusX(), detector.getFocusY(), mLastTouchTime);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mNativeClass == 0 || !isClickable() || !isLongClickable()) {
@@ -4536,15 +4451,23 @@ public class WebView extends AbsoluteLayout

        // FIXME: we may consider to give WebKit an option to handle multi-touch
        // events later.
        if (mSupportMultiTouch && ev.getPointerCount() > 1) {
            if (mZoomManager.mMinZoomScale < mZoomManager.mMaxZoomScale) {
                mScaleDetector.onTouchEvent(ev);
                if (mScaleDetector.isInProgress()) {
        if (mZoomManager.supportsMultiTouchZoom() && ev.getPointerCount() > 1) {

            // if the page disallows zoom, then skip multi-pointer action
            if (mZoomManager.mMinZoomScale >= mZoomManager.mMaxZoomScale) {
                return true;
            }

            ScaleGestureDetector detector = mZoomManager.getMultiTouchGestureDetector();
            detector.onTouchEvent(ev);

            if (detector.isInProgress()) {
                mLastTouchTime = eventTime;
                return true;
            }
                x = mScaleDetector.getFocusX();
                y = mScaleDetector.getFocusY();

            x = detector.getFocusX();
            y = detector.getFocusY();
            action = ev.getAction() & MotionEvent.ACTION_MASK;
            if (action == MotionEvent.ACTION_POINTER_DOWN) {
                cancelTouch();
@@ -4559,10 +4482,6 @@ public class WebView extends AbsoluteLayout
                    return true;
                }
            }
            } else {
                // if the page disallow zoom, skip multi-pointer action
                return true;
            }
        } else {
            action = ev.getAction();
            x = ev.getX();
@@ -4625,7 +4544,6 @@ public class WebView extends AbsoluteLayout
                                contentX, contentY) : false;
                    }
                } else { // the normal case
                    mZoomManager.mPreviewZoomOnly = false;
                    mTouchMode = TOUCH_INIT_MODE;
                    mDeferTouchProcess = (!inFullScreenMode()
                            && mForwardTouchEvents) ? hitFocusedPlugin(
@@ -5922,7 +5840,7 @@ public class WebView extends AbsoluteLayout
                                                 boolean immediate) {
        // don't scroll while in zoom animation. When it is done, we will adjust
        // the necessary components (e.g., WebTextView if it is in editing mode)
        if(mZoomManager.isZoomAnimating()) {
        if (mZoomManager.isFixedLengthAnimationInProgress()) {
            return false;
        }

+142 −23
Original line number Diff line number Diff line
@@ -16,11 +16,33 @@

package android.webkit;

import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Point;
import android.os.SystemClock;
import android.util.Log;
import android.view.ScaleGestureDetector;
import android.view.View;

/**
 * The ZoomManager is responsible for maintaining the WebView's current zoom
 * level state.  It is also responsible for managing the on-screen zoom controls
 * as well as any animation of the WebView due to zooming.
 *
 * Currently, there are two methods for animating the zoom of a WebView.
 *
 * (1) The first method is triggered by startZoomAnimation(...) and is a fixed
 * length animation where the final zoom scale is known at startup.  This type of
 * animation notifies webkit of the final scale BEFORE it animates. The animation
 * is then done by scaling the CANVAS incrementally based on a stepping function.
 *
 * (2) The second method is triggered by a multi-touch pinch and the new scale
 * is determined dynamically based on the user's gesture. This type of animation
 * only notifies webkit of new scale AFTER the gesture is complete. The animation
 * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView)
 * to the new scale in response to events related to the user's gesture.
 */
class ZoomManager {

    static final String LOGTAG = "webviewZoom";
@@ -73,9 +95,6 @@ class ZoomManager {

    private static float MINIMUM_SCALE_INCREMENT = 0.01f;

    // set to true temporarily during ScaleGesture triggered zoom
    boolean mPreviewZoomOnly = false;

    // the current computed zoom scale and its inverse.
    float mActualScale;
    float mInvActualScale;
@@ -104,6 +123,14 @@ class ZoomManager {
    private long mZoomStart;
    static final int ZOOM_ANIMATION_LENGTH = 500;

    // whether support multi-touch
    private boolean mSupportMultiTouch;

    // use the framework's ScaleGestureDetector to handle multi-touch
    private ScaleGestureDetector mScaleDetector;

    private boolean mPinchToZoomAnimating = false;

    public ZoomManager(WebView webView, CallbackProxy callbackProxy) {
        mWebView = webView;
        mCallbackProxy = callbackProxy;
@@ -226,7 +253,7 @@ class ZoomManager {
            mInvInitialZoomScale = 1.0f / oldScale;
            mInvFinalZoomScale = 1.0f / mActualScale;
            mZoomScale = mActualScale;
            WebViewCore.pauseUpdatePicture(mWebView.getWebViewCore());
            mWebView.onFixedLengthZoomAnimationStart();
            mWebView.invalidate();
            return true;
        } else {
@@ -235,25 +262,23 @@ class ZoomManager {
    }

    /**
     * Computes and returns the relevant data needed by the WebView's drawing
     * model to animate a zoom.
     * This method is called by the WebView's drawing code when a fixed length zoom
     * animation is occurring. Its purpose is to animate the zooming of the canvas
     * to the desired scale which was specified in startZoomAnimation(...).
     *
     * This method is to be called when a zoom animation is occurring. The
     * animation begins by calling startZoomAnimation(...).  The caller can
     * check to see if the animation has completed by calling isZoomAnimating().
     * A fixed length animation begins when startZoomAnimation(...) is called and
     * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that
     * interval each time the WebView draws it calls this function which is 
     * responsible for generating the animation.
     *
     * @return an array containing the values needed to animate the drawing
     * surface.
     * [0] = delta for the new scrollX position
     * [1] = delta for the new scrollY position
     * [2] = current zoom scale
     * Additionally, the WebView can check to see if such an animation is currently
     * in progress by calling isFixedLengthAnimationInProgress().
     */
    public float[] animateZoom() {
    public void animateZoom(Canvas canvas) {
        if (mZoomScale == 0) {
            Log.w(LOGTAG, "A WebView is attempting to animate a zoom when no " +
                    "zoom is in progress");
            float[] result = {0, 0, mActualScale};
            return result;
            Log.w(LOGTAG, "A WebView is attempting to perform a fixed length "
                    + "zoom animation when no zoom is in progress");
            return;
        }

        float zoomScale;
@@ -262,10 +287,12 @@ class ZoomManager {
            float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
            zoomScale = 1.0f / (mInvInitialZoomScale
                    + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
            mWebView.invalidate();
        } else {
            zoomScale = mZoomScale;
            // set mZoomScale to be 0 as we have finished animating
            mZoomScale = 0;
            mWebView.onFixedLengthZoomAnimationEnd();
        }
        // calculate the intermediate scroll position. Since we need to use
        // zoomScale, we can't use the WebView's pinLocX/Y functions directly.
@@ -281,11 +308,15 @@ class ZoomManager {
                - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight()
                * zoomScale)) + titleHeight) + mWebView.getScrollY();

        float[] result = {tx, ty, zoomScale};
        return result;
        canvas.translate(tx, ty);
        canvas.scale(zoomScale, zoomScale);
    }

    public boolean isZoomAnimating() {
        return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating;
    }

    public boolean isFixedLengthAnimationInProgress() {
        return mZoomScale != 0;
    }

@@ -316,7 +347,7 @@ class ZoomManager {
            float oldScale = mActualScale;
            float oldInvScale = mInvActualScale;

            if (scale != mActualScale && !mPreviewZoomOnly) {
            if (scale != mActualScale && !mPinchToZoomAnimating) {
                mCallbackProxy.onScaleChanged(mActualScale, scale);
            }

@@ -357,10 +388,98 @@ class ZoomManager {
        }
    }

    public void updateMultiTouchSupport(Context context) {
        // check the preconditions
        assert mWebView.getSettings() != null;

        WebSettings settings = mWebView.getSettings();
        mSupportMultiTouch = context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
                && settings.supportZoom() && settings.getBuiltInZoomControls();
        if (mSupportMultiTouch && (mScaleDetector == null)) {
            mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener());
        } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
            mScaleDetector = null;
        }
    }

    public boolean supportsMultiTouchZoom() {
        return mSupportMultiTouch;
    }

    /**
     * Notifies the caller that the ZoomManager is requesting that scale related
     * updates should not be sent to webkit. This can occur in cases where the
     * ZoomManager is performing an animation and does not want webkit to update
     * until the animation is complete.
     *
     * @return true if scale related updates should not be sent to webkit and
     *         false otherwise.
     */
    public boolean isPreventingWebkitUpdates() {
        // currently only animating a multi-touch zoom prevents updates, but
        // others can add their own conditions to this method if necessary.
        return mPinchToZoomAnimating;
    }

    public ScaleGestureDetector getMultiTouchGestureDetector() {
        return mScaleDetector;
    }

    private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener {

        public boolean onScaleBegin(ScaleGestureDetector detector) {
            dismissZoomPicker();
            // reset the zoom overview mode so that the page won't auto grow
            mInZoomOverview = false;
            mWebView.mViewManager.startZoom();
            mWebView.onPinchToZoomAnimationStart();
            return true;
        }

        public boolean onScale(ScaleGestureDetector detector) {
            float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f;
            if (willScaleTriggerZoom(scale)) {
                mPinchToZoomAnimating = true;
                // limit the scale change per step
                if (scale > mActualScale) {
                    scale = Math.min(scale, mActualScale * 1.25f);
                } else {
                    scale = Math.max(scale, mActualScale * 0.8f);
                }
                setZoomCenter(detector.getFocusX(), detector.getFocusY());
                setZoomScale(scale, false);
                mWebView.invalidate();
                return true;
            }
            return false;
        }

        public void onScaleEnd(ScaleGestureDetector detector) {
            if (mPinchToZoomAnimating) {
                mPinchToZoomAnimating = false;
                mWebView.setViewSizeAnchor(mWebView.viewToContentX((int) mZoomCenterX
                        + mWebView.getScrollX()), mWebView.viewToContentY((int) mZoomCenterY
                        + mWebView.getScrollY()));
                // don't reflow when zoom in; when zoom out, do reflow if the
                // new scale is almost minimum scale;
                boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale);
                // force zoom after mPreviewZoomOnly is set to false so that the
                // new view size will be passed to the WebKit
                refreshZoomScale(reflowNow);
                // call invalidate() to draw without zoom filter
                mWebView.invalidate();
            }

            mWebView.mViewManager.endZoom();
            mWebView.onPinchToZoomAnimationEnd(detector);
        }
    }

    public void onSizeChanged(int w, int h, int ow, int oh) {
        // reset zoom and anchor to the top left corner of the screen
        // unless we are already zooming
        if (!isZoomAnimating()) {
        if (!isFixedLengthAnimationInProgress()) {
            int visibleTitleHeight = mWebView.getVisibleTitleHeight();
            mZoomCenterX = 0;
            mZoomCenterY = visibleTitleHeight;