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