blob: 8033c9c293d3b5bd6176c4352f50c9357bf9f409 [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 Sollenbergerbffa8512010-06-10 14:24:03 -040023import android.os.Bundle;
Derek Sollenberger03e48912010-05-18 17:03:42 -040024import android.os.SystemClock;
Derek Sollenberger87b17be52010-06-01 11:49:31 -040025import android.util.Log;
Derek Sollenberger293c3602010-06-04 10:44:48 -040026import android.view.ScaleGestureDetector;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040027import android.view.View;
28
Derek Sollenberger293c3602010-06-04 10:44:48 -040029/**
30 * The ZoomManager is responsible for maintaining the WebView's current zoom
31 * level state. It is also responsible for managing the on-screen zoom controls
32 * as well as any animation of the WebView due to zooming.
33 *
34 * Currently, there are two methods for animating the zoom of a WebView.
35 *
36 * (1) The first method is triggered by startZoomAnimation(...) and is a fixed
37 * length animation where the final zoom scale is known at startup. This type of
38 * animation notifies webkit of the final scale BEFORE it animates. The animation
39 * is then done by scaling the CANVAS incrementally based on a stepping function.
40 *
41 * (2) The second method is triggered by a multi-touch pinch and the new scale
42 * is determined dynamically based on the user's gesture. This type of animation
43 * only notifies webkit of new scale AFTER the gesture is complete. The animation
44 * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView)
45 * to the new scale in response to events related to the user's gesture.
46 */
Derek Sollenberger90b6e482010-05-10 12:38:54 -040047class ZoomManager {
48
49 static final String LOGTAG = "webviewZoom";
50
51 private final WebView mWebView;
Derek Sollenberger03e48912010-05-18 17:03:42 -040052 private final CallbackProxy mCallbackProxy;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040053
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040054 // Widgets responsible for the on-screen zoom functions of the WebView.
Derek Sollenberger90b6e482010-05-10 12:38:54 -040055 private ZoomControlEmbedded mEmbeddedZoomControl;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040056 private ZoomControlExternal mExternalZoomControl;
57
Derek Sollenberger4aef6972010-06-24 15:03:43 -040058 /*
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -070059 * For large screen devices, the defaultScale usually set to 1.0 and
60 * equal to the overview scale, to differentiate the zoom level for double tapping,
61 * a minimum reading level scale is used.
62 */
63 private static final float MIN_READING_LEVEL_SCALE = 1.5f;
64
65 /*
Derek Sollenberger4aef6972010-06-24 15:03:43 -040066 * The scale factors that determine the upper and lower bounds for the
67 * default zoom scale.
68 */
69 protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f;
70 protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040071
Derek Sollenberger4aef6972010-06-24 15:03:43 -040072 // The default scale limits, which are dependent on the display density.
73 private float mDefaultMaxZoomScale;
74 private float mDefaultMinZoomScale;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040075
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040076 // The actual scale limits, which can be set through a webpage's viewport
77 // meta-tag.
Derek Sollenberger369aca22010-06-09 14:11:59 -040078 private float mMaxZoomScale;
79 private float mMinZoomScale;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040080
Derek Sollenbergerbffa8512010-06-10 14:24:03 -040081 // Locks the minimum ZoomScale to the value currently set in mMinZoomScale.
Derek Sollenberger369aca22010-06-09 14:11:59 -040082 private boolean mMinZoomScaleFixed = true;
Derek Sollenberger90b6e482010-05-10 12:38:54 -040083
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -040084 /*
Derek Sollenberger94345f42010-06-25 09:42:53 -040085 * When loading a new page the WebView does not initially know the final
86 * width of the page. Therefore, when a new page is loaded in overview mode
87 * the overview scale is initialized to a default value. This flag is then
88 * set and used to notify the ZoomManager to take the width of the next
89 * picture from webkit and use that width to enter into zoom overview mode.
90 */
91 private boolean mInitialZoomOverview = false;
92
93 /*
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -040094 * When in the zoom overview mode, the page's width is fully fit to the
95 * current window. Additionally while the page is in this state it is
96 * active, in other words, you can click to follow the links. We cache a
97 * boolean to enable us to quickly check whether or not we are in overview
98 * mode, but this value should only be modified by changes to the zoom
99 * scale.
100 */
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400101 private boolean mInZoomOverview = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400102 private int mZoomOverviewWidth;
103 private float mInvZoomOverviewWidth;
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400104
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400105 /*
106 * These variables track the center point of the zoom and they are used to
107 * determine the point around which we should zoom. They are stored in view
108 * coordinates.
109 */
110 private float mZoomCenterX;
111 private float mZoomCenterY;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400112
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400113 /*
114 * These values represent the point around which the screen should be
115 * centered after zooming. In other words it is used to determine the center
116 * point of the visible document after the page has finished zooming. This
117 * is important because the zoom may have potentially reflowed the text and
118 * we need to ensure the proper portion of the document remains on the
119 * screen.
120 */
121 private int mAnchorX;
122 private int mAnchorY;
123
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400124 // The scale factor that is used to determine the column width for text
125 private float mTextWrapScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400126
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400127 /*
128 * The default zoom scale is the scale factor used when the user triggers a
129 * zoom in by double tapping on the WebView. The value is initially set
130 * based on the display density, but can be changed at any time via the
131 * WebSettings.
132 */
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400133 private float mDefaultScale;
134 private float mInvDefaultScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400135
Derek Sollenberger03e48912010-05-18 17:03:42 -0400136 // the current computed zoom scale and its inverse.
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400137 private float mActualScale;
138 private float mInvActualScale;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400139
140 /*
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400141 * The initial scale for the WebView. 0 means default. If initial scale is
142 * greater than 0 the WebView starts with this value as its initial scale. The
143 * value is converted from an integer percentage so it is guarenteed to have
144 * no more than 2 significant digits after the decimal. This restriction
145 * allows us to convert the scale back to the original percentage by simply
146 * multiplying the value by 100.
147 */
148 private float mInitialScale;
149
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400150 private static float MINIMUM_SCALE_INCREMENT = 0.01f;
151
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400152 /*
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400153 * The following member variables are only to be used for animating zoom. If
154 * mZoomScale is non-zero then we are in the middle of a zoom animation. The
155 * other variables are used as a cache (e.g. inverse) or as a way to store
156 * the state of the view prior to animating (e.g. initial scroll coords).
157 */
158 private float mZoomScale;
159 private float mInvInitialZoomScale;
160 private float mInvFinalZoomScale;
161 private int mInitialScrollX;
162 private int mInitialScrollY;
163 private long mZoomStart;
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400164
165 private static final int ZOOM_ANIMATION_LENGTH = 500;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400166
Derek Sollenberger293c3602010-06-04 10:44:48 -0400167 // whether support multi-touch
168 private boolean mSupportMultiTouch;
169
170 // use the framework's ScaleGestureDetector to handle multi-touch
171 private ScaleGestureDetector mScaleDetector;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400172 private boolean mPinchToZoomAnimating = false;
173
Derek Sollenberger03e48912010-05-18 17:03:42 -0400174 public ZoomManager(WebView webView, CallbackProxy callbackProxy) {
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400175 mWebView = webView;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400176 mCallbackProxy = callbackProxy;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400177
178 /*
179 * Ideally mZoomOverviewWidth should be mContentWidth. But sites like
180 * ESPN and Engadget always have wider mContentWidth no matter what the
181 * viewport size is.
182 */
183 setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH);
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400184 }
185
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400186 /**
187 * Initialize both the default and actual zoom scale to the given density.
188 *
189 * @param density The logical density of the display. This is a scaling factor
190 * for the Density Independent Pixel unit, where one DIP is one pixel on an
191 * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
192 */
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400193 public void init(float density) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400194 assert density > 0;
195
Derek Sollenberger03e48912010-05-18 17:03:42 -0400196 setDefaultZoomScale(density);
Derek Sollenberger03e48912010-05-18 17:03:42 -0400197 mActualScale = density;
198 mInvActualScale = 1 / density;
199 mTextWrapScale = density;
200 }
201
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400202 /**
203 * Update the default zoom scale using the given density. It will also reset
204 * the current min and max zoom scales to the default boundaries as well as
205 * ensure that the actual scale falls within those boundaries.
206 *
207 * @param density The logical density of the display. This is a scaling factor
208 * for the Density Independent Pixel unit, where one DIP is one pixel on an
209 * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
210 */
Derek Sollenberger03e48912010-05-18 17:03:42 -0400211 public void updateDefaultZoomDensity(float density) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400212 assert density > 0;
213
Derek Sollenberger03e48912010-05-18 17:03:42 -0400214 if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400215 // set the new default density
216 setDefaultZoomScale(density);
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400217 // adjust the scale if it falls outside the new zoom bounds
218 setZoomScale(mActualScale, true);
Derek Sollenberger03e48912010-05-18 17:03:42 -0400219 }
220 }
221
222 private void setDefaultZoomScale(float defaultScale) {
223 mDefaultScale = defaultScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400224 mInvDefaultScale = 1 / defaultScale;
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400225 mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR;
226 mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR;
227 mMaxZoomScale = mDefaultMaxZoomScale;
228 mMinZoomScale = mDefaultMinZoomScale;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400229 }
230
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400231 public final float getScale() {
232 return mActualScale;
233 }
234
235 public final float getInvScale() {
236 return mInvActualScale;
237 }
238
239 public final float getTextWrapScale() {
240 return mTextWrapScale;
241 }
242
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400243 public final float getMaxZoomScale() {
244 return mMaxZoomScale;
245 }
246
247 public final float getMinZoomScale() {
248 return mMinZoomScale;
249 }
250
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400251 public final float getDefaultScale() {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400252 return mDefaultScale;
253 }
254
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700255 public final float getReadingLevelScale() {
256 return Math.max(mDefaultScale, MIN_READING_LEVEL_SCALE);
257 }
258
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400259 public final float getInvDefaultScale() {
260 return mInvDefaultScale;
261 }
262
263 public final float getDefaultMaxZoomScale() {
264 return mDefaultMaxZoomScale;
265 }
266
267 public final float getDefaultMinZoomScale() {
268 return mDefaultMinZoomScale;
269 }
270
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400271 public final int getDocumentAnchorX() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400272 return mAnchorX;
273 }
274
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400275 public final int getDocumentAnchorY() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400276 return mAnchorY;
277 }
278
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400279 public final void clearDocumentAnchor() {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400280 mAnchorX = mAnchorY = 0;
281 }
282
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400283 public final void setZoomCenter(float x, float y) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400284 mZoomCenterX = x;
285 mZoomCenterY = y;
286 }
287
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400288 public final void setInitialScaleInPercent(int scaleInPercent) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400289 mInitialScale = scaleInPercent * 0.01f;
290 }
291
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400292 public final float computeScaleWithLimits(float scale) {
Derek Sollenberger369aca22010-06-09 14:11:59 -0400293 if (scale < mMinZoomScale) {
294 scale = mMinZoomScale;
295 } else if (scale > mMaxZoomScale) {
296 scale = mMaxZoomScale;
297 }
298 return scale;
299 }
300
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400301 public final boolean isZoomScaleFixed() {
Derek Sollenberger369aca22010-06-09 14:11:59 -0400302 return mMinZoomScale >= mMaxZoomScale;
303 }
304
Derek Sollenberger03e48912010-05-18 17:03:42 -0400305 public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) {
306 return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT;
307 }
308
309 public boolean willScaleTriggerZoom(float scale) {
310 return exceedsMinScaleIncrement(scale, mActualScale);
311 }
312
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400313 public final boolean canZoomIn() {
Grace Kloba6164ef12010-06-01 15:59:13 -0700314 return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT;
315 }
316
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400317 public final boolean canZoomOut() {
Grace Kloba6164ef12010-06-01 15:59:13 -0700318 return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT;
Derek Sollenberger03e48912010-05-18 17:03:42 -0400319 }
320
Derek Sollenberger03e48912010-05-18 17:03:42 -0400321 public boolean zoomIn() {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400322 return zoom(1.25f);
323 }
324
325 public boolean zoomOut() {
326 return zoom(0.8f);
327 }
328
329 // returns TRUE if zoom out succeeds and FALSE if no zoom changes.
330 private boolean zoom(float zoomMultiplier) {
331 // TODO: alternatively we can disallow this during draw history mode
332 mWebView.switchOutDrawHistory();
333 // Center zooming to the center of the screen.
334 mZoomCenterX = mWebView.getViewWidth() * .5f;
335 mZoomCenterY = mWebView.getViewHeight() * .5f;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400336 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
337 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400338 return startZoomAnimation(mActualScale * zoomMultiplier, true);
Derek Sollenberger03e48912010-05-18 17:03:42 -0400339 }
340
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400341 /**
342 * Initiates an animated zoom of the WebView.
343 *
344 * @return true if the new scale triggered an animation and false otherwise.
345 */
346 public boolean startZoomAnimation(float scale, boolean reflowText) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400347 float oldScale = mActualScale;
348 mInitialScrollX = mWebView.getScrollX();
349 mInitialScrollY = mWebView.getScrollY();
350
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700351 // snap to reading level scale if it is close
352 if (!exceedsMinScaleIncrement(scale, getReadingLevelScale())) {
353 scale = getReadingLevelScale();
Derek Sollenberger03e48912010-05-18 17:03:42 -0400354 }
355
356 setZoomScale(scale, reflowText);
357
358 if (oldScale != mActualScale) {
359 // use mZoomPickerScale to see zoom preview first
360 mZoomStart = SystemClock.uptimeMillis();
361 mInvInitialZoomScale = 1.0f / oldScale;
362 mInvFinalZoomScale = 1.0f / mActualScale;
363 mZoomScale = mActualScale;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400364 mWebView.onFixedLengthZoomAnimationStart();
Derek Sollenberger03e48912010-05-18 17:03:42 -0400365 mWebView.invalidate();
366 return true;
367 } else {
368 return false;
369 }
370 }
371
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400372 /**
Derek Sollenberger293c3602010-06-04 10:44:48 -0400373 * This method is called by the WebView's drawing code when a fixed length zoom
374 * animation is occurring. Its purpose is to animate the zooming of the canvas
375 * to the desired scale which was specified in startZoomAnimation(...).
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400376 *
Derek Sollenberger293c3602010-06-04 10:44:48 -0400377 * A fixed length animation begins when startZoomAnimation(...) is called and
378 * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that
379 * interval each time the WebView draws it calls this function which is
380 * responsible for generating the animation.
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400381 *
Derek Sollenberger293c3602010-06-04 10:44:48 -0400382 * Additionally, the WebView can check to see if such an animation is currently
383 * in progress by calling isFixedLengthAnimationInProgress().
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400384 */
Derek Sollenberger293c3602010-06-04 10:44:48 -0400385 public void animateZoom(Canvas canvas) {
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400386 if (mZoomScale == 0) {
Derek Sollenberger293c3602010-06-04 10:44:48 -0400387 Log.w(LOGTAG, "A WebView is attempting to perform a fixed length "
388 + "zoom animation when no zoom is in progress");
389 return;
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400390 }
391
392 float zoomScale;
393 int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
394 if (interval < ZOOM_ANIMATION_LENGTH) {
395 float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
396 zoomScale = 1.0f / (mInvInitialZoomScale
397 + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
Derek Sollenberger293c3602010-06-04 10:44:48 -0400398 mWebView.invalidate();
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400399 } else {
400 zoomScale = mZoomScale;
401 // set mZoomScale to be 0 as we have finished animating
402 mZoomScale = 0;
Derek Sollenberger293c3602010-06-04 10:44:48 -0400403 mWebView.onFixedLengthZoomAnimationEnd();
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400404 }
405 // calculate the intermediate scroll position. Since we need to use
406 // zoomScale, we can't use the WebView's pinLocX/Y functions directly.
407 float scale = zoomScale * mInvInitialZoomScale;
408 int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX);
409 tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth()
410 * zoomScale)) + mWebView.getScrollX();
411 int titleHeight = mWebView.getTitleHeight();
412 int ty = Math.round(scale
413 * (mInitialScrollY + mZoomCenterY - titleHeight)
414 - (mZoomCenterY - titleHeight));
415 ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty
416 - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight()
417 * zoomScale)) + titleHeight) + mWebView.getScrollY();
418
Derek Sollenberger293c3602010-06-04 10:44:48 -0400419 canvas.translate(tx, ty);
420 canvas.scale(zoomScale, zoomScale);
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400421 }
422
423 public boolean isZoomAnimating() {
Derek Sollenberger293c3602010-06-04 10:44:48 -0400424 return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating;
425 }
426
427 public boolean isFixedLengthAnimationInProgress() {
Derek Sollenberger87b17be52010-06-01 11:49:31 -0400428 return mZoomScale != 0;
429 }
430
Derek Sollenberger03e48912010-05-18 17:03:42 -0400431 public void refreshZoomScale(boolean reflowText) {
432 setZoomScale(mActualScale, reflowText, true);
433 }
434
435 public void setZoomScale(float scale, boolean reflowText) {
436 setZoomScale(scale, reflowText, false);
437 }
438
439 private void setZoomScale(float scale, boolean reflowText, boolean force) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400440 final boolean isScaleLessThanMinZoom = scale < mMinZoomScale;
441 scale = computeScaleWithLimits(scale);
442
443 // determine whether or not we are in the zoom overview mode
444 if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) {
445 mInZoomOverview = true;
446 } else {
447 mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale());
Derek Sollenberger03e48912010-05-18 17:03:42 -0400448 }
449
450 if (reflowText) {
451 mTextWrapScale = scale;
452 }
453
454 if (scale != mActualScale || force) {
455 float oldScale = mActualScale;
456 float oldInvScale = mInvActualScale;
457
Derek Sollenberger293c3602010-06-04 10:44:48 -0400458 if (scale != mActualScale && !mPinchToZoomAnimating) {
Derek Sollenberger03e48912010-05-18 17:03:42 -0400459 mCallbackProxy.onScaleChanged(mActualScale, scale);
460 }
461
462 mActualScale = scale;
463 mInvActualScale = 1 / scale;
464
465 if (!mWebView.drawHistory()) {
466
467 // If history Picture is drawn, don't update scroll. They will
468 // be updated when we get out of that mode.
469 // update our scroll so we don't appear to jump
470 // i.e. keep the center of the doc in the center of the view
471 int oldX = mWebView.getScrollX();
472 int oldY = mWebView.getScrollY();
473 float ratio = scale * oldInvScale;
474 float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
475 float sy = ratio * oldY + (ratio - 1)
476 * (mZoomCenterY - mWebView.getTitleHeight());
477
478 // Scale all the child views
479 mWebView.mViewManager.scaleAll();
480
481 // as we don't have animation for scaling, don't do animation
482 // for scrolling, as it causes weird intermediate state
483 int scrollX = mWebView.pinLocX(Math.round(sx));
484 int scrollY = mWebView.pinLocY(Math.round(sy));
485 if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) {
486 // the scroll position is adjusted at the beginning of the
487 // zoom animation. But we want to update the WebKit at the
488 // end of the zoom animation. See comments in onScaleEnd().
489 mWebView.sendOurVisibleRect();
490 }
491 }
492
493 // if the we need to reflow the text then force the VIEW_SIZE_CHANGED
494 // event to be sent to WebKit
495 mWebView.sendViewSizeZoom(reflowText);
496 }
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400497 }
498
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400499 /**
500 * The double tap gesture can result in different behaviors depending on the
501 * content that is tapped.
502 *
503 * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on
504 * the screen. If the plugin is already maximized then zoom the user into
505 * overview mode.
506 *
507 * (2) HTML/OTHER: If the taps occur outside a plugin then the following
508 * heuristic is used.
509 * A. If the current scale is not the same as the text wrap scale and the
510 * layout algorithm specifies the use of NARROW_COLUMNS, then fit to
511 * column by reflowing the text.
512 * B. If the page is not in overview mode then change to overview mode.
513 * C. If the page is in overmode then change to the default scale.
514 */
515 public void handleDoubleTap(float lastTouchX, float lastTouchY) {
516 WebSettings settings = mWebView.getSettings();
517 if (settings == null || settings.getUseWideViewPort() == false) {
518 return;
519 }
520
521 setZoomCenter(lastTouchX, lastTouchY);
522 mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX());
523 mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY());
524 settings.setDoubleTapToastCount(0);
525
526 // remove the zoom control after double tap
527 dismissZoomPicker();
528
529 /*
530 * If the double tap was on a plugin then either zoom to maximize the
531 * plugin on the screen or scale to overview mode.
532 */
533 ViewManager.ChildView plugin = mWebView.mViewManager.hitTest(mAnchorX, mAnchorY);
534 if (plugin != null) {
535 if (mWebView.isPluginFitOnScreen(plugin)) {
536 zoomToOverview();
537 } else {
538 mWebView.centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height);
539 }
540 return;
541 }
542
543 if (settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS
544 && willScaleTriggerZoom(mTextWrapScale)) {
545 refreshZoomScale(true);
546 } else if (!mInZoomOverview) {
547 zoomToOverview();
548 } else {
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700549 zoomToReadingLevel();
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400550 }
551 }
552
553 private void setZoomOverviewWidth(int width) {
554 mZoomOverviewWidth = width;
555 mInvZoomOverviewWidth = 1.0f / width;
556 }
557
558 private float getZoomOverviewScale() {
559 return mWebView.getViewWidth() * mInvZoomOverviewWidth;
560 }
561
562 public boolean isInZoomOverview() {
563 return mInZoomOverview;
564 }
565
566 private void zoomToOverview() {
567 if (!willScaleTriggerZoom(getZoomOverviewScale())) return;
568
569 // Force the titlebar fully reveal in overview mode
570 int scrollY = mWebView.getScrollY();
571 if (scrollY < mWebView.getTitleHeight()) {
572 mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0);
573 }
574 startZoomAnimation(getZoomOverviewScale(), true);
575 }
576
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700577 private void zoomToReadingLevel() {
578 final float readingScale = getReadingLevelScale();
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400579 int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
580 if (left != WebView.NO_LEFTEDGE) {
581 // add a 5pt padding to the left edge.
582 int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5))
583 - mWebView.getScrollX();
584 // Re-calculate the zoom center so that the new scroll x will be
585 // on the left edge.
586 if (viewLeft > 0) {
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700587 mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale);
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400588 } else {
589 mWebView.scrollBy(viewLeft, 0);
590 mZoomCenterX = 0;
591 }
592 }
Shimeng (Simon) Wangdde858c2010-08-11 15:42:00 -0700593 startZoomAnimation(readingScale, true);
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400594 }
595
Derek Sollenberger293c3602010-06-04 10:44:48 -0400596 public void updateMultiTouchSupport(Context context) {
597 // check the preconditions
598 assert mWebView.getSettings() != null;
599
600 WebSettings settings = mWebView.getSettings();
601 mSupportMultiTouch = context.getPackageManager().hasSystemFeature(
602 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
603 && settings.supportZoom() && settings.getBuiltInZoomControls();
604 if (mSupportMultiTouch && (mScaleDetector == null)) {
605 mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener());
606 } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
607 mScaleDetector = null;
608 }
609 }
610
611 public boolean supportsMultiTouchZoom() {
612 return mSupportMultiTouch;
613 }
614
615 /**
616 * Notifies the caller that the ZoomManager is requesting that scale related
617 * updates should not be sent to webkit. This can occur in cases where the
618 * ZoomManager is performing an animation and does not want webkit to update
619 * until the animation is complete.
620 *
621 * @return true if scale related updates should not be sent to webkit and
622 * false otherwise.
623 */
624 public boolean isPreventingWebkitUpdates() {
625 // currently only animating a multi-touch zoom prevents updates, but
626 // others can add their own conditions to this method if necessary.
627 return mPinchToZoomAnimating;
628 }
629
630 public ScaleGestureDetector getMultiTouchGestureDetector() {
631 return mScaleDetector;
632 }
633
634 private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener {
635
636 public boolean onScaleBegin(ScaleGestureDetector detector) {
637 dismissZoomPicker();
Derek Sollenberger293c3602010-06-04 10:44:48 -0400638 mWebView.mViewManager.startZoom();
639 mWebView.onPinchToZoomAnimationStart();
640 return true;
641 }
642
643 public boolean onScale(ScaleGestureDetector detector) {
644 float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f;
645 if (willScaleTriggerZoom(scale)) {
646 mPinchToZoomAnimating = true;
647 // limit the scale change per step
648 if (scale > mActualScale) {
649 scale = Math.min(scale, mActualScale * 1.25f);
650 } else {
651 scale = Math.max(scale, mActualScale * 0.8f);
652 }
653 setZoomCenter(detector.getFocusX(), detector.getFocusY());
654 setZoomScale(scale, false);
655 mWebView.invalidate();
656 return true;
657 }
658 return false;
659 }
660
661 public void onScaleEnd(ScaleGestureDetector detector) {
662 if (mPinchToZoomAnimating) {
663 mPinchToZoomAnimating = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400664 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
665 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
Derek Sollenberger293c3602010-06-04 10:44:48 -0400666 // don't reflow when zoom in; when zoom out, do reflow if the
667 // new scale is almost minimum scale;
668 boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale);
669 // force zoom after mPreviewZoomOnly is set to false so that the
670 // new view size will be passed to the WebKit
671 refreshZoomScale(reflowNow);
672 // call invalidate() to draw without zoom filter
673 mWebView.invalidate();
674 }
675
676 mWebView.mViewManager.endZoom();
677 mWebView.onPinchToZoomAnimationEnd(detector);
678 }
679 }
680
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400681 public void onSizeChanged(int w, int h, int ow, int oh) {
682 // reset zoom and anchor to the top left corner of the screen
683 // unless we are already zooming
Derek Sollenberger293c3602010-06-04 10:44:48 -0400684 if (!isFixedLengthAnimationInProgress()) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400685 int visibleTitleHeight = mWebView.getVisibleTitleHeight();
686 mZoomCenterX = 0;
687 mZoomCenterY = visibleTitleHeight;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400688 mAnchorX = mWebView.viewToContentX(mWebView.getScrollX());
689 mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY());
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400690 }
691
692 // update mMinZoomScale if the minimum zoom scale is not fixed
693 if (!mMinZoomScaleFixed) {
694 // when change from narrow screen to wide screen, the new viewWidth
695 // can be wider than the old content width. We limit the minimum
696 // scale to 1.0f. The proper minimum scale will be calculated when
697 // the new picture shows up.
698 mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth()
699 / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth()
700 : mZoomOverviewWidth));
701 // limit the minZoomScale to the initialScale if it is set
702 if (mInitialScale > 0 && mInitialScale < mMinZoomScale) {
703 mMinZoomScale = mInitialScale;
704 }
705 }
706
707 dismissZoomPicker();
708
709 // onSizeChanged() is called during WebView layout. And any
710 // requestLayout() is blocked during layout. As refreshZoomScale() will
711 // cause its child View to reposition itself through ViewManager's
712 // scaleAll(), we need to post a Runnable to ensure requestLayout().
713 // Additionally, only update the text wrap scale if the width changed.
714 mWebView.post(new PostScale(w != ow));
715 }
716
717 private class PostScale implements Runnable {
718 final boolean mUpdateTextWrap;
719
720 public PostScale(boolean updateTextWrap) {
721 mUpdateTextWrap = updateTextWrap;
722 }
723
724 public void run() {
725 if (mWebView.getWebViewCore() != null) {
726 // we always force, in case our height changed, in which case we
727 // still want to send the notification over to webkit.
728 refreshZoomScale(mUpdateTextWrap);
729 // update the zoom buttons as the scale can be changed
730 updateZoomPicker();
731 }
732 }
733 }
734
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400735 public void updateZoomRange(WebViewCore.ViewState viewState,
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400736 int viewWidth, int minPrefWidth) {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400737 if (viewState.mMinScale == 0) {
738 if (viewState.mMobileSite) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400739 if (minPrefWidth > Math.max(0, viewWidth)) {
740 mMinZoomScale = (float) viewWidth / minPrefWidth;
741 mMinZoomScaleFixed = false;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400742 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400743 mMinZoomScale = viewState.mDefaultScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400744 mMinZoomScaleFixed = true;
745 }
746 } else {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400747 mMinZoomScale = mDefaultMinZoomScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400748 mMinZoomScaleFixed = false;
749 }
750 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400751 mMinZoomScale = viewState.mMinScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400752 mMinZoomScaleFixed = true;
753 }
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400754 if (viewState.mMaxScale == 0) {
Derek Sollenberger4aef6972010-06-24 15:03:43 -0400755 mMaxZoomScale = mDefaultMaxZoomScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400756 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400757 mMaxZoomScale = viewState.mMaxScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400758 }
759 }
760
761 /**
762 * Updates zoom values when Webkit produces a new picture. This method
763 * should only be called from the UI thread's message handler.
764 */
765 public void onNewPicture(WebViewCore.DrawData drawData) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400766 final int viewWidth = mWebView.getViewWidth();
767
768 if (mWebView.getSettings().getUseWideViewPort()) {
769 // limit mZoomOverviewWidth upper bound to
770 // sMaxViewportWidth so that if the page doesn't behave
771 // well, the WebView won't go insane. limit the lower
772 // bound to match the default scale for mobile sites.
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400773 setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth,
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400774 Math.max((int) (viewWidth * mInvDefaultScale),
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400775 Math.max(drawData.mMinPrefWidth, drawData.mViewPoint.x))));
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400776 }
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400777
778 final float zoomOverviewScale = getZoomOverviewScale();
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400779 if (!mMinZoomScaleFixed) {
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400780 mMinZoomScale = zoomOverviewScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400781 }
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400782 // fit the content width to the current view. Ignore the rounding error case.
Shimeng (Simon) Wangd5c6a162010-07-01 14:19:58 -0700783 if (!mWebView.drawHistory() && (mInitialZoomOverview || (mInZoomOverview
784 && Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1))) {
Derek Sollenberger94345f42010-06-25 09:42:53 -0400785 mInitialZoomOverview = false;
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400786 setZoomScale(zoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale));
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400787 }
788 }
789
790 /**
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400791 * Updates zoom values after Webkit completes the initial page layout. It
792 * is called when visiting a page for the first time as well as when the
793 * user navigates back to a page (in which case we may need to restore the
794 * zoom levels to the state they were when you left the page). This method
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400795 * should only be called from the UI thread's message handler.
796 */
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400797 public void onFirstLayout(WebViewCore.DrawData drawData) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400798 // precondition check
799 assert drawData != null;
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400800 assert drawData.mViewState != null;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400801 assert mWebView.getSettings() != null;
802
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400803 WebViewCore.ViewState viewState = drawData.mViewState;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400804 final Point viewSize = drawData.mViewPoint;
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400805 updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth);
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400806
807 if (!mWebView.drawHistory()) {
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400808 final float scale;
809 final boolean reflowText;
810
811 if (mInitialScale > 0) {
812 scale = mInitialScale;
813 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400814 } else if (viewState.mViewScale > 0) {
815 mTextWrapScale = viewState.mTextWrapScale;
816 scale = viewState.mViewScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400817 reflowText = false;
818 } else {
819 WebSettings settings = mWebView.getSettings();
Derek Sollenberger15c5ddb2010-06-10 12:31:29 -0400820 if (settings.getUseWideViewPort() && settings.getLoadWithOverviewMode()) {
Derek Sollenberger94345f42010-06-25 09:42:53 -0400821 mInitialZoomOverview = true;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400822 scale = (float) mWebView.getViewWidth() / WebView.DEFAULT_VIEWPORT_WIDTH;
823 } else {
Derek Sollenbergerb983c892010-06-28 08:38:28 -0400824 scale = viewState.mTextWrapScale;
Derek Sollenberger341e22f2010-06-02 12:34:34 -0400825 }
826 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
827 }
828 setZoomScale(scale, reflowText);
829
830 // update the zoom buttons as the scale can be changed
831 updateZoomPicker();
832 }
833 }
834
Derek Sollenbergerbffa8512010-06-10 14:24:03 -0400835 public void saveZoomState(Bundle b) {
836 b.putFloat("scale", mActualScale);
837 b.putFloat("textwrapScale", mTextWrapScale);
838 b.putBoolean("overview", mInZoomOverview);
839 }
840
841 public void restoreZoomState(Bundle b) {
842 // as getWidth() / getHeight() of the view are not available yet, set up
843 // mActualScale, so that when onSizeChanged() is called, the rest will
844 // be set correctly
845 mActualScale = b.getFloat("scale", 1.0f);
846 mInvActualScale = 1 / mActualScale;
847 mTextWrapScale = b.getFloat("textwrapScale", mActualScale);
848 mInZoomOverview = b.getBoolean("overview");
849 }
850
Derek Sollenberger90b6e482010-05-10 12:38:54 -0400851 private ZoomControlBase getCurrentZoomControl() {
852 if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) {
853 if (mWebView.getSettings().getBuiltInZoomControls()) {
854 if (mEmbeddedZoomControl == null) {
855 mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView);
856 }
857 return mEmbeddedZoomControl;
858 } else {
859 if (mExternalZoomControl == null) {
860 mExternalZoomControl = new ZoomControlExternal(mWebView);
861 }
862 return mExternalZoomControl;
863 }
864 }
865 return null;
866 }
867
868 public void invokeZoomPicker() {
869 ZoomControlBase control = getCurrentZoomControl();
870 if (control != null) {
871 control.show();
872 }
873 }
874
875 public void dismissZoomPicker() {
876 ZoomControlBase control = getCurrentZoomControl();
877 if (control != null) {
878 control.hide();
879 }
880 }
881
882 public boolean isZoomPickerVisible() {
883 ZoomControlBase control = getCurrentZoomControl();
884 return (control != null) ? control.isVisible() : false;
885 }
886
887 public void updateZoomPicker() {
888 ZoomControlBase control = getCurrentZoomControl();
889 if (control != null) {
890 control.update();
891 }
892 }
893
894 /**
895 * The embedded zoom control intercepts touch events and automatically stays
896 * visible. The external control needs to constantly refresh its internal
897 * timer to stay visible.
898 */
899 public void keepZoomPickerVisible() {
900 ZoomControlBase control = getCurrentZoomControl();
901 if (control != null && control == mExternalZoomControl) {
902 control.show();
903 }
904 }
905
906 public View getExternalZoomPicker() {
907 ZoomControlBase control = getCurrentZoomControl();
908 if (control != null && control == mExternalZoomControl) {
909 return mExternalZoomControl.getControls();
910 } else {
911 return null;
912 }
913 }
914}