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