ZoomManager.java revision c55886aee84034f7fcf4431fdbeeaff1a9eafbd9
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.Bundle; 24import android.os.SystemClock; 25import android.util.Log; 26import android.view.ScaleGestureDetector; 27import android.view.View; 28 29/** 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 */ 47class ZoomManager { 48 49 static final String LOGTAG = "webviewZoom"; 50 51 private final WebView mWebView; 52 private final CallbackProxy mCallbackProxy; 53 54 // Widgets responsible for the on-screen zoom functions of the WebView. 55 private ZoomControlEmbedded mEmbeddedZoomControl; 56 private ZoomControlExternal mExternalZoomControl; 57 58 /* 59 * 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 default reading level scale is used. 62 */ 63 private static final float DEFAULT_READING_LEVEL_SCALE = 1.5f; 64 65 /* 66 * 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; 71 72 // The default scale limits, which are dependent on the display density. 73 private float mDefaultMaxZoomScale; 74 private float mDefaultMinZoomScale; 75 76 // The actual scale limits, which can be set through a webpage's viewport 77 // meta-tag. 78 private float mMaxZoomScale; 79 private float mMinZoomScale; 80 81 // Locks the minimum ZoomScale to the value currently set in mMinZoomScale. 82 private boolean mMinZoomScaleFixed = true; 83 84 /* 85 * 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 /* 94 * 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 */ 101 private boolean mInZoomOverview = false; 102 private int mZoomOverviewWidth; 103 private float mInvZoomOverviewWidth; 104 105 /* 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; 112 113 /* 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 124 // The scale factor that is used to determine the column width for text 125 private float mTextWrapScale; 126 127 /* 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 */ 133 private float mDefaultScale; 134 private float mInvDefaultScale; 135 136 // the current computed zoom scale and its inverse. 137 private float mActualScale; 138 private float mInvActualScale; 139 140 /* 141 * 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 150 private static float MINIMUM_SCALE_INCREMENT = 0.01f; 151 152 /* 153 * 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; 164 165 private static final int ZOOM_ANIMATION_LENGTH = 500; 166 167 // whether support multi-touch 168 private boolean mSupportMultiTouch; 169 170 /** 171 * True if we have a touch panel capable of detecting smooth pan/scale at the same time 172 */ 173 private boolean mAllowPanAndScale; 174 175 // use the framework's ScaleGestureDetector to handle multi-touch 176 private ScaleGestureDetector mScaleDetector; 177 private boolean mPinchToZoomAnimating = false; 178 179 public ZoomManager(WebView webView, CallbackProxy callbackProxy) { 180 mWebView = webView; 181 mCallbackProxy = callbackProxy; 182 183 /* 184 * Ideally mZoomOverviewWidth should be mContentWidth. But sites like 185 * ESPN and Engadget always have wider mContentWidth no matter what the 186 * viewport size is. 187 */ 188 setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH); 189 } 190 191 /** 192 * Initialize both the default and actual zoom scale to the given density. 193 * 194 * @param density The logical density of the display. This is a scaling factor 195 * for the Density Independent Pixel unit, where one DIP is one pixel on an 196 * approximately 160 dpi screen (see android.util.DisplayMetrics.density). 197 */ 198 public void init(float density) { 199 assert density > 0; 200 201 setDefaultZoomScale(density); 202 mActualScale = density; 203 mInvActualScale = 1 / density; 204 mTextWrapScale = density; 205 } 206 207 /** 208 * Update the default zoom scale using the given density. It will also reset 209 * the current min and max zoom scales to the default boundaries as well as 210 * ensure that the actual scale falls within those boundaries. 211 * 212 * @param density The logical density of the display. This is a scaling factor 213 * for the Density Independent Pixel unit, where one DIP is one pixel on an 214 * approximately 160 dpi screen (see android.util.DisplayMetrics.density). 215 */ 216 public void updateDefaultZoomDensity(float density) { 217 assert density > 0; 218 219 if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) { 220 // set the new default density 221 setDefaultZoomScale(density); 222 // adjust the scale if it falls outside the new zoom bounds 223 setZoomScale(mActualScale, true); 224 } 225 } 226 227 private void setDefaultZoomScale(float defaultScale) { 228 mDefaultScale = defaultScale; 229 mInvDefaultScale = 1 / defaultScale; 230 mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR; 231 mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR; 232 mMaxZoomScale = mDefaultMaxZoomScale; 233 mMinZoomScale = mDefaultMinZoomScale; 234 } 235 236 public final float getScale() { 237 return mActualScale; 238 } 239 240 public final float getInvScale() { 241 return mInvActualScale; 242 } 243 244 public final float getTextWrapScale() { 245 return mTextWrapScale; 246 } 247 248 public final float getMaxZoomScale() { 249 return mMaxZoomScale; 250 } 251 252 public final float getMinZoomScale() { 253 return mMinZoomScale; 254 } 255 256 public final float getDefaultScale() { 257 return mDefaultScale; 258 } 259 260 public final float getReadingLevelScale() { 261 // The reading scale is at least 0.5f apart from the overview scale. 262 final float MIN_SCALE_DIFF = 0.5f; 263 final float zoomOverviewScale = getZoomOverviewScale(); 264 if (zoomOverviewScale > DEFAULT_READING_LEVEL_SCALE) { 265 return Math.min(DEFAULT_READING_LEVEL_SCALE, 266 zoomOverviewScale - MIN_SCALE_DIFF); 267 } 268 return Math.max(zoomOverviewScale + MIN_SCALE_DIFF, 269 DEFAULT_READING_LEVEL_SCALE); 270 } 271 272 public final float getInvDefaultScale() { 273 return mInvDefaultScale; 274 } 275 276 public final float getDefaultMaxZoomScale() { 277 return mDefaultMaxZoomScale; 278 } 279 280 public final float getDefaultMinZoomScale() { 281 return mDefaultMinZoomScale; 282 } 283 284 public final int getDocumentAnchorX() { 285 return mAnchorX; 286 } 287 288 public final int getDocumentAnchorY() { 289 return mAnchorY; 290 } 291 292 public final void clearDocumentAnchor() { 293 mAnchorX = mAnchorY = 0; 294 } 295 296 public final void setZoomCenter(float x, float y) { 297 mZoomCenterX = x; 298 mZoomCenterY = y; 299 } 300 301 public final void setInitialScaleInPercent(int scaleInPercent) { 302 mInitialScale = scaleInPercent * 0.01f; 303 } 304 305 public final float computeScaleWithLimits(float scale) { 306 if (scale < mMinZoomScale) { 307 scale = mMinZoomScale; 308 } else if (scale > mMaxZoomScale) { 309 scale = mMaxZoomScale; 310 } 311 return scale; 312 } 313 314 public final boolean isZoomScaleFixed() { 315 return mMinZoomScale >= mMaxZoomScale; 316 } 317 318 public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) { 319 return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT; 320 } 321 322 public boolean willScaleTriggerZoom(float scale) { 323 return exceedsMinScaleIncrement(scale, mActualScale); 324 } 325 326 public final boolean canZoomIn() { 327 return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT; 328 } 329 330 public final boolean canZoomOut() { 331 return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT; 332 } 333 334 public boolean zoomIn() { 335 return zoom(1.25f); 336 } 337 338 public boolean zoomOut() { 339 return zoom(0.8f); 340 } 341 342 // returns TRUE if zoom out succeeds and FALSE if no zoom changes. 343 private boolean zoom(float zoomMultiplier) { 344 // TODO: alternatively we can disallow this during draw history mode 345 mWebView.switchOutDrawHistory(); 346 // Center zooming to the center of the screen. 347 mZoomCenterX = mWebView.getViewWidth() * .5f; 348 mZoomCenterY = mWebView.getViewHeight() * .5f; 349 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 350 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 351 return startZoomAnimation(mActualScale * zoomMultiplier, 352 !mWebView.getSettings().getUseFixedViewport()); 353 } 354 355 /** 356 * Initiates an animated zoom of the WebView. 357 * 358 * @return true if the new scale triggered an animation and false otherwise. 359 */ 360 public boolean startZoomAnimation(float scale, boolean reflowText) { 361 float oldScale = mActualScale; 362 mInitialScrollX = mWebView.getScrollX(); 363 mInitialScrollY = mWebView.getScrollY(); 364 365 // snap to reading level scale if it is close 366 if (!exceedsMinScaleIncrement(scale, getReadingLevelScale())) { 367 scale = getReadingLevelScale(); 368 } 369 370 setZoomScale(scale, reflowText); 371 372 if (oldScale != mActualScale) { 373 // use mZoomPickerScale to see zoom preview first 374 mZoomStart = SystemClock.uptimeMillis(); 375 mInvInitialZoomScale = 1.0f / oldScale; 376 mInvFinalZoomScale = 1.0f / mActualScale; 377 mZoomScale = mActualScale; 378 mWebView.onFixedLengthZoomAnimationStart(); 379 mWebView.invalidate(); 380 return true; 381 } else { 382 return false; 383 } 384 } 385 386 /** 387 * This method is called by the WebView's drawing code when a fixed length zoom 388 * animation is occurring. Its purpose is to animate the zooming of the canvas 389 * to the desired scale which was specified in startZoomAnimation(...). 390 * 391 * A fixed length animation begins when startZoomAnimation(...) is called and 392 * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that 393 * interval each time the WebView draws it calls this function which is 394 * responsible for generating the animation. 395 * 396 * Additionally, the WebView can check to see if such an animation is currently 397 * in progress by calling isFixedLengthAnimationInProgress(). 398 */ 399 public void animateZoom(Canvas canvas) { 400 if (mZoomScale == 0) { 401 Log.w(LOGTAG, "A WebView is attempting to perform a fixed length " 402 + "zoom animation when no zoom is in progress"); 403 return; 404 } 405 406 float zoomScale; 407 int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); 408 if (interval < ZOOM_ANIMATION_LENGTH) { 409 float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; 410 zoomScale = 1.0f / (mInvInitialZoomScale 411 + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); 412 mWebView.invalidate(); 413 } else { 414 zoomScale = mZoomScale; 415 // set mZoomScale to be 0 as we have finished animating 416 mZoomScale = 0; 417 mWebView.onFixedLengthZoomAnimationEnd(); 418 } 419 // calculate the intermediate scroll position. Since we need to use 420 // zoomScale, we can't use the WebView's pinLocX/Y functions directly. 421 float scale = zoomScale * mInvInitialZoomScale; 422 int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX); 423 tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth() 424 * zoomScale)) + mWebView.getScrollX(); 425 int titleHeight = mWebView.getTitleHeight(); 426 int ty = Math.round(scale 427 * (mInitialScrollY + mZoomCenterY - titleHeight) 428 - (mZoomCenterY - titleHeight)); 429 ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty 430 - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight() 431 * zoomScale)) + titleHeight) + mWebView.getScrollY(); 432 433 canvas.translate(tx, ty); 434 canvas.scale(zoomScale, zoomScale); 435 } 436 437 public boolean isZoomAnimating() { 438 return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating; 439 } 440 441 public boolean isFixedLengthAnimationInProgress() { 442 return mZoomScale != 0; 443 } 444 445 public void refreshZoomScale(boolean reflowText) { 446 setZoomScale(mActualScale, reflowText, true); 447 } 448 449 public void setZoomScale(float scale, boolean reflowText) { 450 setZoomScale(scale, reflowText, false); 451 } 452 453 private void setZoomScale(float scale, boolean reflowText, boolean force) { 454 final boolean isScaleLessThanMinZoom = scale < mMinZoomScale; 455 scale = computeScaleWithLimits(scale); 456 457 // determine whether or not we are in the zoom overview mode 458 if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) { 459 mInZoomOverview = true; 460 } else { 461 mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale()); 462 } 463 464 if (reflowText && !mWebView.getSettings().getUseFixedViewport()) { 465 mTextWrapScale = scale; 466 } 467 468 if (scale != mActualScale || force) { 469 float oldScale = mActualScale; 470 float oldInvScale = mInvActualScale; 471 472 if (scale != mActualScale && !mPinchToZoomAnimating) { 473 mCallbackProxy.onScaleChanged(mActualScale, scale); 474 } 475 476 mActualScale = scale; 477 mInvActualScale = 1 / scale; 478 479 if (!mWebView.drawHistory()) { 480 481 // If history Picture is drawn, don't update scroll. They will 482 // be updated when we get out of that mode. 483 // update our scroll so we don't appear to jump 484 // i.e. keep the center of the doc in the center of the view 485 int oldX = mWebView.getScrollX(); 486 int oldY = mWebView.getScrollY(); 487 float ratio = scale * oldInvScale; 488 float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; 489 float sy = ratio * oldY + (ratio - 1) 490 * (mZoomCenterY - mWebView.getTitleHeight()); 491 492 // Scale all the child views 493 mWebView.mViewManager.scaleAll(); 494 495 // as we don't have animation for scaling, don't do animation 496 // for scrolling, as it causes weird intermediate state 497 int scrollX = mWebView.pinLocX(Math.round(sx)); 498 int scrollY = mWebView.pinLocY(Math.round(sy)); 499 if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) { 500 // the scroll position is adjusted at the beginning of the 501 // zoom animation. But we want to update the WebKit at the 502 // end of the zoom animation. See comments in onScaleEnd(). 503 mWebView.sendOurVisibleRect(); 504 } 505 } 506 507 // if the we need to reflow the text then force the VIEW_SIZE_CHANGED 508 // event to be sent to WebKit 509 mWebView.sendViewSizeZoom(reflowText); 510 } 511 } 512 513 /** 514 * The double tap gesture can result in different behaviors depending on the 515 * content that is tapped. 516 * 517 * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on 518 * the screen. If the plugin is already maximized then zoom the user into 519 * overview mode. 520 * 521 * (2) HTML/OTHER: If the taps occur outside a plugin then the following 522 * heuristic is used. 523 * A. If the current text wrap scale differs from newly calculated and the 524 * layout algorithm specifies the use of NARROW_COLUMNS, then fit to 525 * column by reflowing the text. 526 * B. If the page is not in overview mode then change to overview mode. 527 * C. If the page is in overmode then change to the default scale. 528 */ 529 public void handleDoubleTap(float lastTouchX, float lastTouchY) { 530 WebSettings settings = mWebView.getSettings(); 531 if (settings == null || settings.getUseWideViewPort() == false) { 532 return; 533 } 534 535 setZoomCenter(lastTouchX, lastTouchY); 536 mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX()); 537 mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY()); 538 settings.setDoubleTapToastCount(0); 539 540 // remove the zoom control after double tap 541 dismissZoomPicker(); 542 543 /* 544 * If the double tap was on a plugin then either zoom to maximize the 545 * plugin on the screen or scale to overview mode. 546 */ 547 ViewManager.ChildView plugin = mWebView.mViewManager.hitTest(mAnchorX, mAnchorY); 548 if (plugin != null) { 549 if (mWebView.isPluginFitOnScreen(plugin)) { 550 zoomToOverview(); 551 } else { 552 mWebView.centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height); 553 } 554 return; 555 } 556 557 final float newTextWrapScale; 558 if (settings.getUseFixedViewport()) { 559 newTextWrapScale = Math.max(mActualScale, getReadingLevelScale()); 560 } else { 561 newTextWrapScale = mActualScale; 562 } 563 if (settings.isNarrowColumnLayout() 564 && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale)) { 565 mTextWrapScale = newTextWrapScale; 566 refreshZoomScale(true); 567 } else if (!mInZoomOverview) { 568 zoomToOverview(); 569 } else { 570 zoomToReadingLevel(); 571 } 572 } 573 574 private void setZoomOverviewWidth(int width) { 575 mZoomOverviewWidth = width; 576 mInvZoomOverviewWidth = 1.0f / width; 577 } 578 579 private float getZoomOverviewScale() { 580 return mWebView.getViewWidth() * mInvZoomOverviewWidth; 581 } 582 583 public boolean isInZoomOverview() { 584 return mInZoomOverview; 585 } 586 587 private void zoomToOverview() { 588 if (!willScaleTriggerZoom(getZoomOverviewScale())) return; 589 590 // Force the titlebar fully reveal in overview mode 591 int scrollY = mWebView.getScrollY(); 592 if (scrollY < mWebView.getTitleHeight()) { 593 mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0); 594 } 595 startZoomAnimation(getZoomOverviewScale(), 596 !mWebView.getSettings().getUseFixedViewport()); 597 } 598 599 private void zoomToReadingLevel() { 600 final float readingScale = getReadingLevelScale(); 601 int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); 602 if (left != WebView.NO_LEFTEDGE) { 603 // add a 5pt padding to the left edge. 604 int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5)) 605 - mWebView.getScrollX(); 606 // Re-calculate the zoom center so that the new scroll x will be 607 // on the left edge. 608 if (viewLeft > 0) { 609 mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale); 610 } else { 611 mWebView.scrollBy(viewLeft, 0); 612 mZoomCenterX = 0; 613 } 614 } 615 startZoomAnimation(readingScale, 616 !mWebView.getSettings().getUseFixedViewport()); 617 } 618 619 public void updateMultiTouchSupport(Context context) { 620 // check the preconditions 621 assert mWebView.getSettings() != null; 622 623 final WebSettings settings = mWebView.getSettings(); 624 final PackageManager pm = context.getPackageManager(); 625 mSupportMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) 626 && settings.supportZoom() && settings.getBuiltInZoomControls(); 627 mAllowPanAndScale = pm.hasSystemFeature( 628 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 629 if (mSupportMultiTouch && (mScaleDetector == null)) { 630 mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener()); 631 } else if (!mSupportMultiTouch && (mScaleDetector != null)) { 632 mScaleDetector = null; 633 } 634 } 635 636 public boolean supportsMultiTouchZoom() { 637 return mSupportMultiTouch; 638 } 639 640 public boolean supportsPanDuringZoom() { 641 return mAllowPanAndScale; 642 } 643 644 /** 645 * Notifies the caller that the ZoomManager is requesting that scale related 646 * updates should not be sent to webkit. This can occur in cases where the 647 * ZoomManager is performing an animation and does not want webkit to update 648 * until the animation is complete. 649 * 650 * @return true if scale related updates should not be sent to webkit and 651 * false otherwise. 652 */ 653 public boolean isPreventingWebkitUpdates() { 654 // currently only animating a multi-touch zoom prevents updates, but 655 // others can add their own conditions to this method if necessary. 656 return mPinchToZoomAnimating; 657 } 658 659 public ScaleGestureDetector getMultiTouchGestureDetector() { 660 return mScaleDetector; 661 } 662 663 private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener { 664 665 public boolean onScaleBegin(ScaleGestureDetector detector) { 666 dismissZoomPicker(); 667 mWebView.mViewManager.startZoom(); 668 mWebView.onPinchToZoomAnimationStart(); 669 return true; 670 } 671 672 public boolean onScale(ScaleGestureDetector detector) { 673 float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f; 674 if (willScaleTriggerZoom(scale)) { 675 mPinchToZoomAnimating = true; 676 // limit the scale change per step 677 if (scale > mActualScale) { 678 scale = Math.min(scale, mActualScale * 1.25f); 679 } else { 680 scale = Math.max(scale, mActualScale * 0.8f); 681 } 682 setZoomCenter(detector.getFocusX(), detector.getFocusY()); 683 setZoomScale(scale, false); 684 mWebView.invalidate(); 685 return true; 686 } 687 return false; 688 } 689 690 public void onScaleEnd(ScaleGestureDetector detector) { 691 if (mPinchToZoomAnimating) { 692 mPinchToZoomAnimating = false; 693 mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); 694 mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); 695 // don't reflow when zoom in; when zoom out, do reflow if the 696 // new scale is almost minimum scale. 697 boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale); 698 // force zoom after mPreviewZoomOnly is set to false so that the 699 // new view size will be passed to the WebKit 700 refreshZoomScale(reflowNow && 701 !mWebView.getSettings().getUseFixedViewport()); 702 // call invalidate() to draw without zoom filter 703 mWebView.invalidate(); 704 } 705 706 mWebView.mViewManager.endZoom(); 707 mWebView.onPinchToZoomAnimationEnd(detector); 708 } 709 } 710 711 public void onSizeChanged(int w, int h, int ow, int oh) { 712 // reset zoom and anchor to the top left corner of the screen 713 // unless we are already zooming 714 if (!isFixedLengthAnimationInProgress()) { 715 int visibleTitleHeight = mWebView.getVisibleTitleHeight(); 716 mZoomCenterX = 0; 717 mZoomCenterY = visibleTitleHeight; 718 mAnchorX = mWebView.viewToContentX(mWebView.getScrollX()); 719 mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY()); 720 } 721 722 // update mMinZoomScale if the minimum zoom scale is not fixed 723 if (!mMinZoomScaleFixed) { 724 // when change from narrow screen to wide screen, the new viewWidth 725 // can be wider than the old content width. We limit the minimum 726 // scale to 1.0f. The proper minimum scale will be calculated when 727 // the new picture shows up. 728 mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth() 729 / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth() 730 : mZoomOverviewWidth)); 731 // limit the minZoomScale to the initialScale if it is set 732 if (mInitialScale > 0 && mInitialScale < mMinZoomScale) { 733 mMinZoomScale = mInitialScale; 734 } 735 } 736 737 dismissZoomPicker(); 738 739 // onSizeChanged() is called during WebView layout. And any 740 // requestLayout() is blocked during layout. As refreshZoomScale() will 741 // cause its child View to reposition itself through ViewManager's 742 // scaleAll(), we need to post a Runnable to ensure requestLayout(). 743 // Additionally, only update the text wrap scale if the width changed. 744 mWebView.post(new PostScale(w != ow && 745 !mWebView.getSettings().getUseFixedViewport())); 746 } 747 748 private class PostScale implements Runnable { 749 final boolean mUpdateTextWrap; 750 751 public PostScale(boolean updateTextWrap) { 752 mUpdateTextWrap = updateTextWrap; 753 } 754 755 public void run() { 756 if (mWebView.getWebViewCore() != null) { 757 // we always force, in case our height changed, in which case we 758 // still want to send the notification over to webkit. 759 setZoomScale(Math.max(mActualScale, getZoomOverviewScale()), 760 mUpdateTextWrap, true); 761 // update the zoom buttons as the scale can be changed 762 updateZoomPicker(); 763 } 764 } 765 } 766 767 public void updateZoomRange(WebViewCore.ViewState viewState, 768 int viewWidth, int minPrefWidth) { 769 if (viewState.mMinScale == 0) { 770 if (viewState.mMobileSite) { 771 if (minPrefWidth > Math.max(0, viewWidth)) { 772 mMinZoomScale = (float) viewWidth / minPrefWidth; 773 mMinZoomScaleFixed = false; 774 } else { 775 mMinZoomScale = viewState.mDefaultScale; 776 mMinZoomScaleFixed = true; 777 } 778 } else { 779 mMinZoomScale = mDefaultMinZoomScale; 780 mMinZoomScaleFixed = false; 781 } 782 } else { 783 mMinZoomScale = viewState.mMinScale; 784 mMinZoomScaleFixed = true; 785 } 786 if (viewState.mMaxScale == 0) { 787 mMaxZoomScale = mDefaultMaxZoomScale; 788 } else { 789 mMaxZoomScale = viewState.mMaxScale; 790 } 791 } 792 793 /** 794 * Updates zoom values when Webkit produces a new picture. This method 795 * should only be called from the UI thread's message handler. 796 */ 797 public void onNewPicture(WebViewCore.DrawData drawData) { 798 final int viewWidth = mWebView.getViewWidth(); 799 800 if (mWebView.getSettings().getUseWideViewPort()) { 801 if (!mWebView.getSettings().getUseFixedViewport()) { 802 // limit mZoomOverviewWidth upper bound to 803 // sMaxViewportWidth so that if the page doesn't behave 804 // well, the WebView won't go insane. limit the lower 805 // bound to match the default scale for mobile sites. 806 setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth, 807 Math.max((int) (viewWidth * mInvDefaultScale), 808 Math.max(drawData.mMinPrefWidth, drawData.mViewSize.x)))); 809 } else { 810 final int contentWidth = drawData.mContentSize.x; 811 setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth, contentWidth)); 812 } 813 } 814 815 final float zoomOverviewScale = getZoomOverviewScale(); 816 if (!mMinZoomScaleFixed) { 817 mMinZoomScale = zoomOverviewScale; 818 } 819 // fit the content width to the current view. Ignore the rounding error case. 820 if (!mWebView.drawHistory() && (mInitialZoomOverview || (mInZoomOverview 821 && Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1))) { 822 mInitialZoomOverview = false; 823 setZoomScale(zoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale) && 824 !mWebView.getSettings().getUseFixedViewport()); 825 } 826 } 827 828 /** 829 * Updates zoom values after Webkit completes the initial page layout. It 830 * is called when visiting a page for the first time as well as when the 831 * user navigates back to a page (in which case we may need to restore the 832 * zoom levels to the state they were when you left the page). This method 833 * should only be called from the UI thread's message handler. 834 */ 835 public void onFirstLayout(WebViewCore.DrawData drawData) { 836 // precondition check 837 assert drawData != null; 838 assert drawData.mViewState != null; 839 assert mWebView.getSettings() != null; 840 841 WebViewCore.ViewState viewState = drawData.mViewState; 842 final Point viewSize = drawData.mViewSize; 843 updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth); 844 if (mWebView.getSettings().getUseWideViewPort() && 845 mWebView.getSettings().getUseFixedViewport()) { 846 final int contentWidth = drawData.mContentSize.x; 847 setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth, contentWidth)); 848 } 849 850 if (!mWebView.drawHistory()) { 851 float scale; 852 final boolean reflowText; 853 WebSettings settings = mWebView.getSettings(); 854 855 if (mInitialScale > 0) { 856 scale = mInitialScale; 857 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); 858 } else if (viewState.mViewScale > 0) { 859 mTextWrapScale = viewState.mTextWrapScale; 860 scale = viewState.mViewScale; 861 reflowText = false; 862 } else { 863 scale = getZoomOverviewScale(); 864 if (settings.getUseWideViewPort() 865 && settings.getLoadWithOverviewMode()) { 866 mInitialZoomOverview = true; 867 } else { 868 scale = Math.max(viewState.mTextWrapScale, scale); 869 mInitialZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale()); 870 } 871 if (settings.isNarrowColumnLayout() && settings.getUseFixedViewport()) { 872 // When first layout, reflow using the reading level scale to avoid 873 // reflow when double tapped. 874 mTextWrapScale = getReadingLevelScale(); 875 } 876 reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); 877 } 878 setZoomScale(scale, reflowText); 879 880 // update the zoom buttons as the scale can be changed 881 updateZoomPicker(); 882 } 883 } 884 885 public void saveZoomState(Bundle b) { 886 b.putFloat("scale", mActualScale); 887 b.putFloat("textwrapScale", mTextWrapScale); 888 b.putBoolean("overview", mInZoomOverview); 889 } 890 891 public void restoreZoomState(Bundle b) { 892 // as getWidth() / getHeight() of the view are not available yet, set up 893 // mActualScale, so that when onSizeChanged() is called, the rest will 894 // be set correctly 895 mActualScale = b.getFloat("scale", 1.0f); 896 mInvActualScale = 1 / mActualScale; 897 mTextWrapScale = b.getFloat("textwrapScale", mActualScale); 898 mInZoomOverview = b.getBoolean("overview"); 899 } 900 901 private ZoomControlBase getCurrentZoomControl() { 902 if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) { 903 if (mWebView.getSettings().getBuiltInZoomControls()) { 904 if ((mEmbeddedZoomControl == null) 905 && mWebView.getSettings().getDisplayZoomControls()) { 906 mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView); 907 } 908 return mEmbeddedZoomControl; 909 } else { 910 if (mExternalZoomControl == null) { 911 mExternalZoomControl = new ZoomControlExternal(mWebView); 912 } 913 return mExternalZoomControl; 914 } 915 } 916 return null; 917 } 918 919 public void invokeZoomPicker() { 920 ZoomControlBase control = getCurrentZoomControl(); 921 if (control != null) { 922 control.show(); 923 } 924 } 925 926 public void dismissZoomPicker() { 927 ZoomControlBase control = getCurrentZoomControl(); 928 if (control != null) { 929 control.hide(); 930 } 931 } 932 933 public boolean isZoomPickerVisible() { 934 ZoomControlBase control = getCurrentZoomControl(); 935 return (control != null) ? control.isVisible() : false; 936 } 937 938 public void updateZoomPicker() { 939 ZoomControlBase control = getCurrentZoomControl(); 940 if (control != null) { 941 control.update(); 942 } 943 } 944 945 /** 946 * The embedded zoom control intercepts touch events and automatically stays 947 * visible. The external control needs to constantly refresh its internal 948 * timer to stay visible. 949 */ 950 public void keepZoomPickerVisible() { 951 ZoomControlBase control = getCurrentZoomControl(); 952 if (control != null && control == mExternalZoomControl) { 953 control.show(); 954 } 955 } 956 957 public View getExternalZoomPicker() { 958 ZoomControlBase control = getCurrentZoomControl(); 959 if (control != null && control == mExternalZoomControl) { 960 return mExternalZoomControl.getControls(); 961 } else { 962 return null; 963 } 964 } 965} 966