1 /* 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 17 package android.webkit; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.graphics.Canvas; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.os.Bundle; 25 import android.os.SystemClock; 26 import android.util.FloatMath; 27 import android.util.Log; 28 import android.view.ScaleGestureDetector; 29 import android.view.View; 30 31 /** 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 */ 49 class ZoomManager { 50 51 static final String LOGTAG = "webviewZoom"; 52 53 private final WebViewClassic mWebView; 54 private final CallbackProxy mCallbackProxy; 55 56 // Widgets responsible for the on-screen zoom functions of the WebView. 57 private ZoomControlEmbedded mEmbeddedZoomControl; 58 private ZoomControlExternal mExternalZoomControl; 59 60 /* 61 * The scale factors that determine the upper and lower bounds for the 62 * default zoom scale. 63 */ 64 protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f; 65 protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f; 66 67 // The default scale limits, which are dependent on the display density. 68 private float mDefaultMaxZoomScale; 69 private float mDefaultMinZoomScale; 70 71 // The actual scale limits, which can be set through a webpage's viewport 72 // meta-tag. 73 private float mMaxZoomScale; 74 private float mMinZoomScale; 75 76 // Locks the minimum ZoomScale to the value currently set in mMinZoomScale. 77 private boolean mMinZoomScaleFixed = true; 78 79 /* 80 * When loading a new page the WebView does not initially know the final 81 * width of the page. Therefore, when a new page is loaded in overview mode 82 * the overview scale is initialized to a default value. This flag is then 83 * set and used to notify the ZoomManager to take the width of the next 84 * picture from webkit and use that width to enter into zoom overview mode. 85 */ 86 private boolean mInitialZoomOverview = false; 87 88 /* 89 * When in the zoom overview mode, the page's width is fully fit to the 90 * current window. Additionally while the page is in this state it is 91 * active, in other words, you can click to follow the links. We cache a 92 * boolean to enable us to quickly check whether or not we are in overview 93 * mode, but this value should only be modified by changes to the zoom 94 * scale. 95 */ 96 private boolean mInZoomOverview = false; 97 private int mZoomOverviewWidth; 98 private float mInvZoomOverviewWidth; 99 100 /* 101 * These variables track the center point of the zoom and they are used to 102 * determine the point around which we should zoom. They are stored in view 103 * coordinates. 104 */ 105 private float mZoomCenterX; 106 private float mZoomCenterY; 107 108 /* 109 * Similar to mZoomCenterX(Y), these track the focus point of the scale 110 * gesture. The difference is these get updated every time when onScale is 111 * invoked no matter if a zooming really happens. 112 */ 113 private float mFocusX; 114 private float mFocusY; 115 116 /* 117 * mFocusMovementQueue keeps track of the previous focus point movement 118 * has been through. Comparing to the difference of the gesture's previous 119 * span and current span, it determines if the gesture is for panning or 120 * zooming or both. 121 */ 122 private FocusMovementQueue mFocusMovementQueue; 123 124 /* 125 * These values represent the point around which the screen should be 126 * centered after zooming. In other words it is used to determine the center 127 * point of the visible document after the page has finished zooming. This 128 * is important because the zoom may have potentially reflowed the text and 129 * we need to ensure the proper portion of the document remains on the 130 * screen. 131 */ 132 private int mAnchorX; 133 private int mAnchorY; 134 135 // The scale factor that is used to determine the column width for text 136 private float mTextWrapScale; 137 138 /* 139 * The default zoom scale is the scale factor used when the user triggers a 140 * zoom in by double tapping on the WebView. The value is initially set 141 * based on the display density, but can be changed at any time via the 142 * WebSettings. 143 */ 144 private float mDefaultScale; 145 private float mInvDefaultScale; 146 147 /* 148 * The logical density of the display. This is a scaling factor for the 149 * Density Independent Pixel unit, where one DIP is one pixel on an 150 * approximately 160 dpi screen (see android.util.DisplayMetrics.density) 151 */ 152 private float mDisplayDensity; 153 154 /* 155 * The factor that is used to tweak the zoom scale on a double-tap, 156 * and can be changed via WebSettings. Range is from 0.75f to 1.25f. 157 */ 158 private float mDoubleTapZoomFactor = 1.0f; 159 160 /* 161 * The scale factor that is used as the minimum increment when going from 162 * overview to reading level on a double tap. 163 */ 164 private static float MIN_DOUBLE_TAP_SCALE_INCREMENT = 0.5f; 165 166 // the current computed zoom scale and its inverse. 167 private float mActualScale; 168 private float mInvActualScale; 169 170 /* 171 * The initial scale for the WebView. 0 means default. If initial scale is 172 * greater than 0, the WebView starts with this value as its initial scale. 173 */ 174 private float mInitialScale; 175 176 private static float MINIMUM_SCALE_INCREMENT = 0.007f; 177 178 /* 179 * The touch points could be changed even the fingers stop moving. 180 * We use the following to filter out the zooming jitters. 181 */ 182 private static float MINIMUM_SCALE_WITHOUT_JITTER = 0.007f; 183 184 /* 185 * The following member variables are only to be used for animating zoom. If 186 * mZoomScale is non-zero then we are in the middle of a zoom animation. The 187 * other variables are used as a cache (e.g. inverse) or as a way to store 188 * the state of the view prior to animating (e.g. initial scroll coords). 189 */ 190 private float mZoomScale; 191 private float mInvInitialZoomScale; 192 private float mInvFinalZoomScale; 193 private int mInitialScrollX; 194 private int mInitialScrollY; 195 private long mZoomStart; 196 197 private static final int ZOOM_ANIMATION_LENGTH = 175; 198 199 // whether support multi-touch 200 private boolean mSupportMultiTouch; 201 202 /** 203 * True if we have a touch panel capable of detecting smooth pan/scale at the same time 204 */ 205 private boolean mAllowPanAndScale; 206 207 // use the framework's ScaleGestureDetector to handle multi-touch 208 private ScaleGestureDetector mScaleDetector; 209 private boolean mPinchToZoomAnimating = false; 210 211 private boolean mHardwareAccelerated = false; 212 private boolean mInHWAcceleratedZoom = false; 213 214 public ZoomManager(WebViewClassic webView, CallbackProxy callbackProxy) { 215 mWebView = webView; 216 mCallbackProxy = callbackProxy; 217 218 /* 219 * Ideally mZoomOverviewWidth should be mContentWidth. But sites like 220 * ESPN and Engadget always have wider mContentWidth no matter what the 221 * viewport size is. 222 */ 223 setZoomOverviewWidth(WebViewClassic.DEFAULT_VIEWPORT_WIDTH); 224 225 mFocusMovementQueue = new FocusMovementQueue(); 226 } 227 228 /** 229 * Initialize both the default and actual zoom scale to the given density. 230 * 231 * @param density The logical density of the display. This is a scaling factor 232 * for the Density Independent Pixel unit, where one DIP is one pixel on an 233 * approximately 160 dpi screen (see android.util.DisplayMetrics.density). 234 */ 235 public void init(float density) { 236 assert density > 0; 237 238 mDisplayDensity = density; 239 setDefaultZoomScale(density); 240 mActualScale = density; 241 mInvActualScale = 1 / density; 242 mTextWrapScale = getReadingLevelScale(); 243 } 244 245 /** 246 * Update the default zoom scale using the given density. It will also reset 247 * the current min and max zoom scales to the default boundaries as well as 248 * ensure that the actual scale falls within those boundaries. 249 * 250 * @param density The logical density of the display. This is a scaling factor 251 * for the Density Independent Pixel unit, where one DIP is one pixel on an 252 * approximately 160 dpi screen (see android.util.DisplayMetrics.density). 253 */ 254 public void updateDefaultZoomDensity(float density) { 255 assert density > 0; 256 257 if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) { 258 // Remember the current zoom density before it gets changed. 259 final float originalDefault = mDefaultScale; 260 // set the new default density 261 mDisplayDensity = density; 262 setDefaultZoomScale(density); 263 float scaleChange = (originalDefault > 0.0) ? density / originalDefault: 1.0f; 264 // adjust the scale if it falls outside the new zoom bounds 265 setZoomScale(mActualScale * scaleChange, true); 266 } 267 } 268 269 private void setDefaultZoomScale(float defaultScale) { 270 final float originalDefault = mDefaultScale; 271 mDefaultScale = defaultScale; 272 mInvDefaultScale = 1 / defaultScale; 273 mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR; 274 mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR; 275 if (originalDefault > 0.0 && mMaxZoomScale > 0.0) { 276 // Keeps max zoom scale when zoom density changes. 277 mMaxZoomScale = defaultScale / originalDefault * mMaxZoomScale; 278 } else { 279 mMaxZoomScale = mDefaultMaxZoomScale; 280 } 281 if (originalDefault > 0.0 && mMinZoomScale > 0.0) { 282 // Keeps min zoom scale when zoom density changes. 283 mMinZoomScale = defaultScale / originalDefault * mMinZoomScale; 284 } else { 285 mMinZoomScale = mDefaultMinZoomScale; 286 } 287 if (!exceedsMinScaleIncrement(mMinZoomScale, mMaxZoomScale)) { 288 mMaxZoomScale = mMinZoomScale; 289 } 290 } 291 292 public final float getScale() { 293 return mActualScale; 294 } 295 296 public final float getInvScale() { 297 return mInvActualScale; 298 } 299 300 public final float getTextWrapScale() { 301 return mTextWrapScale; 302 } 303 304 public final float getMaxZoomScale() { 305 return mMaxZoomScale; 306 } 307 308 public final float getMinZoomScale() { 309 return mMinZoomScale; 310 } 311 312 public final float getDefaultScale() { 313 return mDefaultScale; 314 } 315 316 /** 317 * Returns the zoom scale used for reading text on a double-tap. 318 */ 319 public final float getReadingLevelScale() { 320 return computeScaleWithLimits(computeReadingLevelScale(getZoomOverviewScale())); 321 } 322 323 /* package */ final float computeReadingLevelScale(float scale) { 324 return Math.max(mDisplayDensity * mDoubleTapZoomFactor, 325 scale + MIN_DOUBLE_TAP_SCALE_INCREMENT); 326 } 327 328 public final float getInvDefaultScale() { 329 return mInvDefaultScale; 330 } 331 332 public final float getDefaultMaxZoomScale() { 333 return mDefaultMaxZoomScale; 334 } 335 336 public final float getDefaultMinZoomScale() { 337 return mDefaultMinZoomScale; 338 } 339 340 public final int getDocumentAnchorX() { 341 return mAnchorX; 342 } 343 344 public final int getDocumentAnchorY() { 345 return mAnchorY; 346 } 347 348 public final void clearDocumentAnchor() { 349 mAnchorX = mAnchorY = 0; 350 } 351 352 public final void setZoomCenter(float x, float y) { 353 mZoomCenterX = x; 354 mZoomCenterY = y; 355 } 356 357 public final void setInitialScaleInPercent(int scaleInPercent) { 358 mInitialScale = scaleInPercent * 0.01f; 359 } 360 361 public final float computeScaleWithLimits(float scale) { 362 if (scale < mMinZoomScale) { 363 scale = mMinZoomScale; 364 } else if (scale > mMaxZoomScale) { 365 scale = mMaxZoomScale; 366 } 367 return scale; 368 } 369 370 public final boolean isScaleOverLimits(float scale) { 371 return scale <= mMinZoomScale || scale >= mMaxZoomScale; 372 } 373 374 public final boolean isZoomScaleFixed() { 375 return mMinZoomScale >= mMaxZoomScale; 376 } 377 378 public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) { 379 return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT; 380 } 381 382 public boolean willScaleTriggerZoom(float scale) { 383 return exceedsMinScaleIncrement(scale, mActualScale); 384 } 385 386 public final boolean canZoomIn() { 387 return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT; 388 } 389 390 public final boolean canZoomOut() { 391 return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT; 392 } 393 394 public boolean zoomIn() { 395 return zoom(1.25f); 396 } 397 398 public boolean zoomOut() { 399 return zoom(0.8f); 400 } 401 402 // returns TRUE if zoom out succeeds and FALSE if no zoom changes. 403 private boolean zoom(float zoomMultiplier) { 404 mInitialZoomOverview = false; 405 // TODO: alternatively we can disallow this during draw history mode 406 mWebView.switchOutDrawHistory(); 407 // Center zooming to the center of the screen. 408 mZoomCenterX = mWebView.getViewWidth() * .5f; 409 mZoomCenterY = mWebView.getViewHeight() * .5f; 410 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 411 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 412 return startZoomAnimation(mActualScale * zoomMultiplier, 413 !mWebView.getSettings().getUseFixedViewport()); 414 } 415 416 /** 417 * Initiates an animated zoom of the WebView. 418 * 419 * @return true if the new scale triggered an animation and false otherwise. 420 */ 421 public boolean startZoomAnimation(float scale, boolean reflowText) { 422 mInitialZoomOverview = false; 423 float oldScale = mActualScale; 424 mInitialScrollX = mWebView.getScrollX(); 425 mInitialScrollY = mWebView.getScrollY(); 426 427 // snap to reading level scale if it is close 428 if (!exceedsMinScaleIncrement(scale, getReadingLevelScale())) { 429 scale = getReadingLevelScale(); 430 } 431 432 setZoomScale(scale, reflowText); 433 434 if (oldScale != mActualScale) { 435 if (mHardwareAccelerated) { 436 mInHWAcceleratedZoom = true; 437 } 438 // use mZoomPickerScale to see zoom preview first 439 mZoomStart = SystemClock.uptimeMillis(); 440 mInvInitialZoomScale = 1.0f / oldScale; 441 mInvFinalZoomScale = 1.0f / mActualScale; 442 mZoomScale = mActualScale; 443 mWebView.onFixedLengthZoomAnimationStart(); 444 mWebView.invalidate(); 445 return true; 446 } else { 447 return false; 448 } 449 } 450 451 /** 452 * This method is called by the WebView's drawing code when a fixed length zoom 453 * animation is occurring. Its purpose is to animate the zooming of the canvas 454 * to the desired scale which was specified in startZoomAnimation(...). 455 * 456 * A fixed length animation begins when startZoomAnimation(...) is called and 457 * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that 458 * interval each time the WebView draws it calls this function which is 459 * responsible for generating the animation. 460 * 461 * Additionally, the WebView can check to see if such an animation is currently 462 * in progress by calling isFixedLengthAnimationInProgress(). 463 */ 464 public void animateZoom(Canvas canvas) { 465 mInitialZoomOverview = false; 466 if (mZoomScale == 0) { 467 Log.w(LOGTAG, "A WebView is attempting to perform a fixed length " 468 + "zoom animation when no zoom is in progress"); 469 // Now that we've logged about it, go ahead and just recover 470 mInHWAcceleratedZoom = false; 471 return; 472 } 473 474 float zoomScale; 475 int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); 476 if (interval < ZOOM_ANIMATION_LENGTH) { 477 float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; 478 zoomScale = 1.0f / (mInvInitialZoomScale 479 + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); 480 mWebView.invalidate(); 481 } else { 482 zoomScale = mZoomScale; 483 // set mZoomScale to be 0 as we have finished animating 484 mZoomScale = 0; 485 mWebView.onFixedLengthZoomAnimationEnd(); 486 } 487 // calculate the intermediate scroll position. Since we need to use 488 // zoomScale, we can't use the WebView's pinLocX/Y functions directly. 489 float scale = zoomScale * mInvInitialZoomScale; 490 int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX); 491 tx = -WebViewClassic.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth() 492 * zoomScale)) + mWebView.getScrollX(); 493 int titleHeight = mWebView.getTitleHeight(); 494 int ty = Math.round(scale 495 * (mInitialScrollY + mZoomCenterY - titleHeight) 496 - (mZoomCenterY - titleHeight)); 497 ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebViewClassic.pinLoc(ty 498 - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight() 499 * zoomScale)) + titleHeight) + mWebView.getScrollY(); 500 501 if (mHardwareAccelerated) { 502 mWebView.updateScrollCoordinates(mWebView.getScrollX() - tx, mWebView.getScrollY() - ty); 503 // By adding webView matrix, we need to offset the canvas a bit 504 // to make the animation smooth. 505 canvas.translate(tx, ty); 506 setZoomScale(zoomScale, false); 507 508 if (mZoomScale == 0) { 509 // We've reached the end of the zoom animation. 510 mInHWAcceleratedZoom = false; 511 512 // Ensure that the zoom level is pushed to WebCore. This has not 513 // yet occurred because we prevent it from happening while 514 // mInHWAcceleratedZoom is true. 515 mWebView.sendViewSizeZoom(false); 516 } 517 } else { 518 canvas.translate(tx, ty); 519 canvas.scale(zoomScale, zoomScale); 520 } 521 } 522 523 public boolean isZoomAnimating() { 524 return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating; 525 } 526 527 public boolean isFixedLengthAnimationInProgress() { 528 return mZoomScale != 0 || mInHWAcceleratedZoom; 529 } 530 531 public void updateDoubleTapZoom(int doubleTapZoom) { 532 boolean zoomIn = (mTextWrapScale - mActualScale) < .1f; 533 mDoubleTapZoomFactor = doubleTapZoom / 100.0f; 534 mTextWrapScale = getReadingLevelScale(); 535 float newScale = zoomIn ? mTextWrapScale 536 : Math.min(mTextWrapScale, mActualScale); 537 setZoomScale(newScale, true, true); 538 } 539 540 public void refreshZoomScale(boolean reflowText) { 541 setZoomScale(mActualScale, reflowText, true); 542 } 543 544 public void setZoomScale(float scale, boolean reflowText) { 545 setZoomScale(scale, reflowText, false); 546 } 547 548 private void setZoomScale(float scale, boolean reflowText, boolean force) { 549 final boolean isScaleLessThanMinZoom = scale < mMinZoomScale; 550 scale = computeScaleWithLimits(scale); 551 552 // determine whether or not we are in the zoom overview mode 553 if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) { 554 mInZoomOverview = true; 555 } else { 556 mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale()); 557 } 558 559 if (reflowText && !mWebView.getSettings().getUseFixedViewport()) { 560 mTextWrapScale = scale; 561 } 562 563 if (scale != mActualScale || force) { 564 float oldScale = mActualScale; 565 float oldInvScale = mInvActualScale; 566 567 if (scale != mActualScale && !mPinchToZoomAnimating) { 568 mCallbackProxy.onScaleChanged(mActualScale, scale); 569 } 570 571 mActualScale = scale; 572 mInvActualScale = 1 / scale; 573 574 if (!mWebView.drawHistory() && !mInHWAcceleratedZoom) { 575 576 // If history Picture is drawn, don't update scroll. They will 577 // be updated when we get out of that mode. 578 // update our scroll so we don't appear to jump 579 // i.e. keep the center of the doc in the center of the view 580 // If this is part of a zoom on a HW accelerated canvas, we 581 // have already updated the scroll so don't do it again. 582 int oldX = mWebView.getScrollX(); 583 int oldY = mWebView.getScrollY(); 584 float ratio = scale * oldInvScale; 585 float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; 586 float sy = ratio * oldY + (ratio - 1) 587 * (mZoomCenterY - mWebView.getTitleHeight()); 588 589 // Scale all the child views 590 mWebView.mViewManager.scaleAll(); 591 592 // as we don't have animation for scaling, don't do animation 593 // for scrolling, as it causes weird intermediate state 594 int scrollX = mWebView.pinLocX(Math.round(sx)); 595 int scrollY = mWebView.pinLocY(Math.round(sy)); 596 if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) { 597 // the scroll position is adjusted at the beginning of the 598 // zoom animation. But we want to update the WebKit at the 599 // end of the zoom animation. See comments in onScaleEnd(). 600 mWebView.sendOurVisibleRect(); 601 } 602 } 603 604 // if the we need to reflow the text then force the VIEW_SIZE_CHANGED 605 // event to be sent to WebKit 606 mWebView.sendViewSizeZoom(reflowText); 607 } 608 } 609 610 public boolean isDoubleTapEnabled() { 611 WebSettings settings = mWebView.getSettings(); 612 return settings != null && settings.getUseWideViewPort(); 613 } 614 615 /** 616 * The double tap gesture can result in different behaviors depending on the 617 * content that is tapped. 618 * 619 * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on 620 * the screen. If the plugin is already maximized then zoom the user into 621 * overview mode. 622 * 623 * (2) HTML/OTHER: If the taps occur outside a plugin then the following 624 * heuristic is used. 625 * A. If the current text wrap scale differs from newly calculated and the 626 * layout algorithm specifies the use of NARROW_COLUMNS, then fit to 627 * column by reflowing the text. 628 * B. If the page is not in overview mode then change to overview mode. 629 * C. If the page is in overmode then change to the default scale. 630 */ 631 public void handleDoubleTap(float lastTouchX, float lastTouchY) { 632 // User takes action, set initial zoom overview to false. 633 mInitialZoomOverview = false; 634 WebSettingsClassic settings = mWebView.getSettings(); 635 if (!isDoubleTapEnabled()) { 636 return; 637 } 638 639 setZoomCenter(lastTouchX, lastTouchY); 640 mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX()); 641 mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY()); 642 settings.setDoubleTapToastCount(0); 643 644 // remove the zoom control after double tap 645 dismissZoomPicker(); 646 647 final float newTextWrapScale; 648 if (settings.getUseFixedViewport()) { 649 newTextWrapScale = Math.max(mActualScale, getReadingLevelScale()); 650 } else { 651 newTextWrapScale = mActualScale; 652 } 653 final boolean firstTimeReflow = !exceedsMinScaleIncrement(mActualScale, mTextWrapScale); 654 if (firstTimeReflow || mInZoomOverview) { 655 // In case first time reflow or in zoom overview mode, let reflow and zoom 656 // happen at the same time. 657 mTextWrapScale = newTextWrapScale; 658 } 659 if (settings.isNarrowColumnLayout() 660 && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale) 661 && !firstTimeReflow 662 && !mInZoomOverview) { 663 // Reflow only. 664 mTextWrapScale = newTextWrapScale; 665 refreshZoomScale(true); 666 } else if (!mInZoomOverview && willScaleTriggerZoom(getZoomOverviewScale())) { 667 // Reflow, if necessary. 668 if (mTextWrapScale > getReadingLevelScale()) { 669 mTextWrapScale = getReadingLevelScale(); 670 refreshZoomScale(true); 671 } 672 zoomToOverview(); 673 } else { 674 zoomToReadingLevel(); 675 } 676 } 677 678 private void setZoomOverviewWidth(int width) { 679 if (width == 0) { 680 mZoomOverviewWidth = WebViewClassic.DEFAULT_VIEWPORT_WIDTH; 681 } else { 682 mZoomOverviewWidth = width; 683 } 684 mInvZoomOverviewWidth = 1.0f / width; 685 } 686 687 /* package */ float getZoomOverviewScale() { 688 return mWebView.getViewWidth() * mInvZoomOverviewWidth; 689 } 690 691 public boolean isInZoomOverview() { 692 return mInZoomOverview; 693 } 694 695 private void zoomToOverview() { 696 // Force the titlebar fully reveal in overview mode 697 int scrollY = mWebView.getScrollY(); 698 if (scrollY < mWebView.getTitleHeight()) { 699 mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0); 700 } 701 startZoomAnimation(getZoomOverviewScale(), 702 !mWebView.getSettings().getUseFixedViewport()); 703 } 704 705 private void zoomToReadingLevel() { 706 final float readingScale = getReadingLevelScale(); 707 708 int left = mWebView.getBlockLeftEdge(mAnchorX, mAnchorY, readingScale); 709 if (left != WebViewClassic.NO_LEFTEDGE) { 710 // add a 5pt padding to the left edge. 711 int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5)) 712 - mWebView.getScrollX(); 713 // Re-calculate the zoom center so that the new scroll x will be 714 // on the left edge. 715 if (viewLeft > 0) { 716 mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale); 717 } else { 718 mWebView.getWebView().scrollBy(viewLeft, 0); 719 mZoomCenterX = 0; 720 } 721 } 722 startZoomAnimation(readingScale, 723 !mWebView.getSettings().getUseFixedViewport()); 724 } 725 726 public void updateMultiTouchSupport(Context context) { 727 // check the preconditions 728 assert mWebView.getSettings() != null; 729 730 final WebSettings settings = mWebView.getSettings(); 731 final PackageManager pm = context.getPackageManager(); 732 mSupportMultiTouch = 733 (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) 734 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT)) 735 && settings.supportZoom() && settings.getBuiltInZoomControls(); 736 mAllowPanAndScale = 737 pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT) 738 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT); 739 740 if (mSupportMultiTouch && (mScaleDetector == null)) { 741 mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener()); 742 } else if (!mSupportMultiTouch && (mScaleDetector != null)) { 743 mScaleDetector = null; 744 } 745 } 746 747 public boolean supportsMultiTouchZoom() { 748 return mSupportMultiTouch; 749 } 750 751 public boolean supportsPanDuringZoom() { 752 return mAllowPanAndScale; 753 } 754 755 /** 756 * Notifies the caller that the ZoomManager is requesting that scale related 757 * updates should not be sent to webkit. This can occur in cases where the 758 * ZoomManager is performing an animation and does not want webkit to update 759 * until the animation is complete. 760 * 761 * @return true if scale related updates should not be sent to webkit and 762 * false otherwise. 763 */ 764 public boolean isPreventingWebkitUpdates() { 765 // currently only animating a multi-touch zoom and fixed length 766 // animations prevent updates, but others can add their own conditions 767 // to this method if necessary. 768 return isZoomAnimating(); 769 } 770 771 public ScaleGestureDetector getMultiTouchGestureDetector() { 772 return mScaleDetector; 773 } 774 775 private class FocusMovementQueue { 776 private static final int QUEUE_CAPACITY = 5; 777 private float[] mQueue; 778 private float mSum; 779 private int mSize; 780 private int mIndex; 781 782 FocusMovementQueue() { 783 mQueue = new float[QUEUE_CAPACITY]; 784 mSize = 0; 785 mSum = 0; 786 mIndex = 0; 787 } 788 789 private void clear() { 790 mSize = 0; 791 mSum = 0; 792 mIndex = 0; 793 for (int i = 0; i < QUEUE_CAPACITY; ++i) { 794 mQueue[i] = 0; 795 } 796 } 797 798 private void add(float focusDelta) { 799 mSum += focusDelta; 800 if (mSize < QUEUE_CAPACITY) { // fill up the queue. 801 mSize++; 802 } else { // circulate the queue. 803 mSum -= mQueue[mIndex]; 804 } 805 mQueue[mIndex] = focusDelta; 806 mIndex = (mIndex + 1) % QUEUE_CAPACITY; 807 } 808 809 private float getSum() { 810 return mSum; 811 } 812 } 813 814 private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener { 815 private float mAccumulatedSpan; 816 817 public boolean onScaleBegin(ScaleGestureDetector detector) { 818 mInitialZoomOverview = false; 819 dismissZoomPicker(); 820 mFocusMovementQueue.clear(); 821 mFocusX = detector.getFocusX(); 822 mFocusY = detector.getFocusY(); 823 mWebView.mViewManager.startZoom(); 824 mWebView.onPinchToZoomAnimationStart(); 825 mAccumulatedSpan = 0; 826 return true; 827 } 828 829 // If the user moves the fingers but keeps the same distance between them, 830 // we should do panning only. 831 public boolean isPanningOnly(ScaleGestureDetector detector) { 832 float prevFocusX = mFocusX; 833 float prevFocusY = mFocusY; 834 mFocusX = detector.getFocusX(); 835 mFocusY = detector.getFocusY(); 836 float focusDelta = (prevFocusX == 0 && prevFocusY == 0) ? 0 : 837 FloatMath.sqrt((mFocusX - prevFocusX) * (mFocusX - prevFocusX) 838 + (mFocusY - prevFocusY) * (mFocusY - prevFocusY)); 839 mFocusMovementQueue.add(focusDelta); 840 float deltaSpan = detector.getCurrentSpan() - detector.getPreviousSpan() + 841 mAccumulatedSpan; 842 final boolean result = mFocusMovementQueue.getSum() > Math.abs(deltaSpan); 843 if (result) { 844 mAccumulatedSpan += deltaSpan; 845 } else { 846 mAccumulatedSpan = 0; 847 } 848 return result; 849 } 850 851 public boolean handleScale(ScaleGestureDetector detector) { 852 float scale = detector.getScaleFactor() * mActualScale; 853 854 // if scale is limited by any reason, don't zoom but do ask 855 // the detector to update the event. 856 boolean isScaleLimited = 857 isScaleOverLimits(scale) || scale < getZoomOverviewScale(); 858 859 // Prevent scaling beyond overview scale. 860 scale = Math.max(computeScaleWithLimits(scale), getZoomOverviewScale()); 861 862 if (mPinchToZoomAnimating || willScaleTriggerZoom(scale)) { 863 mPinchToZoomAnimating = true; 864 // limit the scale change per step 865 if (scale > mActualScale) { 866 scale = Math.min(scale, mActualScale * 1.25f); 867 } else { 868 scale = Math.max(scale, mActualScale * 0.8f); 869 } 870 scale = computeScaleWithLimits(scale); 871 // if the scale change is too small, regard it as jitter and skip it. 872 if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_WITHOUT_JITTER) { 873 return isScaleLimited; 874 } 875 setZoomCenter(detector.getFocusX(), detector.getFocusY()); 876 setZoomScale(scale, false); 877 mWebView.invalidate(); 878 return true; 879 } 880 return isScaleLimited; 881 } 882 883 public boolean onScale(ScaleGestureDetector detector) { 884 if (isPanningOnly(detector) || handleScale(detector)) { 885 mFocusMovementQueue.clear(); 886 return true; 887 } 888 return false; 889 } 890 891 public void onScaleEnd(ScaleGestureDetector detector) { 892 if (mPinchToZoomAnimating) { 893 mPinchToZoomAnimating = false; 894 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 895 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 896 // don't reflow when zoom in; when zoom out, do reflow if the 897 // new scale is almost minimum scale. 898 boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale); 899 // force zoom after mPreviewZoomOnly is set to false so that the 900 // new view size will be passed to the WebKit 901 refreshZoomScale(reflowNow && 902 !mWebView.getSettings().getUseFixedViewport()); 903 // call invalidate() to draw without zoom filter 904 mWebView.invalidate(); 905 } 906 907 mWebView.mViewManager.endZoom(); 908 mWebView.onPinchToZoomAnimationEnd(detector); 909 } 910 } 911 912 public void onSizeChanged(int w, int h, int ow, int oh) { 913 // reset zoom and anchor to the top left corner of the screen 914 // unless we are already zooming 915 if (!isFixedLengthAnimationInProgress()) { 916 int visibleTitleHeight = mWebView.getVisibleTitleHeight(); 917 mZoomCenterX = 0; 918 mZoomCenterY = visibleTitleHeight; 919 mAnchorX = mWebView.viewToContentX(mWebView.getScrollX()); 920 mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY()); 921 } 922 923 // update mMinZoomScale if the minimum zoom scale is not fixed 924 if (!mMinZoomScaleFixed) { 925 // when change from narrow screen to wide screen, the new viewWidth 926 // can be wider than the old content width. We limit the minimum 927 // scale to 1.0f. The proper minimum scale will be calculated when 928 // the new picture shows up. 929 mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth() 930 / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth() 931 : mZoomOverviewWidth)); 932 // limit the minZoomScale to the initialScale if it is set 933 if (mInitialScale > 0 && mInitialScale < mMinZoomScale) { 934 mMinZoomScale = mInitialScale; 935 } 936 } 937 938 dismissZoomPicker(); 939 940 // onSizeChanged() is called during WebView layout. And any 941 // requestLayout() is blocked during layout. As refreshZoomScale() will 942 // cause its child View to reposition itself through ViewManager's 943 // scaleAll(), we need to post a Runnable to ensure requestLayout(). 944 // Additionally, only update the text wrap scale if the width changed. 945 mWebView.getWebView().post(new PostScale(w != ow && 946 !mWebView.getSettings().getUseFixedViewport(), mInZoomOverview, w < ow)); 947 } 948 949 private class PostScale implements Runnable { 950 final boolean mUpdateTextWrap; 951 // Remember the zoom overview state right after rotation since 952 // it could be changed between the time this callback is initiated and 953 // the time it's actually run. 954 final boolean mInZoomOverviewBeforeSizeChange; 955 final boolean mInPortraitMode; 956 957 public PostScale(boolean updateTextWrap, 958 boolean inZoomOverview, 959 boolean inPortraitMode) { 960 mUpdateTextWrap = updateTextWrap; 961 mInZoomOverviewBeforeSizeChange = inZoomOverview; 962 mInPortraitMode = inPortraitMode; 963 } 964 965 public void run() { 966 if (mWebView.getWebViewCore() != null) { 967 // we always force, in case our height changed, in which case we 968 // still want to send the notification over to webkit. 969 // Keep overview mode unchanged when rotating. 970 float newScale = mActualScale; 971 if (mWebView.getSettings().getUseWideViewPort() && 972 mInPortraitMode && 973 mInZoomOverviewBeforeSizeChange) { 974 newScale = getZoomOverviewScale(); 975 } 976 setZoomScale(newScale, mUpdateTextWrap, true); 977 // update the zoom buttons as the scale can be changed 978 updateZoomPicker(); 979 } 980 } 981 } 982 983 public void updateZoomRange(WebViewCore.ViewState viewState, 984 int viewWidth, int minPrefWidth) { 985 if (viewState.mMinScale == 0) { 986 if (viewState.mMobileSite) { 987 if (minPrefWidth > Math.max(0, viewWidth)) { 988 mMinZoomScale = (float) viewWidth / minPrefWidth; 989 mMinZoomScaleFixed = false; 990 } else { 991 mMinZoomScale = viewState.mDefaultScale; 992 mMinZoomScaleFixed = true; 993 } 994 } else { 995 mMinZoomScale = mDefaultMinZoomScale; 996 mMinZoomScaleFixed = false; 997 } 998 } else { 999 mMinZoomScale = viewState.mMinScale; 1000 mMinZoomScaleFixed = true; 1001 } 1002 if (viewState.mMaxScale == 0) { 1003 mMaxZoomScale = mDefaultMaxZoomScale; 1004 } else { 1005 mMaxZoomScale = viewState.mMaxScale; 1006 } 1007 } 1008 1009 /** 1010 * Updates zoom values when Webkit produces a new picture. This method 1011 * should only be called from the UI thread's message handler. 1012 * 1013 * @return True if zoom value has changed 1014 */ 1015 public boolean onNewPicture(WebViewCore.DrawData drawData) { 1016 final int viewWidth = mWebView.getViewWidth(); 1017 final boolean zoomOverviewWidthChanged = setupZoomOverviewWidth(drawData, viewWidth); 1018 final float newZoomOverviewScale = getZoomOverviewScale(); 1019 WebSettingsClassic settings = mWebView.getSettings(); 1020 if (zoomOverviewWidthChanged && settings.isNarrowColumnLayout() && 1021 settings.getUseFixedViewport() && 1022 (mInitialZoomOverview || mInZoomOverview)) { 1023 // Keep mobile site's text wrap scale unchanged. For mobile sites, 1024 // the text wrap scale is the same as zoom overview scale. 1025 if (exceedsMinScaleIncrement(mTextWrapScale, mDefaultScale) || 1026 exceedsMinScaleIncrement(newZoomOverviewScale, mDefaultScale)) { 1027 mTextWrapScale = getReadingLevelScale(); 1028 } else { 1029 mTextWrapScale = newZoomOverviewScale; 1030 } 1031 } 1032 1033 if (!mMinZoomScaleFixed || settings.getUseWideViewPort()) { 1034 mMinZoomScale = newZoomOverviewScale; 1035 mMaxZoomScale = Math.max(mMaxZoomScale, mMinZoomScale); 1036 } 1037 // fit the content width to the current view for the first new picture 1038 // after first layout. 1039 boolean scaleHasDiff = exceedsMinScaleIncrement(newZoomOverviewScale, mActualScale); 1040 // Make sure the actual scale is no less than zoom overview scale. 1041 boolean scaleLessThanOverview = 1042 (newZoomOverviewScale - mActualScale) >= MINIMUM_SCALE_INCREMENT; 1043 // Make sure mobile sites are correctly handled since mobile site will 1044 // change content width after rotating. 1045 boolean mobileSiteInOverview = mInZoomOverview && 1046 !exceedsMinScaleIncrement(newZoomOverviewScale, mDefaultScale); 1047 if (!mWebView.drawHistory() && 1048 ((scaleLessThanOverview && settings.getUseWideViewPort())|| 1049 ((mInitialZoomOverview || mobileSiteInOverview) && 1050 scaleHasDiff && zoomOverviewWidthChanged))) { 1051 mInitialZoomOverview = false; 1052 setZoomScale(newZoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale) && 1053 !mWebView.getSettings().getUseFixedViewport()); 1054 } else { 1055 mInZoomOverview = !scaleHasDiff; 1056 } 1057 if (drawData.mFirstLayoutForNonStandardLoad && settings.getLoadWithOverviewMode()) { 1058 // Set mInitialZoomOverview in case this is the first picture for non standard load, 1059 // so next new picture could be forced into overview mode if it's true. 1060 mInitialZoomOverview = mInZoomOverview; 1061 } 1062 1063 return scaleHasDiff; 1064 } 1065 1066 /** 1067 * Set up correct zoom overview width based on different settings. 1068 * 1069 * @param drawData webviewcore draw data 1070 * @param viewWidth current view width 1071 */ 1072 private boolean setupZoomOverviewWidth(WebViewCore.DrawData drawData, final int viewWidth) { 1073 WebSettings settings = mWebView.getSettings(); 1074 int newZoomOverviewWidth = mZoomOverviewWidth; 1075 if (settings.getUseWideViewPort()) { 1076 if (drawData.mContentSize.x > 0) { 1077 // The webkitDraw for layers will not populate contentSize, and it'll be 1078 // ignored for zoom overview width update. 1079 newZoomOverviewWidth = Math.min(WebViewClassic.sMaxViewportWidth, 1080 drawData.mContentSize.x); 1081 } 1082 } else { 1083 // If not use wide viewport, use view width as the zoom overview width. 1084 newZoomOverviewWidth = Math.round(viewWidth / mDefaultScale); 1085 } 1086 if (newZoomOverviewWidth != mZoomOverviewWidth) { 1087 setZoomOverviewWidth(newZoomOverviewWidth); 1088 return true; 1089 } 1090 return false; 1091 } 1092 1093 /** 1094 * Updates zoom values after Webkit completes the initial page layout. It 1095 * is called when visiting a page for the first time as well as when the 1096 * user navigates back to a page (in which case we may need to restore the 1097 * zoom levels to the state they were when you left the page). This method 1098 * should only be called from the UI thread's message handler. 1099 */ 1100 public void onFirstLayout(WebViewCore.DrawData drawData) { 1101 // precondition check 1102 assert drawData != null; 1103 assert drawData.mViewState != null; 1104 assert mWebView.getSettings() != null; 1105 1106 WebViewCore.ViewState viewState = drawData.mViewState; 1107 final Point viewSize = drawData.mViewSize; 1108 updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth); 1109 setupZoomOverviewWidth(drawData, mWebView.getViewWidth()); 1110 final float overviewScale = getZoomOverviewScale(); 1111 WebSettingsClassic settings = mWebView.getSettings(); 1112 if (!mMinZoomScaleFixed || settings.getUseWideViewPort()) { 1113 mMinZoomScale = (mInitialScale > 0) ? 1114 Math.min(mInitialScale, overviewScale) : overviewScale; 1115 mMaxZoomScale = Math.max(mMaxZoomScale, mMinZoomScale); 1116 } 1117 1118 if (!mWebView.drawHistory()) { 1119 float scale; 1120 if (mInitialScale > 0) { 1121 scale = mInitialScale; 1122 } else if (viewState.mIsRestored || viewState.mViewScale > 0) { 1123 scale = (viewState.mViewScale > 0) 1124 ? viewState.mViewScale : overviewScale; 1125 mTextWrapScale = (viewState.mTextWrapScale > 0) 1126 ? viewState.mTextWrapScale : getReadingLevelScale(); 1127 } else { 1128 scale = overviewScale; 1129 if (!settings.getUseWideViewPort() 1130 || !settings.getLoadWithOverviewMode()) { 1131 scale = Math.max(mDefaultScale, scale); 1132 } 1133 if (settings.isNarrowColumnLayout() && 1134 settings.getUseFixedViewport()) { 1135 // When first layout, reflow using the reading level scale to avoid 1136 // reflow when double tapped. 1137 mTextWrapScale = getReadingLevelScale(); 1138 } 1139 } 1140 boolean reflowText = false; 1141 if (!viewState.mIsRestored) { 1142 if (settings.getUseFixedViewport()) { 1143 // Override the scale only in case of fixed viewport. 1144 scale = Math.max(scale, overviewScale); 1145 mTextWrapScale = Math.max(mTextWrapScale, overviewScale); 1146 } 1147 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); 1148 } 1149 mInitialZoomOverview = settings.getLoadWithOverviewMode() && 1150 !exceedsMinScaleIncrement(scale, overviewScale); 1151 setZoomScale(scale, reflowText); 1152 1153 // update the zoom buttons as the scale can be changed 1154 updateZoomPicker(); 1155 } 1156 } 1157 1158 public void saveZoomState(Bundle b) { 1159 b.putFloat("scale", mActualScale); 1160 b.putFloat("textwrapScale", mTextWrapScale); 1161 b.putBoolean("overview", mInZoomOverview); 1162 } 1163 1164 public void restoreZoomState(Bundle b) { 1165 // as getWidth() / getHeight() of the view are not available yet, set up 1166 // mActualScale, so that when onSizeChanged() is called, the rest will 1167 // be set correctly 1168 mActualScale = b.getFloat("scale", 1.0f); 1169 mInvActualScale = 1 / mActualScale; 1170 mTextWrapScale = b.getFloat("textwrapScale", mActualScale); 1171 mInZoomOverview = b.getBoolean("overview"); 1172 } 1173 1174 private ZoomControlBase getCurrentZoomControl() { 1175 if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) { 1176 if (mWebView.getSettings().getBuiltInZoomControls()) { 1177 if ((mEmbeddedZoomControl == null) 1178 && mWebView.getSettings().getDisplayZoomControls()) { 1179 mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView); 1180 } 1181 return mEmbeddedZoomControl; 1182 } else { 1183 if (mExternalZoomControl == null) { 1184 mExternalZoomControl = new ZoomControlExternal(mWebView); 1185 } 1186 return mExternalZoomControl; 1187 } 1188 } 1189 return null; 1190 } 1191 1192 public void invokeZoomPicker() { 1193 ZoomControlBase control = getCurrentZoomControl(); 1194 if (control != null) { 1195 control.show(); 1196 } 1197 } 1198 1199 public void dismissZoomPicker() { 1200 ZoomControlBase control = getCurrentZoomControl(); 1201 if (control != null) { 1202 control.hide(); 1203 } 1204 } 1205 1206 public boolean isZoomPickerVisible() { 1207 ZoomControlBase control = getCurrentZoomControl(); 1208 return (control != null) ? control.isVisible() : false; 1209 } 1210 1211 public void updateZoomPicker() { 1212 ZoomControlBase control = getCurrentZoomControl(); 1213 if (control != null) { 1214 control.update(); 1215 } 1216 } 1217 1218 /** 1219 * The embedded zoom control intercepts touch events and automatically stays 1220 * visible. The external control needs to constantly refresh its internal 1221 * timer to stay visible. 1222 */ 1223 public void keepZoomPickerVisible() { 1224 ZoomControlBase control = getCurrentZoomControl(); 1225 if (control != null && control == mExternalZoomControl) { 1226 control.show(); 1227 } 1228 } 1229 1230 public View getExternalZoomPicker() { 1231 ZoomControlBase control = getCurrentZoomControl(); 1232 if (control != null && control == mExternalZoomControl) { 1233 return mExternalZoomControl.getControls(); 1234 } else { 1235 return null; 1236 } 1237 } 1238 1239 public void setHardwareAccelerated() { 1240 mHardwareAccelerated = true; 1241 } 1242 1243 /** 1244 * OnPageFinished called by webview when a page is fully loaded. 1245 */ 1246 /* package*/ void onPageFinished(String url) { 1247 // Turn off initial zoom overview flag when a page is fully loaded. 1248 mInitialZoomOverview = false; 1249 } 1250 } 1251