blob: f2a1ec395b09ce25bbe26733e8302b82e195daec [file] [log] [blame]
Derek Sollenberger90b6e482010-05-10 12:38:54 -04001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
Derek Sollenberger293c3602010-06-04 10:44:48 -040019import android.content.Context;
20import android.content.pm.PackageManager;
21import android.graphics.Canvas;
Derek Sollenberger341e22f2010-06-02 12:34:34 -040022import android.graphics.Point;
Derek Sollenbergerdd1173b2011-01-18 11:11:28 -050023import android.graphics.Rect;
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040024import android.os.Bundle;
Derek Sollenberger03e48912010-05-18 17:03:42 -040025import android.os.SystemClock;
Huahui Wu463cc0c2011-03-07 10:22:18 -080026import android.util.FloatMath;
Derek Sollenberger87b17be52010-06-01 11:49:31 -040027import android.util.Log;
Derek Sollenberger293c3602010-06-04 10:44:48 -040028import android.view.ScaleGestureDetector;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040029import android.view.View;
30
Derek Sollenberger293c3602010-06-04 10:44:48 -040031/**
32 * The ZoomManager is responsible for maintaining the WebView's current zoom
33 * level state. It is also responsible for managing the on-screen zoom controls
34 * as well as any animation of the WebView due to zooming.
35 *
36 * Currently, there are two methods for animating the zoom of a WebView.
37 *
38 * (1) The first method is triggered by startZoomAnimation(...) and is a fixed
39 * length animation where the final zoom scale is known at startup. This type of
40 * animation notifies webkit of the final scale BEFORE it animates. The animation
41 * is then done by scaling the CANVAS incrementally based on a stepping function.
42 *
43 * (2) The second method is triggered by a multi-touch pinch and the new scale
44 * is determined dynamically based on the user's gesture. This type of animation
45 * only notifies webkit of new scale AFTER the gesture is complete. The animation
46 * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView)
47 * to the new scale in response to events related to the user's gesture.
48 */
Derek Sollenberger90b6e482010-05-10 12:38:54 -040049class ZoomManager {
50
51 static final String LOGTAG = "webviewZoom";
52
53 private final WebView mWebView;
Derek Sollenberger03e48912010-05-18 17:03:42 -040054 private final CallbackProxy mCallbackProxy;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040055
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040056 // Widgets responsible for the on-screen zoom functions of the WebView.
Derek Sollenberger90b6e482010-05-10 12:38:54 -040057 private ZoomControlEmbedded mEmbeddedZoomControl;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040058 private ZoomControlExternal mExternalZoomControl;
59
Derek Sollenberger4aef6972010-06-24 15:03:43 -040060 /*
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -070061 * For large screen devices, the defaultScale usually set to 1.0 and
62 * equal to the overview scale, to differentiate the zoom level for double tapping,
Shimeng (Simon) Wang938a1142010-09-21 13:42:07 -070063 * a default reading level scale is used.
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -070064 */
Shimeng (Simon) Wang938a1142010-09-21 13:42:07 -070065 private static final float DEFAULT_READING_LEVEL_SCALE = 1.5f;
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -070066
67 /*
Derek Sollenberger4aef6972010-06-24 15:03:43 -040068 * The scale factors that determine the upper and lower bounds for the
69 * default zoom scale.
70 */
71 protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f;
72 protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040073
Derek Sollenberger4aef6972010-06-24 15:03:43 -040074 // The default scale limits, which are dependent on the display density.
75 private float mDefaultMaxZoomScale;
76 private float mDefaultMinZoomScale;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040077
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040078 // The actual scale limits, which can be set through a webpage's viewport
79 // meta-tag.
Derek Sollenberger369aca22010-06-09 14:11:59 -040080 private float mMaxZoomScale;
81 private float mMinZoomScale;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040082
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040083 // Locks the minimum ZoomScale to the value currently set in mMinZoomScale.
Derek Sollenberger369aca22010-06-09 14:11:59 -040084 private boolean mMinZoomScaleFixed = true;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040085
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -040086 /*
Derek Sollenberger94345f42010-06-25 09:42:53 -040087 * When loading a new page the WebView does not initially know the final
88 * width of the page. Therefore, when a new page is loaded in overview mode
89 * the overview scale is initialized to a default value. This flag is then
90 * set and used to notify the ZoomManager to take the width of the next
91 * picture from webkit and use that width to enter into zoom overview mode.
92 */
93 private boolean mInitialZoomOverview = false;
94
95 /*
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -040096 * When in the zoom overview mode, the page's width is fully fit to the
97 * current window. Additionally while the page is in this state it is
98 * active, in other words, you can click to follow the links. We cache a
99 * boolean to enable us to quickly check whether or not we are in overview
100 * mode, but this value should only be modified by changes to the zoom
101 * scale.
102 */
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400103 private boolean mInZoomOverview = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400104 private int mZoomOverviewWidth;
105 private float mInvZoomOverviewWidth;
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400106
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400107 /*
108 * These variables track the center point of the zoom and they are used to
109 * determine the point around which we should zoom. They are stored in view
110 * coordinates.
111 */
112 private float mZoomCenterX;
113 private float mZoomCenterY;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400114
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400115 /*
Huahui Wu463cc0c2011-03-07 10:22:18 -0800116 * Similar to mZoomCenterX(Y), these track the focus point of the scale
117 * gesture. The difference is these get updated every time when onScale is
118 * invoked no matter if a zooming really happens.
119 */
120 private float mFocusX;
121 private float mFocusY;
122
123 /*
Huahui Wu31484fb2011-03-10 17:37:15 -0800124 * mFocusMovementQueue keeps track of the previous focus point movement
125 * has been through. Comparing to the difference of the gesture's previous
126 * span and current span, it determines if the gesture is for panning or
127 * zooming or both.
Huahui Wu463cc0c2011-03-07 10:22:18 -0800128 */
Huahui Wu31484fb2011-03-10 17:37:15 -0800129 private FocusMovementQueue mFocusMovementQueue;
Huahui Wu463cc0c2011-03-07 10:22:18 -0800130
131 /*
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400132 * These values represent the point around which the screen should be
133 * centered after zooming. In other words it is used to determine the center
134 * point of the visible document after the page has finished zooming. This
135 * is important because the zoom may have potentially reflowed the text and
136 * we need to ensure the proper portion of the document remains on the
137 * screen.
138 */
139 private int mAnchorX;
140 private int mAnchorY;
141
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400142 // The scale factor that is used to determine the column width for text
143 private float mTextWrapScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400144
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400145 /*
146 * The default zoom scale is the scale factor used when the user triggers a
147 * zoom in by double tapping on the WebView. The value is initially set
148 * based on the display density, but can be changed at any time via the
149 * WebSettings.
150 */
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400151 private float mDefaultScale;
152 private float mInvDefaultScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400153
Derek Sollenberger03e48912010-05-18 17:03:42 -0400154 // the current computed zoom scale and its inverse.
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400155 private float mActualScale;
156 private float mInvActualScale;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400157
158 /*
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400159 * The initial scale for the WebView. 0 means default. If initial scale is
160 * greater than 0 the WebView starts with this value as its initial scale. The
161 * value is converted from an integer percentage so it is guarenteed to have
162 * no more than 2 significant digits after the decimal. This restriction
163 * allows us to convert the scale back to the original percentage by simply
164 * multiplying the value by 100.
165 */
166 private float mInitialScale;
167
Romain Guy4f066782011-01-18 15:41:44 -0800168 private static float MINIMUM_SCALE_INCREMENT = 0.007f;
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400169
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400170 /*
Huahui Wuacd944c2011-01-11 10:21:20 -0800171 * The touch points could be changed even the fingers stop moving.
172 * We use the following to filter out the zooming jitters.
173 */
Romain Guy4f066782011-01-18 15:41:44 -0800174 private static float MINIMUM_SCALE_WITHOUT_JITTER = 0.007f;
Huahui Wuacd944c2011-01-11 10:21:20 -0800175
176 /*
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400177 * The following member variables are only to be used for animating zoom. If
178 * mZoomScale is non-zero then we are in the middle of a zoom animation. The
179 * other variables are used as a cache (e.g. inverse) or as a way to store
180 * the state of the view prior to animating (e.g. initial scroll coords).
181 */
182 private float mZoomScale;
183 private float mInvInitialZoomScale;
184 private float mInvFinalZoomScale;
185 private int mInitialScrollX;
186 private int mInitialScrollY;
187 private long mZoomStart;
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400188
Shimeng (Simon) Wang47e57fd2011-03-03 15:29:27 -0800189 private static final int ZOOM_ANIMATION_LENGTH = 175;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400190
Derek Sollenberger293c3602010-06-04 10:44:48 -0400191 // whether support multi-touch
192 private boolean mSupportMultiTouch;
Adam Powellbe366682010-09-12 12:47:04 -0700193
194 /**
195 * True if we have a touch panel capable of detecting smooth pan/scale at the same time
196 */
197 private boolean mAllowPanAndScale;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400198
199 // use the framework's ScaleGestureDetector to handle multi-touch
200 private ScaleGestureDetector mScaleDetector;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400201 private boolean mPinchToZoomAnimating = false;
202
Ben Murdoch811ba6c2011-01-26 10:19:39 +0000203 private boolean mHardwareAccelerated = false;
204 private boolean mInHWAcceleratedZoom = false;
205
Derek Sollenberger03e48912010-05-18 17:03:42 -0400206 public ZoomManager(WebView webView, CallbackProxy callbackProxy) {
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400207 mWebView = webView;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400208 mCallbackProxy = callbackProxy;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400209
210 /*
211 * Ideally mZoomOverviewWidth should be mContentWidth. But sites like
212 * ESPN and Engadget always have wider mContentWidth no matter what the
213 * viewport size is.
214 */
215 setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH);
Huahui Wu463cc0c2011-03-07 10:22:18 -0800216
Huahui Wu31484fb2011-03-10 17:37:15 -0800217 mFocusMovementQueue = new FocusMovementQueue();
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400218 }
219
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400220 /**
221 * Initialize both the default and actual zoom scale to the given density.
222 *
223 * @param density The logical density of the display. This is a scaling factor
224 * for the Density Independent Pixel unit, where one DIP is one pixel on an
225 * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
226 */
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400227 public void init(float density) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400228 assert density > 0;
229
Derek Sollenberger03e48912010-05-18 17:03:42 -0400230 setDefaultZoomScale(density);
Derek Sollenberger03e48912010-05-18 17:03:42 -0400231 mActualScale = density;
232 mInvActualScale = 1 / density;
233 mTextWrapScale = density;
234 }
235
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400236 /**
237 * Update the default zoom scale using the given density. It will also reset
238 * the current min and max zoom scales to the default boundaries as well as
239 * ensure that the actual scale falls within those boundaries.
240 *
241 * @param density The logical density of the display. This is a scaling factor
242 * for the Density Independent Pixel unit, where one DIP is one pixel on an
243 * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
244 */
Derek Sollenberger03e48912010-05-18 17:03:42 -0400245 public void updateDefaultZoomDensity(float density) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400246 assert density > 0;
247
Derek Sollenberger03e48912010-05-18 17:03:42 -0400248 if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) {
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -0800249 // Remember the current zoom density before it gets changed.
250 final float originalDefault = mDefaultScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400251 // set the new default density
252 setDefaultZoomScale(density);
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -0800253 float scaleChange = (originalDefault > 0.0) ? density / originalDefault: 1.0f;
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400254 // adjust the scale if it falls outside the new zoom bounds
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -0800255 setZoomScale(mActualScale * scaleChange, true);
Derek Sollenberger03e48912010-05-18 17:03:42 -0400256 }
257 }
258
259 private void setDefaultZoomScale(float defaultScale) {
Shimeng (Simon) Wang66064912011-01-20 17:43:10 -0800260 final float originalDefault = mDefaultScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400261 mDefaultScale = defaultScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400262 mInvDefaultScale = 1 / defaultScale;
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400263 mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR;
264 mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR;
Shimeng (Simon) Wang66064912011-01-20 17:43:10 -0800265 if (originalDefault > 0.0 && mMaxZoomScale > 0.0) {
266 // Keeps max zoom scale when zoom density changes.
267 mMaxZoomScale = defaultScale / originalDefault * mMaxZoomScale;
268 } else {
269 mMaxZoomScale = mDefaultMaxZoomScale;
270 }
271 if (originalDefault > 0.0 && mMinZoomScale > 0.0) {
272 // Keeps min zoom scale when zoom density changes.
273 mMinZoomScale = defaultScale / originalDefault * mMinZoomScale;
274 } else {
275 mMinZoomScale = mDefaultMinZoomScale;
276 }
277 if (!exceedsMinScaleIncrement(mMinZoomScale, mMaxZoomScale)) {
278 mMaxZoomScale = mMinZoomScale;
279 }
Derek Sollenberger03e48912010-05-18 17:03:42 -0400280 }
281
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400282 public final float getScale() {
283 return mActualScale;
284 }
285
286 public final float getInvScale() {
287 return mInvActualScale;
288 }
289
290 public final float getTextWrapScale() {
291 return mTextWrapScale;
292 }
293
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400294 public final float getMaxZoomScale() {
295 return mMaxZoomScale;
296 }
297
298 public final float getMinZoomScale() {
299 return mMinZoomScale;
300 }
301
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400302 public final float getDefaultScale() {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400303 return mDefaultScale;
304 }
305
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700306 public final float getReadingLevelScale() {
Shimeng (Simon) Wang76064642011-02-22 15:31:02 -0800307 return computeScaleWithLimits(computeReadingLevelScale(getZoomOverviewScale()));
308 }
309
310 /* package */ final static float computeReadingLevelScale(float scale) {
311 // The reading scale is at least 0.5f apart from the input scale.
Shimeng (Simon) Wang938a1142010-09-21 13:42:07 -0700312 final float MIN_SCALE_DIFF = 0.5f;
Shimeng (Simon) Wang76064642011-02-22 15:31:02 -0800313 return Math.max(scale + MIN_SCALE_DIFF, DEFAULT_READING_LEVEL_SCALE);
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700314 }
315
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400316 public final float getInvDefaultScale() {
317 return mInvDefaultScale;
318 }
319
320 public final float getDefaultMaxZoomScale() {
321 return mDefaultMaxZoomScale;
322 }
323
324 public final float getDefaultMinZoomScale() {
325 return mDefaultMinZoomScale;
326 }
327
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400328 public final int getDocumentAnchorX() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400329 return mAnchorX;
330 }
331
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400332 public final int getDocumentAnchorY() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400333 return mAnchorY;
334 }
335
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400336 public final void clearDocumentAnchor() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400337 mAnchorX = mAnchorY = 0;
338 }
339
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400340 public final void setZoomCenter(float x, float y) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400341 mZoomCenterX = x;
342 mZoomCenterY = y;
343 }
344
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400345 public final void setInitialScaleInPercent(int scaleInPercent) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400346 mInitialScale = scaleInPercent * 0.01f;
347 }
348
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400349 public final float computeScaleWithLimits(float scale) {
Derek Sollenberger369aca22010-06-09 14:11:59 -0400350 if (scale < mMinZoomScale) {
351 scale = mMinZoomScale;
352 } else if (scale > mMaxZoomScale) {
353 scale = mMaxZoomScale;
354 }
355 return scale;
356 }
357
Huahui Wuff6f4c22011-03-09 10:52:42 -0800358 public final boolean isScaleOverLimits(float scale) {
359 return scale <= mMinZoomScale || scale >= mMaxZoomScale;
360 }
361
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400362 public final boolean isZoomScaleFixed() {
Derek Sollenberger369aca22010-06-09 14:11:59 -0400363 return mMinZoomScale >= mMaxZoomScale;
364 }
365
Derek Sollenberger03e48912010-05-18 17:03:42 -0400366 public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) {
367 return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT;
368 }
369
370 public boolean willScaleTriggerZoom(float scale) {
371 return exceedsMinScaleIncrement(scale, mActualScale);
372 }
373
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400374 public final boolean canZoomIn() {
Grace Kloba6164ef12010-06-01 15:59:13 -0700375 return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT;
376 }
377
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400378 public final boolean canZoomOut() {
Grace Kloba6164ef12010-06-01 15:59:13 -0700379 return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400380 }
381
Derek Sollenberger03e48912010-05-18 17:03:42 -0400382 public boolean zoomIn() {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400383 return zoom(1.25f);
384 }
385
386 public boolean zoomOut() {
387 return zoom(0.8f);
388 }
389
390 // returns TRUE if zoom out succeeds and FALSE if no zoom changes.
391 private boolean zoom(float zoomMultiplier) {
Shimeng (Simon) Wang459c4232011-02-17 15:02:34 -0800392 mInitialZoomOverview = false;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400393 // TODO: alternatively we can disallow this during draw history mode
394 mWebView.switchOutDrawHistory();
395 // Center zooming to the center of the screen.
396 mZoomCenterX = mWebView.getViewWidth() * .5f;
397 mZoomCenterY = mWebView.getViewHeight() * .5f;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400398 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
399 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700400 return startZoomAnimation(mActualScale * zoomMultiplier,
401 !mWebView.getSettings().getUseFixedViewport());
Derek Sollenberger03e48912010-05-18 17:03:42 -0400402 }
403
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400404 /**
405 * Initiates an animated zoom of the WebView.
406 *
407 * @return true if the new scale triggered an animation and false otherwise.
408 */
409 public boolean startZoomAnimation(float scale, boolean reflowText) {
Shimeng (Simon) Wang459c4232011-02-17 15:02:34 -0800410 mInitialZoomOverview = false;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400411 float oldScale = mActualScale;
412 mInitialScrollX = mWebView.getScrollX();
413 mInitialScrollY = mWebView.getScrollY();
414
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700415 // snap to reading level scale if it is close
416 if (!exceedsMinScaleIncrement(scale, getReadingLevelScale())) {
417 scale = getReadingLevelScale();
Derek Sollenberger03e48912010-05-18 17:03:42 -0400418 }
419
Ben Murdoch811ba6c2011-01-26 10:19:39 +0000420 if (mHardwareAccelerated) {
421 mInHWAcceleratedZoom = true;
422 }
423
Derek Sollenberger03e48912010-05-18 17:03:42 -0400424 setZoomScale(scale, reflowText);
425
426 if (oldScale != mActualScale) {
427 // use mZoomPickerScale to see zoom preview first
428 mZoomStart = SystemClock.uptimeMillis();
429 mInvInitialZoomScale = 1.0f / oldScale;
430 mInvFinalZoomScale = 1.0f / mActualScale;
431 mZoomScale = mActualScale;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400432 mWebView.onFixedLengthZoomAnimationStart();
Derek Sollenberger03e48912010-05-18 17:03:42 -0400433 mWebView.invalidate();
434 return true;
435 } else {
436 return false;
437 }
438 }
439
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400440 /**
Derek Sollenberger293c3602010-06-04 10:44:48 -0400441 * This method is called by the WebView's drawing code when a fixed length zoom
442 * animation is occurring. Its purpose is to animate the zooming of the canvas
443 * to the desired scale which was specified in startZoomAnimation(...).
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400444 *
Derek Sollenberger293c3602010-06-04 10:44:48 -0400445 * A fixed length animation begins when startZoomAnimation(...) is called and
446 * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that
447 * interval each time the WebView draws it calls this function which is
448 * responsible for generating the animation.
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400449 *
Derek Sollenberger293c3602010-06-04 10:44:48 -0400450 * Additionally, the WebView can check to see if such an animation is currently
451 * in progress by calling isFixedLengthAnimationInProgress().
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400452 */
Derek Sollenberger293c3602010-06-04 10:44:48 -0400453 public void animateZoom(Canvas canvas) {
Shimeng (Simon) Wang459c4232011-02-17 15:02:34 -0800454 mInitialZoomOverview = false;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400455 if (mZoomScale == 0) {
Derek Sollenberger293c3602010-06-04 10:44:48 -0400456 Log.w(LOGTAG, "A WebView is attempting to perform a fixed length "
457 + "zoom animation when no zoom is in progress");
458 return;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400459 }
460
461 float zoomScale;
462 int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
463 if (interval < ZOOM_ANIMATION_LENGTH) {
464 float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
465 zoomScale = 1.0f / (mInvInitialZoomScale
466 + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
Derek Sollenberger293c3602010-06-04 10:44:48 -0400467 mWebView.invalidate();
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400468 } else {
469 zoomScale = mZoomScale;
470 // set mZoomScale to be 0 as we have finished animating
471 mZoomScale = 0;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400472 mWebView.onFixedLengthZoomAnimationEnd();
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400473 }
474 // calculate the intermediate scroll position. Since we need to use
475 // zoomScale, we can't use the WebView's pinLocX/Y functions directly.
476 float scale = zoomScale * mInvInitialZoomScale;
477 int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX);
478 tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth()
479 * zoomScale)) + mWebView.getScrollX();
480 int titleHeight = mWebView.getTitleHeight();
481 int ty = Math.round(scale
482 * (mInitialScrollY + mZoomCenterY - titleHeight)
483 - (mZoomCenterY - titleHeight));
484 ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty
485 - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight()
486 * zoomScale)) + titleHeight) + mWebView.getScrollY();
487
Ben Murdoch811ba6c2011-01-26 10:19:39 +0000488 if (mHardwareAccelerated) {
489 mWebView.updateScrollCoordinates(mWebView.getScrollX() - tx, mWebView.getScrollY() - ty);
490 setZoomScale(zoomScale, false);
491
492 if (mZoomScale == 0) {
493 // We've reached the end of the zoom animation.
494 mInHWAcceleratedZoom = false;
495 }
496 } else {
497 canvas.translate(tx, ty);
498 canvas.scale(zoomScale, zoomScale);
499 }
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400500 }
501
502 public boolean isZoomAnimating() {
Derek Sollenberger293c3602010-06-04 10:44:48 -0400503 return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating;
504 }
505
506 public boolean isFixedLengthAnimationInProgress() {
Derek Sollenbergera249a932011-03-18 11:28:54 -0400507 return mZoomScale != 0 || mInHWAcceleratedZoom;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400508 }
509
Derek Sollenberger03e48912010-05-18 17:03:42 -0400510 public void refreshZoomScale(boolean reflowText) {
511 setZoomScale(mActualScale, reflowText, true);
512 }
513
514 public void setZoomScale(float scale, boolean reflowText) {
515 setZoomScale(scale, reflowText, false);
516 }
517
518 private void setZoomScale(float scale, boolean reflowText, boolean force) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400519 final boolean isScaleLessThanMinZoom = scale < mMinZoomScale;
520 scale = computeScaleWithLimits(scale);
521
522 // determine whether or not we are in the zoom overview mode
523 if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) {
524 mInZoomOverview = true;
525 } else {
526 mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale());
Derek Sollenberger03e48912010-05-18 17:03:42 -0400527 }
528
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700529 if (reflowText && !mWebView.getSettings().getUseFixedViewport()) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400530 mTextWrapScale = scale;
531 }
532
Shimeng (Simon) Wang3911b452011-03-09 17:54:41 -0800533 if (scale != mActualScale || force) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400534 float oldScale = mActualScale;
535 float oldInvScale = mInvActualScale;
536
Derek Sollenberger293c3602010-06-04 10:44:48 -0400537 if (scale != mActualScale && !mPinchToZoomAnimating) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400538 mCallbackProxy.onScaleChanged(mActualScale, scale);
539 }
540
541 mActualScale = scale;
542 mInvActualScale = 1 / scale;
543
Ben Murdoch811ba6c2011-01-26 10:19:39 +0000544 if (!mWebView.drawHistory() && !mInHWAcceleratedZoom) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400545
546 // If history Picture is drawn, don't update scroll. They will
547 // be updated when we get out of that mode.
548 // update our scroll so we don't appear to jump
549 // i.e. keep the center of the doc in the center of the view
Ben Murdoch811ba6c2011-01-26 10:19:39 +0000550 // If this is part of a zoom on a HW accelerated canvas, we
551 // have already updated the scroll so don't do it again.
Derek Sollenberger03e48912010-05-18 17:03:42 -0400552 int oldX = mWebView.getScrollX();
553 int oldY = mWebView.getScrollY();
554 float ratio = scale * oldInvScale;
555 float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
556 float sy = ratio * oldY + (ratio - 1)
557 * (mZoomCenterY - mWebView.getTitleHeight());
558
559 // Scale all the child views
560 mWebView.mViewManager.scaleAll();
561
562 // as we don't have animation for scaling, don't do animation
563 // for scrolling, as it causes weird intermediate state
564 int scrollX = mWebView.pinLocX(Math.round(sx));
565 int scrollY = mWebView.pinLocY(Math.round(sy));
566 if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) {
567 // the scroll position is adjusted at the beginning of the
568 // zoom animation. But we want to update the WebKit at the
569 // end of the zoom animation. See comments in onScaleEnd().
570 mWebView.sendOurVisibleRect();
571 }
572 }
573
574 // if the we need to reflow the text then force the VIEW_SIZE_CHANGED
575 // event to be sent to WebKit
576 mWebView.sendViewSizeZoom(reflowText);
577 }
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400578 }
579
Derek Sollenberger515f6d82010-11-12 15:12:58 -0500580 public boolean isDoubleTapEnabled() {
581 WebSettings settings = mWebView.getSettings();
582 return settings != null && settings.getUseWideViewPort();
583 }
584
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400585 /**
586 * The double tap gesture can result in different behaviors depending on the
587 * content that is tapped.
588 *
589 * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on
590 * the screen. If the plugin is already maximized then zoom the user into
591 * overview mode.
592 *
593 * (2) HTML/OTHER: If the taps occur outside a plugin then the following
594 * heuristic is used.
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700595 * A. If the current text wrap scale differs from newly calculated and the
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400596 * layout algorithm specifies the use of NARROW_COLUMNS, then fit to
597 * column by reflowing the text.
598 * B. If the page is not in overview mode then change to overview mode.
599 * C. If the page is in overmode then change to the default scale.
600 */
601 public void handleDoubleTap(float lastTouchX, float lastTouchY) {
Shimeng (Simon) Wang459c4232011-02-17 15:02:34 -0800602 // User takes action, set initial zoom overview to false.
603 mInitialZoomOverview = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400604 WebSettings settings = mWebView.getSettings();
Derek Sollenberger515f6d82010-11-12 15:12:58 -0500605 if (!isDoubleTapEnabled()) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400606 return;
607 }
608
609 setZoomCenter(lastTouchX, lastTouchY);
610 mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX());
611 mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY());
612 settings.setDoubleTapToastCount(0);
613
614 // remove the zoom control after double tap
615 dismissZoomPicker();
616
617 /*
618 * If the double tap was on a plugin then either zoom to maximize the
619 * plugin on the screen or scale to overview mode.
620 */
Derek Sollenbergerdd1173b2011-01-18 11:11:28 -0500621 Rect pluginBounds = mWebView.getPluginBounds(mAnchorX, mAnchorY);
622 if (pluginBounds != null) {
623 if (mWebView.isRectFitOnScreen(pluginBounds)) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400624 zoomToOverview();
625 } else {
Derek Sollenbergerdd1173b2011-01-18 11:11:28 -0500626 mWebView.centerFitRect(pluginBounds);
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400627 }
628 return;
629 }
630
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700631 final float newTextWrapScale;
632 if (settings.getUseFixedViewport()) {
633 newTextWrapScale = Math.max(mActualScale, getReadingLevelScale());
634 } else {
635 newTextWrapScale = mActualScale;
636 }
637 if (settings.isNarrowColumnLayout()
638 && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale)) {
639 mTextWrapScale = newTextWrapScale;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400640 refreshZoomScale(true);
Shimeng (Simon) Wangdbf5ff22010-11-09 15:37:31 -0800641 } else if (!mInZoomOverview && willScaleTriggerZoom(getZoomOverviewScale())) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400642 zoomToOverview();
643 } else {
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700644 zoomToReadingLevel();
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400645 }
646 }
647
648 private void setZoomOverviewWidth(int width) {
Shimeng (Simon) Wang27c31382010-11-03 14:13:13 -0700649 if (width == 0) {
650 mZoomOverviewWidth = WebView.DEFAULT_VIEWPORT_WIDTH;
651 } else {
652 mZoomOverviewWidth = width;
653 }
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400654 mInvZoomOverviewWidth = 1.0f / width;
655 }
656
Shimeng (Simon) Wang88ec6442010-12-06 15:35:05 -0800657 /* package */ float getZoomOverviewScale() {
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -0800658 return mWebView.getViewWidth() * mInvZoomOverviewWidth;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400659 }
660
661 public boolean isInZoomOverview() {
662 return mInZoomOverview;
663 }
664
665 private void zoomToOverview() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400666 // Force the titlebar fully reveal in overview mode
667 int scrollY = mWebView.getScrollY();
668 if (scrollY < mWebView.getTitleHeight()) {
669 mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0);
670 }
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700671 startZoomAnimation(getZoomOverviewScale(),
672 !mWebView.getSettings().getUseFixedViewport());
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400673 }
674
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700675 private void zoomToReadingLevel() {
676 final float readingScale = getReadingLevelScale();
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400677 int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
678 if (left != WebView.NO_LEFTEDGE) {
679 // add a 5pt padding to the left edge.
680 int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5))
681 - mWebView.getScrollX();
682 // Re-calculate the zoom center so that the new scroll x will be
683 // on the left edge.
684 if (viewLeft > 0) {
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700685 mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale);
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400686 } else {
687 mWebView.scrollBy(viewLeft, 0);
688 mZoomCenterX = 0;
689 }
690 }
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700691 startZoomAnimation(readingScale,
692 !mWebView.getSettings().getUseFixedViewport());
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400693 }
694
Derek Sollenberger293c3602010-06-04 10:44:48 -0400695 public void updateMultiTouchSupport(Context context) {
696 // check the preconditions
697 assert mWebView.getSettings() != null;
698
Adam Powellbe366682010-09-12 12:47:04 -0700699 final WebSettings settings = mWebView.getSettings();
700 final PackageManager pm = context.getPackageManager();
701 mSupportMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
Derek Sollenberger293c3602010-06-04 10:44:48 -0400702 && settings.supportZoom() && settings.getBuiltInZoomControls();
Adam Powellbe366682010-09-12 12:47:04 -0700703 mAllowPanAndScale = pm.hasSystemFeature(
704 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
Derek Sollenberger293c3602010-06-04 10:44:48 -0400705 if (mSupportMultiTouch && (mScaleDetector == null)) {
706 mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener());
707 } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
708 mScaleDetector = null;
709 }
710 }
711
712 public boolean supportsMultiTouchZoom() {
713 return mSupportMultiTouch;
714 }
715
Adam Powellbe366682010-09-12 12:47:04 -0700716 public boolean supportsPanDuringZoom() {
717 return mAllowPanAndScale;
718 }
719
Derek Sollenberger293c3602010-06-04 10:44:48 -0400720 /**
721 * Notifies the caller that the ZoomManager is requesting that scale related
722 * updates should not be sent to webkit. This can occur in cases where the
723 * ZoomManager is performing an animation and does not want webkit to update
724 * until the animation is complete.
725 *
726 * @return true if scale related updates should not be sent to webkit and
727 * false otherwise.
728 */
729 public boolean isPreventingWebkitUpdates() {
Derek Sollenbergerac382cf2011-03-01 15:50:27 -0500730 // currently only animating a multi-touch zoom and fixed length
731 // animations prevent updates, but others can add their own conditions
732 // to this method if necessary.
733 return isZoomAnimating();
Derek Sollenberger293c3602010-06-04 10:44:48 -0400734 }
735
736 public ScaleGestureDetector getMultiTouchGestureDetector() {
737 return mScaleDetector;
738 }
739
Huahui Wu31484fb2011-03-10 17:37:15 -0800740 private class FocusMovementQueue {
741 private static final int QUEUE_CAPACITY = 5;
742 private float[] mQueue;
743 private float mSum;
744 private int mSize;
745 private int mIndex;
746
747 FocusMovementQueue() {
748 mQueue = new float[QUEUE_CAPACITY];
749 mSize = 0;
750 mSum = 0;
751 mIndex = 0;
752 }
753
754 private void clear() {
755 mSize = 0;
756 mSum = 0;
757 mIndex = 0;
758 for (int i = 0; i < QUEUE_CAPACITY; ++i) {
759 mQueue[i] = 0;
760 }
761 }
762
763 private void add(float focusDelta) {
764 mSum += focusDelta;
765 if (mSize < QUEUE_CAPACITY) { // fill up the queue.
766 mSize++;
767 } else { // circulate the queue.
768 mSum -= mQueue[mIndex];
769 }
770 mQueue[mIndex] = focusDelta;
771 mIndex = (mIndex + 1) % QUEUE_CAPACITY;
772 }
773
774 private float getSum() {
775 return mSum;
776 }
777 }
778
Derek Sollenberger293c3602010-06-04 10:44:48 -0400779 private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener {
Adam Powellc37308f2011-03-11 11:44:08 -0800780 private float mAccumulatedSpan;
781
Derek Sollenberger293c3602010-06-04 10:44:48 -0400782 public boolean onScaleBegin(ScaleGestureDetector detector) {
Shimeng (Simon) Wang459c4232011-02-17 15:02:34 -0800783 mInitialZoomOverview = false;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400784 dismissZoomPicker();
Huahui Wu463cc0c2011-03-07 10:22:18 -0800785 mFocusMovementQueue.clear();
Derek Sollenberger293c3602010-06-04 10:44:48 -0400786 mWebView.mViewManager.startZoom();
787 mWebView.onPinchToZoomAnimationStart();
Adam Powellc37308f2011-03-11 11:44:08 -0800788 mAccumulatedSpan = 0;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400789 return true;
790 }
791
Huahui Wuff6f4c22011-03-09 10:52:42 -0800792 // If the user moves the fingers but keeps the same distance between them,
793 // we should do panning only.
794 public boolean isPanningOnly(ScaleGestureDetector detector) {
Huahui Wu463cc0c2011-03-07 10:22:18 -0800795 float prevFocusX = mFocusX;
796 float prevFocusY = mFocusY;
797 mFocusX = detector.getFocusX();
798 mFocusY = detector.getFocusY();
799 float focusDelta = (prevFocusX == 0 && prevFocusY == 0) ? 0 :
800 FloatMath.sqrt((mFocusX - prevFocusX) * (mFocusX - prevFocusX)
801 + (mFocusY - prevFocusY) * (mFocusY - prevFocusY));
Huahui Wu463cc0c2011-03-07 10:22:18 -0800802 mFocusMovementQueue.add(focusDelta);
Adam Powellc37308f2011-03-11 11:44:08 -0800803 float deltaSpan = detector.getCurrentSpan() - detector.getPreviousSpan() +
804 mAccumulatedSpan;
805 final boolean result = mFocusMovementQueue.getSum() > Math.abs(deltaSpan);
806 if (result) {
807 mAccumulatedSpan += deltaSpan;
808 } else {
809 mAccumulatedSpan = 0;
810 }
811 return result;
Huahui Wuff6f4c22011-03-09 10:52:42 -0800812 }
Huahui Wu463cc0c2011-03-07 10:22:18 -0800813
Huahui Wuff6f4c22011-03-09 10:52:42 -0800814 public boolean handleScale(ScaleGestureDetector detector) {
815 float scale = detector.getScaleFactor() * mActualScale;
816
817 // if scale is limited by any reason, don't zoom but do ask
818 // the detector to update the event.
819 boolean isScaleLimited =
820 isScaleOverLimits(scale) || scale < getZoomOverviewScale();
821
822 // Prevent scaling beyond overview scale.
823 scale = Math.max(computeScaleWithLimits(scale), getZoomOverviewScale());
Huahui Wu463cc0c2011-03-07 10:22:18 -0800824
Adam Powell63814df2010-12-11 16:49:05 -0800825 if (mPinchToZoomAnimating || willScaleTriggerZoom(scale)) {
Derek Sollenberger293c3602010-06-04 10:44:48 -0400826 mPinchToZoomAnimating = true;
827 // limit the scale change per step
828 if (scale > mActualScale) {
829 scale = Math.min(scale, mActualScale * 1.25f);
830 } else {
831 scale = Math.max(scale, mActualScale * 0.8f);
832 }
Shimeng (Simon) Wang61bb9112011-02-24 15:30:15 -0800833 scale = computeScaleWithLimits(scale);
Huahui Wuacd944c2011-01-11 10:21:20 -0800834 // if the scale change is too small, regard it as jitter and skip it.
835 if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_WITHOUT_JITTER) {
Huahui Wuff6f4c22011-03-09 10:52:42 -0800836 return isScaleLimited;
Huahui Wuacd944c2011-01-11 10:21:20 -0800837 }
Derek Sollenberger293c3602010-06-04 10:44:48 -0400838 setZoomCenter(detector.getFocusX(), detector.getFocusY());
839 setZoomScale(scale, false);
840 mWebView.invalidate();
841 return true;
842 }
Huahui Wuff6f4c22011-03-09 10:52:42 -0800843 return isScaleLimited;
844 }
845
846 public boolean onScale(ScaleGestureDetector detector) {
847 if (isPanningOnly(detector) || handleScale(detector)) {
Huahui Wuff6f4c22011-03-09 10:52:42 -0800848 mFocusMovementQueue.clear();
849 return true;
850 }
Derek Sollenberger293c3602010-06-04 10:44:48 -0400851 return false;
852 }
853
854 public void onScaleEnd(ScaleGestureDetector detector) {
855 if (mPinchToZoomAnimating) {
856 mPinchToZoomAnimating = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400857 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
858 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
Derek Sollenberger293c3602010-06-04 10:44:48 -0400859 // don't reflow when zoom in; when zoom out, do reflow if the
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700860 // new scale is almost minimum scale.
Derek Sollenberger293c3602010-06-04 10:44:48 -0400861 boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale);
862 // force zoom after mPreviewZoomOnly is set to false so that the
863 // new view size will be passed to the WebKit
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700864 refreshZoomScale(reflowNow &&
865 !mWebView.getSettings().getUseFixedViewport());
Derek Sollenberger293c3602010-06-04 10:44:48 -0400866 // call invalidate() to draw without zoom filter
867 mWebView.invalidate();
868 }
869
870 mWebView.mViewManager.endZoom();
871 mWebView.onPinchToZoomAnimationEnd(detector);
872 }
873 }
874
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400875 public void onSizeChanged(int w, int h, int ow, int oh) {
876 // reset zoom and anchor to the top left corner of the screen
877 // unless we are already zooming
Derek Sollenberger293c3602010-06-04 10:44:48 -0400878 if (!isFixedLengthAnimationInProgress()) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400879 int visibleTitleHeight = mWebView.getVisibleTitleHeight();
880 mZoomCenterX = 0;
881 mZoomCenterY = visibleTitleHeight;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400882 mAnchorX = mWebView.viewToContentX(mWebView.getScrollX());
883 mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY());
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400884 }
885
886 // update mMinZoomScale if the minimum zoom scale is not fixed
887 if (!mMinZoomScaleFixed) {
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -0800888 // when change from narrow screen to wide screen, the new viewWidth
889 // can be wider than the old content width. We limit the minimum
890 // scale to 1.0f. The proper minimum scale will be calculated when
891 // the new picture shows up.
892 mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth()
893 / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth()
894 : mZoomOverviewWidth));
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400895 // limit the minZoomScale to the initialScale if it is set
896 if (mInitialScale > 0 && mInitialScale < mMinZoomScale) {
897 mMinZoomScale = mInitialScale;
898 }
899 }
900
901 dismissZoomPicker();
902
903 // onSizeChanged() is called during WebView layout. And any
904 // requestLayout() is blocked during layout. As refreshZoomScale() will
905 // cause its child View to reposition itself through ViewManager's
906 // scaleAll(), we need to post a Runnable to ensure requestLayout().
907 // Additionally, only update the text wrap scale if the width changed.
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -0700908 mWebView.post(new PostScale(w != ow &&
Shimeng (Simon) Wang96b065f2011-02-16 10:31:16 -0800909 !mWebView.getSettings().getUseFixedViewport(), mInZoomOverview));
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400910 }
911
912 private class PostScale implements Runnable {
913 final boolean mUpdateTextWrap;
Shimeng (Simon) Wang96b065f2011-02-16 10:31:16 -0800914 // Remember the zoom overview state right after rotation since
915 // it could be changed between the time this callback is initiated and
916 // the time it's actually run.
917 final boolean mInZoomOverviewBeforeSizeChange;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400918
Shimeng (Simon) Wang96b065f2011-02-16 10:31:16 -0800919 public PostScale(boolean updateTextWrap, boolean inZoomOverview) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400920 mUpdateTextWrap = updateTextWrap;
Shimeng (Simon) Wang96b065f2011-02-16 10:31:16 -0800921 mInZoomOverviewBeforeSizeChange = inZoomOverview;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400922 }
923
924 public void run() {
925 if (mWebView.getWebViewCore() != null) {
926 // we always force, in case our height changed, in which case we
927 // still want to send the notification over to webkit.
Shimeng (Simon) Wanga5b3d802011-02-15 11:14:57 -0800928 // Keep overview mode unchanged when rotating.
929 final float zoomOverviewScale = getZoomOverviewScale();
Shimeng (Simon) Wang96b065f2011-02-16 10:31:16 -0800930 final float newScale = (mInZoomOverviewBeforeSizeChange) ?
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -0800931 zoomOverviewScale : Math.max(mActualScale, zoomOverviewScale);
Shimeng (Simon) Wanga5b3d802011-02-15 11:14:57 -0800932 setZoomScale(newScale, mUpdateTextWrap, true);
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400933 // update the zoom buttons as the scale can be changed
934 updateZoomPicker();
935 }
936 }
937 }
938
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400939 public void updateZoomRange(WebViewCore.ViewState viewState,
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400940 int viewWidth, int minPrefWidth) {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400941 if (viewState.mMinScale == 0) {
942 if (viewState.mMobileSite) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400943 if (minPrefWidth > Math.max(0, viewWidth)) {
944 mMinZoomScale = (float) viewWidth / minPrefWidth;
945 mMinZoomScaleFixed = false;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400946 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400947 mMinZoomScale = viewState.mDefaultScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400948 mMinZoomScaleFixed = true;
949 }
950 } else {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400951 mMinZoomScale = mDefaultMinZoomScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400952 mMinZoomScaleFixed = false;
953 }
954 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400955 mMinZoomScale = viewState.mMinScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400956 mMinZoomScaleFixed = true;
957 }
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400958 if (viewState.mMaxScale == 0) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400959 mMaxZoomScale = mDefaultMaxZoomScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400960 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400961 mMaxZoomScale = viewState.mMaxScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400962 }
963 }
964
965 /**
966 * Updates zoom values when Webkit produces a new picture. This method
967 * should only be called from the UI thread's message handler.
968 */
969 public void onNewPicture(WebViewCore.DrawData drawData) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400970 final int viewWidth = mWebView.getViewWidth();
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -0800971 final boolean zoomOverviewWidthChanged = setupZoomOverviewWidth(drawData, viewWidth);
Shimeng (Simon) Wanga7699fe2011-02-14 16:44:08 -0800972 final float newZoomOverviewScale = getZoomOverviewScale();
Shimeng (Simon) Wangdbf5ff22010-11-09 15:37:31 -0800973 WebSettings settings = mWebView.getSettings();
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -0800974 if (zoomOverviewWidthChanged && settings.isNarrowColumnLayout() &&
975 settings.getUseFixedViewport() &&
976 (mInitialZoomOverview || mInZoomOverview)) {
Shimeng (Simon) Wanga7699fe2011-02-14 16:44:08 -0800977 // Keep mobile site's text wrap scale unchanged. For mobile sites,
978 // the text wrap scale is the same as zoom overview scale, which is 1.0f.
979 if (exceedsMinScaleIncrement(mTextWrapScale, 1.0f) ||
980 exceedsMinScaleIncrement(newZoomOverviewScale, 1.0f)) {
981 mTextWrapScale = getReadingLevelScale();
982 } else {
983 mTextWrapScale = newZoomOverviewScale;
984 }
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400985 }
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400986
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -0800987 if (!mMinZoomScaleFixed) {
988 mMinZoomScale = newZoomOverviewScale;
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -0800989 mMaxZoomScale = Math.max(mMaxZoomScale, mMinZoomScale);
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -0800990 }
Shimeng (Simon) Wang7c6aaf02011-02-15 17:32:27 -0800991 // fit the content width to the current view for the first new picture
992 // after first layout.
993 boolean scaleHasDiff = exceedsMinScaleIncrement(newZoomOverviewScale, mActualScale);
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -0800994 // Make sure the actual scale is no less than zoom overview scale.
995 boolean scaleLessThanOverview =
996 (newZoomOverviewScale - mActualScale) >= MINIMUM_SCALE_INCREMENT;
Shimeng (Simon) Wangd75fd492011-02-16 14:52:13 -0800997 // Make sure mobile sites are correctly handled since mobile site will
998 // change content width after rotating.
999 boolean mobileSiteInOverview = mInZoomOverview &&
1000 !exceedsMinScaleIncrement(newZoomOverviewScale, 1.0f);
1001 if (!mWebView.drawHistory() &&
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -08001002 (mInitialZoomOverview || scaleLessThanOverview || mobileSiteInOverview) &&
Shimeng (Simon) Wanga05fa962011-02-18 09:31:59 -08001003 scaleHasDiff && zoomOverviewWidthChanged) {
Derek Sollenberger94345f42010-06-25 09:42:53 -04001004 mInitialZoomOverview = false;
Shimeng (Simon) Wanga7699fe2011-02-14 16:44:08 -08001005 setZoomScale(newZoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale) &&
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -07001006 !mWebView.getSettings().getUseFixedViewport());
Shimeng (Simon) Wangd75fd492011-02-16 14:52:13 -08001007 } else {
1008 mInZoomOverview = !scaleHasDiff;
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001009 }
1010 }
1011
1012 /**
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -08001013 * Set up correct zoom overview width based on different settings.
1014 *
1015 * @param drawData webviewcore draw data
1016 * @param viewWidth current view width
1017 */
1018 private boolean setupZoomOverviewWidth(WebViewCore.DrawData drawData, final int viewWidth) {
1019 WebSettings settings = mWebView.getSettings();
1020 int newZoomOverviewWidth = mZoomOverviewWidth;
1021 if (settings.getUseWideViewPort()) {
1022 if (!settings.getUseFixedViewport()) {
1023 // limit mZoomOverviewWidth upper bound to
1024 // sMaxViewportWidth so that if the page doesn't behave
1025 // well, the WebView won't go insane. limit the lower
1026 // bound to match the default scale for mobile sites.
1027 newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth,
1028 Math.max((int) (viewWidth * mInvDefaultScale),
1029 Math.max(drawData.mMinPrefWidth, drawData.mViewSize.x)));
1030 } else if (drawData.mContentSize.x > 0) {
1031 // The webkitDraw for layers will not populate contentSize, and it'll be
1032 // ignored for zoom overview width update.
1033 final int contentWidth = Math.max(drawData.mContentSize.x, drawData.mMinPrefWidth);
1034 newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth, contentWidth);
1035 }
1036 } else {
1037 // If not use wide viewport, use view width as the zoom overview width.
1038 newZoomOverviewWidth = viewWidth;
1039 }
1040 if (newZoomOverviewWidth != mZoomOverviewWidth) {
1041 setZoomOverviewWidth(newZoomOverviewWidth);
1042 return true;
1043 }
1044 return false;
1045 }
1046
1047 /**
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001048 * Updates zoom values after Webkit completes the initial page layout. It
1049 * is called when visiting a page for the first time as well as when the
1050 * user navigates back to a page (in which case we may need to restore the
1051 * zoom levels to the state they were when you left the page). This method
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001052 * should only be called from the UI thread's message handler.
1053 */
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001054 public void onFirstLayout(WebViewCore.DrawData drawData) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001055 // precondition check
1056 assert drawData != null;
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001057 assert drawData.mViewState != null;
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001058 assert mWebView.getSettings() != null;
1059
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001060 WebViewCore.ViewState viewState = drawData.mViewState;
Derek Sollenberger7e6bf6f2010-10-28 09:33:47 -04001061 final Point viewSize = drawData.mViewSize;
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001062 updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth);
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -08001063 setupZoomOverviewWidth(drawData, mWebView.getViewWidth());
Shimeng (Simon) Wang633c37752011-03-08 14:30:38 -08001064 final float overviewScale = getZoomOverviewScale();
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -08001065 if (!mMinZoomScaleFixed) {
Shimeng (Simon) Wang633c37752011-03-08 14:30:38 -08001066 mMinZoomScale = (mInitialScale > 0) ?
1067 Math.min(mInitialScale, overviewScale) : overviewScale;
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -08001068 mMaxZoomScale = Math.max(mMaxZoomScale, mMinZoomScale);
1069 }
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001070
1071 if (!mWebView.drawHistory()) {
Shimeng (Simon) Wang767dbef2010-10-27 13:37:08 -07001072 float scale;
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -08001073 WebSettings settings = mWebView.getSettings();
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001074
1075 if (mInitialScale > 0) {
1076 scale = mInitialScale;
Shimeng (Simon) Wang8db525a2010-10-21 14:20:50 -07001077 } else if (viewState.mViewScale > 0) {
Derek Sollenbergerb983c892010-06-28 08:38:28 -04001078 mTextWrapScale = viewState.mTextWrapScale;
1079 scale = viewState.mViewScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001080 } else {
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -08001081 scale = overviewScale;
1082 if (!settings.getUseWideViewPort()
1083 || !settings.getLoadWithOverviewMode()) {
Shimeng (Simon) Wang9cedbcf2011-03-02 14:21:14 -08001084 scale = Math.max(mDefaultScale, scale);
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001085 }
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -08001086 if (settings.isNarrowColumnLayout() &&
1087 settings.getUseFixedViewport()) {
Shimeng (Simon) Wangc55886a2010-10-28 13:46:02 -07001088 // When first layout, reflow using the reading level scale to avoid
1089 // reflow when double tapped.
1090 mTextWrapScale = getReadingLevelScale();
1091 }
Shimeng (Simon) Wang96fcb872010-11-24 16:01:12 -08001092 }
1093 boolean reflowText = false;
1094 if (!viewState.mIsRestored) {
Shimeng (Simon) Wang2c4ab3a2011-03-02 11:46:21 -08001095 if (settings.getUseFixedViewport()) {
Shimeng (Simon) Wang2c782e32011-01-04 13:32:06 -08001096 // Override the scale only in case of fixed viewport.
1097 scale = Math.max(scale, overviewScale);
1098 mTextWrapScale = Math.max(mTextWrapScale, overviewScale);
1099 }
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001100 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
1101 }
Shimeng (Simon) Wang96fcb872010-11-24 16:01:12 -08001102 mInitialZoomOverview = !exceedsMinScaleIncrement(scale, overviewScale);
Derek Sollenberger341e22f2010-06-02 12:34:34 -04001103 setZoomScale(scale, reflowText);
1104
1105 // update the zoom buttons as the scale can be changed
1106 updateZoomPicker();
1107 }
1108 }
1109
Derek Sollenbergerbffa8512010-06-10 14:24:03 -04001110 public void saveZoomState(Bundle b) {
1111 b.putFloat("scale", mActualScale);
1112 b.putFloat("textwrapScale", mTextWrapScale);
1113 b.putBoolean("overview", mInZoomOverview);
1114 }
1115
1116 public void restoreZoomState(Bundle b) {
1117 // as getWidth() / getHeight() of the view are not available yet, set up
1118 // mActualScale, so that when onSizeChanged() is called, the rest will
1119 // be set correctly
1120 mActualScale = b.getFloat("scale", 1.0f);
1121 mInvActualScale = 1 / mActualScale;
1122 mTextWrapScale = b.getFloat("textwrapScale", mActualScale);
1123 mInZoomOverview = b.getBoolean("overview");
1124 }
1125
Derek Sollenberger90b6e482010-05-10 12:38:54 -04001126 private ZoomControlBase getCurrentZoomControl() {
1127 if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) {
1128 if (mWebView.getSettings().getBuiltInZoomControls()) {
Michael Kolb6fe3b422010-08-19 12:41:24 -07001129 if ((mEmbeddedZoomControl == null)
1130 && mWebView.getSettings().getDisplayZoomControls()) {
Derek Sollenberger90b6e482010-05-10 12:38:54 -04001131 mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView);
1132 }
1133 return mEmbeddedZoomControl;
1134 } else {
1135 if (mExternalZoomControl == null) {
1136 mExternalZoomControl = new ZoomControlExternal(mWebView);
1137 }
1138 return mExternalZoomControl;
1139 }
1140 }
1141 return null;
1142 }
1143
1144 public void invokeZoomPicker() {
1145 ZoomControlBase control = getCurrentZoomControl();
1146 if (control != null) {
1147 control.show();
1148 }
1149 }
1150
1151 public void dismissZoomPicker() {
1152 ZoomControlBase control = getCurrentZoomControl();
1153 if (control != null) {
1154 control.hide();
1155 }
1156 }
1157
1158 public boolean isZoomPickerVisible() {
1159 ZoomControlBase control = getCurrentZoomControl();
1160 return (control != null) ? control.isVisible() : false;
1161 }
1162
1163 public void updateZoomPicker() {
1164 ZoomControlBase control = getCurrentZoomControl();
1165 if (control != null) {
1166 control.update();
1167 }
1168 }
1169
1170 /**
1171 * The embedded zoom control intercepts touch events and automatically stays
1172 * visible. The external control needs to constantly refresh its internal
1173 * timer to stay visible.
1174 */
1175 public void keepZoomPickerVisible() {
1176 ZoomControlBase control = getCurrentZoomControl();
1177 if (control != null && control == mExternalZoomControl) {
1178 control.show();
1179 }
1180 }
1181
1182 public View getExternalZoomPicker() {
1183 ZoomControlBase control = getCurrentZoomControl();
1184 if (control != null && control == mExternalZoomControl) {
1185 return mExternalZoomControl.getControls();
1186 } else {
1187 return null;
1188 }
1189 }
Ben Murdoch811ba6c2011-01-26 10:19:39 +00001190
1191 public void setHardwareAccelerated() {
1192 mHardwareAccelerated = true;
1193 }
Shimeng (Simon) Wangd75fd492011-02-16 14:52:13 -08001194
1195 /**
1196 * OnPageFinished called by webview when a page is fully loaded.
1197 */
1198 /* package*/ void onPageFinished(String url) {
1199 // Turn off initial zoom overview flag when a page is fully loaded.
1200 mInitialZoomOverview = false;
1201 }
Derek Sollenberger90b6e482010-05-10 12:38:54 -04001202}