ZoomManager.java revision 15c5ddb7fd6357ea8ba2cccb18284137b8113401
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 17package android.webkit; 18 19import android.content.Context; 20import android.content.pm.PackageManager; 21import android.graphics.Canvas; 22import android.graphics.Point; 23import android.os.SystemClock; 24import android.util.Log; 25import android.view.ScaleGestureDetector; 26import android.view.View; 27 28/** 29 * The ZoomManager is responsible for maintaining the WebView's current zoom 30 * level state. It is also responsible for managing the on-screen zoom controls 31 * as well as any animation of the WebView due to zooming. 32 * 33 * Currently, there are two methods for animating the zoom of a WebView. 34 * 35 * (1) The first method is triggered by startZoomAnimation(...) and is a fixed 36 * length animation where the final zoom scale is known at startup. This type of 37 * animation notifies webkit of the final scale BEFORE it animates. The animation 38 * is then done by scaling the CANVAS incrementally based on a stepping function. 39 * 40 * (2) The second method is triggered by a multi-touch pinch and the new scale 41 * is determined dynamically based on the user's gesture. This type of animation 42 * only notifies webkit of new scale AFTER the gesture is complete. The animation 43 * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView) 44 * to the new scale in response to events related to the user's gesture. 45 */ 46class ZoomManager { 47 48 static final String LOGTAG = "webviewZoom"; 49 50 private final WebView mWebView; 51 private final CallbackProxy mCallbackProxy; 52 53 // manages the on-screen zoom functions of the WebView 54 private ZoomControlEmbedded mEmbeddedZoomControl; 55 56 private ZoomControlExternal mExternalZoomControl; 57 58 /* 59 * TODO: clean up the visibility of the class variables when the zoom 60 * refactoring is complete 61 */ 62 63 // default scale limits, which are dependent on the display density 64 static float DEFAULT_MAX_ZOOM_SCALE; 65 static float DEFAULT_MIN_ZOOM_SCALE; 66 67 // actual scale limits, which can be set through a webpage viewport meta tag 68 private float mMaxZoomScale; 69 private float mMinZoomScale; 70 71 // locks the minimum ZoomScale to the value currently set in mMinZoomScale 72 private boolean mMinZoomScaleFixed = true; 73 74 /* 75 * When in the zoom overview mode, the page's width is fully fit to the 76 * current window. Additionally while the page is in this state it is 77 * active, in other words, you can click to follow the links. We cache a 78 * boolean to enable us to quickly check whether or not we are in overview 79 * mode, but this value should only be modified by changes to the zoom 80 * scale. 81 */ 82 boolean mInZoomOverview = false; 83 private int mZoomOverviewWidth; 84 private float mInvZoomOverviewWidth; 85 86 // These keep track of the center point of the zoom and they are used to 87 // determine the point around which we should zoom. They are stored in view 88 // coordinates. 89 float mZoomCenterX; 90 float mZoomCenterY; 91 92 /* 93 * These values represent the point around which the screen should be 94 * centered after zooming. In other words it is used to determine the center 95 * point of the visible document after the page has finished zooming. This 96 * is important because the zoom may have potentially reflowed the text and 97 * we need to ensure the proper portion of the document remains on the 98 * screen. 99 */ 100 private int mAnchorX; 101 private int mAnchorY; 102 103 float mTextWrapScale; 104 105 // the default zoom scale. This value will is initially set based on the 106 // display density, but can be changed at any time via the WebSettings. 107 private float mDefaultScale; 108 private float mInvDefaultScale; 109 110 private static float MINIMUM_SCALE_INCREMENT = 0.01f; 111 112 // the current computed zoom scale and its inverse. 113 float mActualScale; 114 float mInvActualScale; 115 116 /* 117 * The initial scale for the WebView. 0 means default. If initial scale is 118 * greater than 0 the WebView starts with this value as its initial scale. The 119 * value is converted from an integer percentage so it is guarenteed to have 120 * no more than 2 significant digits after the decimal. This restriction 121 * allows us to convert the scale back to the original percentage by simply 122 * multiplying the value by 100. 123 */ 124 private float mInitialScale; 125 126 /* 127 * The following member variables are only to be used for animating zoom. If 128 * mZoomScale is non-zero then we are in the middle of a zoom animation. The 129 * other variables are used as a cache (e.g. inverse) or as a way to store 130 * the state of the view prior to animating (e.g. initial scroll coords). 131 */ 132 private float mZoomScale; 133 private float mInvInitialZoomScale; 134 private float mInvFinalZoomScale; 135 private int mInitialScrollX; 136 private int mInitialScrollY; 137 private long mZoomStart; 138 static final int ZOOM_ANIMATION_LENGTH = 500; 139 140 // whether support multi-touch 141 private boolean mSupportMultiTouch; 142 143 // use the framework's ScaleGestureDetector to handle multi-touch 144 private ScaleGestureDetector mScaleDetector; 145 private boolean mPinchToZoomAnimating = false; 146 147 public ZoomManager(WebView webView, CallbackProxy callbackProxy) { 148 mWebView = webView; 149 mCallbackProxy = callbackProxy; 150 151 /* 152 * Ideally mZoomOverviewWidth should be mContentWidth. But sites like 153 * ESPN and Engadget always have wider mContentWidth no matter what the 154 * viewport size is. 155 */ 156 setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH); 157 } 158 159 public void init(float density) { 160 setDefaultZoomScale(density); 161 mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; 162 mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; 163 mActualScale = density; 164 mInvActualScale = 1 / density; 165 mTextWrapScale = density; 166 } 167 168 public void updateDefaultZoomDensity(float density) { 169 if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) { 170 float scaleFactor = density * mInvDefaultScale; 171 // set the new default density 172 setDefaultZoomScale(density); 173 // adjust the limits 174 mMaxZoomScale *= scaleFactor; 175 mMinZoomScale *= scaleFactor; 176 setZoomScale(mActualScale * scaleFactor, true); 177 } 178 } 179 180 private void setDefaultZoomScale(float defaultScale) { 181 mDefaultScale = defaultScale; 182 mInvDefaultScale = 1 / defaultScale; 183 DEFAULT_MAX_ZOOM_SCALE = 4.0f * defaultScale; 184 DEFAULT_MIN_ZOOM_SCALE = 0.25f * defaultScale; 185 } 186 187 public float getDefaultScale() { 188 return mDefaultScale; 189 } 190 191 public int getDocumentAnchorX() { 192 return mAnchorX; 193 } 194 195 public int getDocumentAnchorY() { 196 return mAnchorY; 197 } 198 199 public void clearDocumentAnchor() { 200 mAnchorX = mAnchorY = 0; 201 } 202 203 public void setZoomCenter(float x, float y) { 204 mZoomCenterX = x; 205 mZoomCenterY = y; 206 } 207 208 public void setInitialScaleInPercent(int scaleInPercent) { 209 mInitialScale = scaleInPercent * 0.01f; 210 } 211 212 public float computeScaleWithLimits(float scale) { 213 if (scale < mMinZoomScale) { 214 scale = mMinZoomScale; 215 } else if (scale > mMaxZoomScale) { 216 scale = mMaxZoomScale; 217 } 218 return scale; 219 } 220 221 public boolean isZoomScaleFixed() { 222 return mMinZoomScale >= mMaxZoomScale; 223 } 224 225 public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) { 226 return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT; 227 } 228 229 public boolean willScaleTriggerZoom(float scale) { 230 return exceedsMinScaleIncrement(scale, mActualScale); 231 } 232 233 public boolean canZoomIn() { 234 return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT; 235 } 236 237 public boolean canZoomOut() { 238 return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT; 239 } 240 241 public boolean zoomIn() { 242 return zoom(1.25f); 243 } 244 245 public boolean zoomOut() { 246 return zoom(0.8f); 247 } 248 249 // returns TRUE if zoom out succeeds and FALSE if no zoom changes. 250 private boolean zoom(float zoomMultiplier) { 251 // TODO: alternatively we can disallow this during draw history mode 252 mWebView.switchOutDrawHistory(); 253 // Center zooming to the center of the screen. 254 mZoomCenterX = mWebView.getViewWidth() * .5f; 255 mZoomCenterY = mWebView.getViewHeight() * .5f; 256 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 257 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 258 return startZoomAnimation(mActualScale * zoomMultiplier, true); 259 } 260 261 /** 262 * Initiates an animated zoom of the WebView. 263 * 264 * @return true if the new scale triggered an animation and false otherwise. 265 */ 266 public boolean startZoomAnimation(float scale, boolean reflowText) { 267 float oldScale = mActualScale; 268 mInitialScrollX = mWebView.getScrollX(); 269 mInitialScrollY = mWebView.getScrollY(); 270 271 // snap to DEFAULT_SCALE if it is close 272 if (!exceedsMinScaleIncrement(scale, mDefaultScale)) { 273 scale = mDefaultScale; 274 } 275 276 setZoomScale(scale, reflowText); 277 278 if (oldScale != mActualScale) { 279 // use mZoomPickerScale to see zoom preview first 280 mZoomStart = SystemClock.uptimeMillis(); 281 mInvInitialZoomScale = 1.0f / oldScale; 282 mInvFinalZoomScale = 1.0f / mActualScale; 283 mZoomScale = mActualScale; 284 mWebView.onFixedLengthZoomAnimationStart(); 285 mWebView.invalidate(); 286 return true; 287 } else { 288 return false; 289 } 290 } 291 292 /** 293 * This method is called by the WebView's drawing code when a fixed length zoom 294 * animation is occurring. Its purpose is to animate the zooming of the canvas 295 * to the desired scale which was specified in startZoomAnimation(...). 296 * 297 * A fixed length animation begins when startZoomAnimation(...) is called and 298 * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that 299 * interval each time the WebView draws it calls this function which is 300 * responsible for generating the animation. 301 * 302 * Additionally, the WebView can check to see if such an animation is currently 303 * in progress by calling isFixedLengthAnimationInProgress(). 304 */ 305 public void animateZoom(Canvas canvas) { 306 if (mZoomScale == 0) { 307 Log.w(LOGTAG, "A WebView is attempting to perform a fixed length " 308 + "zoom animation when no zoom is in progress"); 309 return; 310 } 311 312 float zoomScale; 313 int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); 314 if (interval < ZOOM_ANIMATION_LENGTH) { 315 float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; 316 zoomScale = 1.0f / (mInvInitialZoomScale 317 + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); 318 mWebView.invalidate(); 319 } else { 320 zoomScale = mZoomScale; 321 // set mZoomScale to be 0 as we have finished animating 322 mZoomScale = 0; 323 mWebView.onFixedLengthZoomAnimationEnd(); 324 } 325 // calculate the intermediate scroll position. Since we need to use 326 // zoomScale, we can't use the WebView's pinLocX/Y functions directly. 327 float scale = zoomScale * mInvInitialZoomScale; 328 int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX); 329 tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth() 330 * zoomScale)) + mWebView.getScrollX(); 331 int titleHeight = mWebView.getTitleHeight(); 332 int ty = Math.round(scale 333 * (mInitialScrollY + mZoomCenterY - titleHeight) 334 - (mZoomCenterY - titleHeight)); 335 ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty 336 - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight() 337 * zoomScale)) + titleHeight) + mWebView.getScrollY(); 338 339 canvas.translate(tx, ty); 340 canvas.scale(zoomScale, zoomScale); 341 } 342 343 public boolean isZoomAnimating() { 344 return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating; 345 } 346 347 public boolean isFixedLengthAnimationInProgress() { 348 return mZoomScale != 0; 349 } 350 351 public void refreshZoomScale(boolean reflowText) { 352 setZoomScale(mActualScale, reflowText, true); 353 } 354 355 public void setZoomScale(float scale, boolean reflowText) { 356 setZoomScale(scale, reflowText, false); 357 } 358 359 private void setZoomScale(float scale, boolean reflowText, boolean force) { 360 final boolean isScaleLessThanMinZoom = scale < mMinZoomScale; 361 scale = computeScaleWithLimits(scale); 362 363 // determine whether or not we are in the zoom overview mode 364 if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) { 365 mInZoomOverview = true; 366 } else { 367 mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale()); 368 } 369 370 if (reflowText) { 371 mTextWrapScale = scale; 372 } 373 374 if (scale != mActualScale || force) { 375 float oldScale = mActualScale; 376 float oldInvScale = mInvActualScale; 377 378 if (scale != mActualScale && !mPinchToZoomAnimating) { 379 mCallbackProxy.onScaleChanged(mActualScale, scale); 380 } 381 382 mActualScale = scale; 383 mInvActualScale = 1 / scale; 384 385 if (!mWebView.drawHistory()) { 386 387 // If history Picture is drawn, don't update scroll. They will 388 // be updated when we get out of that mode. 389 // update our scroll so we don't appear to jump 390 // i.e. keep the center of the doc in the center of the view 391 int oldX = mWebView.getScrollX(); 392 int oldY = mWebView.getScrollY(); 393 float ratio = scale * oldInvScale; 394 float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; 395 float sy = ratio * oldY + (ratio - 1) 396 * (mZoomCenterY - mWebView.getTitleHeight()); 397 398 // Scale all the child views 399 mWebView.mViewManager.scaleAll(); 400 401 // as we don't have animation for scaling, don't do animation 402 // for scrolling, as it causes weird intermediate state 403 int scrollX = mWebView.pinLocX(Math.round(sx)); 404 int scrollY = mWebView.pinLocY(Math.round(sy)); 405 if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) { 406 // the scroll position is adjusted at the beginning of the 407 // zoom animation. But we want to update the WebKit at the 408 // end of the zoom animation. See comments in onScaleEnd(). 409 mWebView.sendOurVisibleRect(); 410 } 411 } 412 413 // if the we need to reflow the text then force the VIEW_SIZE_CHANGED 414 // event to be sent to WebKit 415 mWebView.sendViewSizeZoom(reflowText); 416 } 417 } 418 419 /** 420 * The double tap gesture can result in different behaviors depending on the 421 * content that is tapped. 422 * 423 * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on 424 * the screen. If the plugin is already maximized then zoom the user into 425 * overview mode. 426 * 427 * (2) HTML/OTHER: If the taps occur outside a plugin then the following 428 * heuristic is used. 429 * A. If the current scale is not the same as the text wrap scale and the 430 * layout algorithm specifies the use of NARROW_COLUMNS, then fit to 431 * column by reflowing the text. 432 * B. If the page is not in overview mode then change to overview mode. 433 * C. If the page is in overmode then change to the default scale. 434 */ 435 public void handleDoubleTap(float lastTouchX, float lastTouchY) { 436 WebSettings settings = mWebView.getSettings(); 437 if (settings == null || settings.getUseWideViewPort() == false) { 438 return; 439 } 440 441 setZoomCenter(lastTouchX, lastTouchY); 442 mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX()); 443 mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY()); 444 settings.setDoubleTapToastCount(0); 445 446 // remove the zoom control after double tap 447 dismissZoomPicker(); 448 449 /* 450 * If the double tap was on a plugin then either zoom to maximize the 451 * plugin on the screen or scale to overview mode. 452 */ 453 ViewManager.ChildView plugin = mWebView.mViewManager.hitTest(mAnchorX, mAnchorY); 454 if (plugin != null) { 455 if (mWebView.isPluginFitOnScreen(plugin)) { 456 zoomToOverview(); 457 } else { 458 mWebView.centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height); 459 } 460 return; 461 } 462 463 if (settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS 464 && willScaleTriggerZoom(mTextWrapScale)) { 465 refreshZoomScale(true); 466 } else if (!mInZoomOverview) { 467 zoomToOverview(); 468 } else { 469 zoomToDefaultLevel(); 470 } 471 } 472 473 private void setZoomOverviewWidth(int width) { 474 mZoomOverviewWidth = width; 475 mInvZoomOverviewWidth = 1.0f / width; 476 } 477 478 private float getZoomOverviewScale() { 479 return mWebView.getViewWidth() * mInvZoomOverviewWidth; 480 } 481 482 public boolean isInZoomOverview() { 483 return mInZoomOverview; 484 } 485 486 private void zoomToOverview() { 487 if (!willScaleTriggerZoom(getZoomOverviewScale())) return; 488 489 // Force the titlebar fully reveal in overview mode 490 int scrollY = mWebView.getScrollY(); 491 if (scrollY < mWebView.getTitleHeight()) { 492 mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0); 493 } 494 startZoomAnimation(getZoomOverviewScale(), true); 495 } 496 497 private void zoomToDefaultLevel() { 498 int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); 499 if (left != WebView.NO_LEFTEDGE) { 500 // add a 5pt padding to the left edge. 501 int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5)) 502 - mWebView.getScrollX(); 503 // Re-calculate the zoom center so that the new scroll x will be 504 // on the left edge. 505 if (viewLeft > 0) { 506 mZoomCenterX = viewLeft * mDefaultScale / (mDefaultScale - mActualScale); 507 } else { 508 mWebView.scrollBy(viewLeft, 0); 509 mZoomCenterX = 0; 510 } 511 } 512 startZoomAnimation(mDefaultScale, true); 513 } 514 515 public void updateMultiTouchSupport(Context context) { 516 // check the preconditions 517 assert mWebView.getSettings() != null; 518 519 WebSettings settings = mWebView.getSettings(); 520 mSupportMultiTouch = context.getPackageManager().hasSystemFeature( 521 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) 522 && settings.supportZoom() && settings.getBuiltInZoomControls(); 523 if (mSupportMultiTouch && (mScaleDetector == null)) { 524 mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener()); 525 } else if (!mSupportMultiTouch && (mScaleDetector != null)) { 526 mScaleDetector = null; 527 } 528 } 529 530 public boolean supportsMultiTouchZoom() { 531 return mSupportMultiTouch; 532 } 533 534 /** 535 * Notifies the caller that the ZoomManager is requesting that scale related 536 * updates should not be sent to webkit. This can occur in cases where the 537 * ZoomManager is performing an animation and does not want webkit to update 538 * until the animation is complete. 539 * 540 * @return true if scale related updates should not be sent to webkit and 541 * false otherwise. 542 */ 543 public boolean isPreventingWebkitUpdates() { 544 // currently only animating a multi-touch zoom prevents updates, but 545 // others can add their own conditions to this method if necessary. 546 return mPinchToZoomAnimating; 547 } 548 549 public ScaleGestureDetector getMultiTouchGestureDetector() { 550 return mScaleDetector; 551 } 552 553 private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener { 554 555 public boolean onScaleBegin(ScaleGestureDetector detector) { 556 dismissZoomPicker(); 557 mWebView.mViewManager.startZoom(); 558 mWebView.onPinchToZoomAnimationStart(); 559 return true; 560 } 561 562 public boolean onScale(ScaleGestureDetector detector) { 563 float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f; 564 if (willScaleTriggerZoom(scale)) { 565 mPinchToZoomAnimating = true; 566 // limit the scale change per step 567 if (scale > mActualScale) { 568 scale = Math.min(scale, mActualScale * 1.25f); 569 } else { 570 scale = Math.max(scale, mActualScale * 0.8f); 571 } 572 setZoomCenter(detector.getFocusX(), detector.getFocusY()); 573 setZoomScale(scale, false); 574 mWebView.invalidate(); 575 return true; 576 } 577 return false; 578 } 579 580 public void onScaleEnd(ScaleGestureDetector detector) { 581 if (mPinchToZoomAnimating) { 582 mPinchToZoomAnimating = false; 583 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 584 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 585 // don't reflow when zoom in; when zoom out, do reflow if the 586 // new scale is almost minimum scale; 587 boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale); 588 // force zoom after mPreviewZoomOnly is set to false so that the 589 // new view size will be passed to the WebKit 590 refreshZoomScale(reflowNow); 591 // call invalidate() to draw without zoom filter 592 mWebView.invalidate(); 593 } 594 595 mWebView.mViewManager.endZoom(); 596 mWebView.onPinchToZoomAnimationEnd(detector); 597 } 598 } 599 600 public void onSizeChanged(int w, int h, int ow, int oh) { 601 // reset zoom and anchor to the top left corner of the screen 602 // unless we are already zooming 603 if (!isFixedLengthAnimationInProgress()) { 604 int visibleTitleHeight = mWebView.getVisibleTitleHeight(); 605 mZoomCenterX = 0; 606 mZoomCenterY = visibleTitleHeight; 607 mAnchorX = mWebView.viewToContentX(mWebView.getScrollX()); 608 mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY()); 609 } 610 611 // update mMinZoomScale if the minimum zoom scale is not fixed 612 if (!mMinZoomScaleFixed) { 613 // when change from narrow screen to wide screen, the new viewWidth 614 // can be wider than the old content width. We limit the minimum 615 // scale to 1.0f. The proper minimum scale will be calculated when 616 // the new picture shows up. 617 mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth() 618 / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth() 619 : mZoomOverviewWidth)); 620 // limit the minZoomScale to the initialScale if it is set 621 if (mInitialScale > 0 && mInitialScale < mMinZoomScale) { 622 mMinZoomScale = mInitialScale; 623 } 624 } 625 626 dismissZoomPicker(); 627 628 // onSizeChanged() is called during WebView layout. And any 629 // requestLayout() is blocked during layout. As refreshZoomScale() will 630 // cause its child View to reposition itself through ViewManager's 631 // scaleAll(), we need to post a Runnable to ensure requestLayout(). 632 // Additionally, only update the text wrap scale if the width changed. 633 mWebView.post(new PostScale(w != ow)); 634 } 635 636 private class PostScale implements Runnable { 637 final boolean mUpdateTextWrap; 638 639 public PostScale(boolean updateTextWrap) { 640 mUpdateTextWrap = updateTextWrap; 641 } 642 643 public void run() { 644 if (mWebView.getWebViewCore() != null) { 645 // we always force, in case our height changed, in which case we 646 // still want to send the notification over to webkit. 647 refreshZoomScale(mUpdateTextWrap); 648 // update the zoom buttons as the scale can be changed 649 updateZoomPicker(); 650 } 651 } 652 } 653 654 public void updateZoomRange(WebViewCore.RestoreState restoreState, 655 int viewWidth, int minPrefWidth) { 656 if (restoreState.mMinScale == 0) { 657 if (restoreState.mMobileSite) { 658 if (minPrefWidth > Math.max(0, viewWidth)) { 659 mMinZoomScale = (float) viewWidth / minPrefWidth; 660 mMinZoomScaleFixed = false; 661 } else { 662 mMinZoomScale = restoreState.mDefaultScale; 663 mMinZoomScaleFixed = true; 664 } 665 } else { 666 mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; 667 mMinZoomScaleFixed = false; 668 } 669 } else { 670 mMinZoomScale = restoreState.mMinScale; 671 mMinZoomScaleFixed = true; 672 } 673 if (restoreState.mMaxScale == 0) { 674 mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; 675 } else { 676 mMaxZoomScale = restoreState.mMaxScale; 677 } 678 } 679 680 /** 681 * Updates zoom values when Webkit produces a new picture. This method 682 * should only be called from the UI thread's message handler. 683 */ 684 public void onNewPicture(WebViewCore.DrawData drawData) { 685 final int viewWidth = mWebView.getViewWidth(); 686 687 if (mWebView.getSettings().getUseWideViewPort()) { 688 // limit mZoomOverviewWidth upper bound to 689 // sMaxViewportWidth so that if the page doesn't behave 690 // well, the WebView won't go insane. limit the lower 691 // bound to match the default scale for mobile sites. 692 setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth, 693 Math.max((int) (viewWidth * mInvDefaultScale), 694 Math.max(drawData.mMinPrefWidth, drawData.mViewPoint.x)))); 695 } 696 697 final float zoomOverviewScale = getZoomOverviewScale(); 698 if (!mMinZoomScaleFixed) { 699 mMinZoomScale = zoomOverviewScale; 700 } 701 // fit the content width to the current view. Ignore the rounding error case. 702 if (!mWebView.drawHistory() && mInZoomOverview 703 && Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1) { 704 setZoomScale(zoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale)); 705 } 706 } 707 708 /** 709 * Updates zoom values when Webkit restores a old picture. This method 710 * should only be called from the UI thread's message handler. 711 */ 712 public void restoreZoomState(WebViewCore.DrawData drawData) { 713 // precondition check 714 assert drawData != null; 715 assert drawData.mRestoreState != null; 716 assert mWebView.getSettings() != null; 717 718 WebViewCore.RestoreState restoreState = drawData.mRestoreState; 719 final Point viewSize = drawData.mViewPoint; 720 updateZoomRange(restoreState, viewSize.x, drawData.mMinPrefWidth); 721 722 if (!mWebView.drawHistory()) { 723 final float scale; 724 final boolean reflowText; 725 726 if (mInitialScale > 0) { 727 scale = mInitialScale; 728 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); 729 } else if (restoreState.mViewScale > 0) { 730 mTextWrapScale = restoreState.mTextWrapScale; 731 scale = restoreState.mViewScale; 732 reflowText = false; 733 } else { 734 WebSettings settings = mWebView.getSettings(); 735 if (settings.getUseWideViewPort() && settings.getLoadWithOverviewMode()) { 736 scale = (float) mWebView.getViewWidth() / WebView.DEFAULT_VIEWPORT_WIDTH; 737 } else { 738 scale = restoreState.mTextWrapScale; 739 } 740 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); 741 } 742 setZoomScale(scale, reflowText); 743 744 // update the zoom buttons as the scale can be changed 745 updateZoomPicker(); 746 } 747 } 748 749 private ZoomControlBase getCurrentZoomControl() { 750 if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) { 751 if (mWebView.getSettings().getBuiltInZoomControls()) { 752 if (mEmbeddedZoomControl == null) { 753 mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView); 754 } 755 return mEmbeddedZoomControl; 756 } else { 757 if (mExternalZoomControl == null) { 758 mExternalZoomControl = new ZoomControlExternal(mWebView); 759 } 760 return mExternalZoomControl; 761 } 762 } 763 return null; 764 } 765 766 public void invokeZoomPicker() { 767 ZoomControlBase control = getCurrentZoomControl(); 768 if (control != null) { 769 control.show(); 770 } 771 } 772 773 public void dismissZoomPicker() { 774 ZoomControlBase control = getCurrentZoomControl(); 775 if (control != null) { 776 control.hide(); 777 } 778 } 779 780 public boolean isZoomPickerVisible() { 781 ZoomControlBase control = getCurrentZoomControl(); 782 return (control != null) ? control.isVisible() : false; 783 } 784 785 public void updateZoomPicker() { 786 ZoomControlBase control = getCurrentZoomControl(); 787 if (control != null) { 788 control.update(); 789 } 790 } 791 792 /** 793 * The embedded zoom control intercepts touch events and automatically stays 794 * visible. The external control needs to constantly refresh its internal 795 * timer to stay visible. 796 */ 797 public void keepZoomPickerVisible() { 798 ZoomControlBase control = getCurrentZoomControl(); 799 if (control != null && control == mExternalZoomControl) { 800 control.show(); 801 } 802 } 803 804 public View getExternalZoomPicker() { 805 ZoomControlBase control = getCurrentZoomControl(); 806 if (control != null && control == mExternalZoomControl) { 807 return mExternalZoomControl.getControls(); 808 } else { 809 return null; 810 } 811 } 812} 813