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