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
This commit is contained in:
Derek Sollenberger
2010-06-04 10:44:48 -04:00
parent 5e4d9a04fd
commit 293c360ffe
2 changed files with 222 additions and 185 deletions

View File

@@ -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;
}

View File

@@ -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;