From 293c360ffe2980b4d5d2d7c4aff69166ff265e9c Mon Sep 17 00:00:00 2001 From: Derek Sollenberger Date: Fri, 4 Jun 2010 10:44:48 -0400 Subject: [PATCH] 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 --- core/java/android/webkit/WebView.java | 242 +++++++--------------- core/java/android/webkit/ZoomManager.java | 165 +++++++++++++-- 2 files changed, 222 insertions(+), 185 deletions(-) diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 19171f1cb17f8..3173de9905fad 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -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,32 +4451,36 @@ 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()) { - mLastTouchTime = eventTime; + 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 = detector.getFocusX(); + y = detector.getFocusY(); + action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action == MotionEvent.ACTION_POINTER_DOWN) { + cancelTouch(); + action = MotionEvent.ACTION_DOWN; + } else if (action == MotionEvent.ACTION_POINTER_UP) { + // set mLastTouchX/Y to the remaining point + mLastTouchX = x; + mLastTouchY = y; + } else if (action == MotionEvent.ACTION_MOVE) { + // negative x or y indicate it is on the edge, skip it. + if (x < 0 || y < 0) { return true; } - x = mScaleDetector.getFocusX(); - y = mScaleDetector.getFocusY(); - action = ev.getAction() & MotionEvent.ACTION_MASK; - if (action == MotionEvent.ACTION_POINTER_DOWN) { - cancelTouch(); - action = MotionEvent.ACTION_DOWN; - } else if (action == MotionEvent.ACTION_POINTER_UP) { - // set mLastTouchX/Y to the remaining point - mLastTouchX = x; - mLastTouchY = y; - } else if (action == MotionEvent.ACTION_MOVE) { - // negative x or y indicate it is on the edge, skip it. - if (x < 0 || y < 0) { - return true; - } - } - } else { - // if the page disallow zoom, skip multi-pointer action - return true; } } else { action = ev.getAction(); @@ -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; } diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java index 1540865116a38..d002b5b3d1d17 100644 --- a/core/java/android/webkit/ZoomManager.java +++ b/core/java/android/webkit/ZoomManager.java @@ -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;