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