root/android_webview/java/src/org/chromium/android_webview/AwLayoutSizer.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. requestLayout
  2. setMeasuredDimension
  3. setFixedLayoutSize
  4. isLayoutParamsHeightWrapContent
  5. setDelegate
  6. setDIPScale
  7. freezeLayoutRequests
  8. unfreezeLayoutRequests
  9. onContentSizeChanged
  10. onPageScaleChanged
  11. doUpdate
  12. onMeasure
  13. onSizeChanged
  14. onLayoutChange
  15. setFixedLayoutSize
  16. updateFixedLayoutSize

// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.view.View;
import android.view.View.MeasureSpec;

/**
 * Helper methods used to manage the layout of the View that contains AwContents.
 */
public class AwLayoutSizer {
    public static final int FIXED_LAYOUT_HEIGHT = 0;

    // These are used to prevent a re-layout if the content size changes within a dimension that is
    // fixed by the view system.
    private boolean mWidthMeasurementIsFixed;
    private boolean mHeightMeasurementIsFixed;

    // Size of the rendered content, as reported by native.
    private int mContentHeightCss;
    private int mContentWidthCss;

    // Page scale factor. This is set to zero initially so that we don't attempt to do a layout if
    // we get the content size change notification first and a page scale change second.
    private float mPageScaleFactor = 0.0f;
    // The page scale factor that was used in the most recent onMeasure call.
    private float mLastMeasuredPageScaleFactor = 0.0f;

    // Whether to postpone layout requests.
    private boolean mFreezeLayoutRequests;
    // Did we try to request a layout since the last time mPostponeLayoutRequests was set to true.
    private boolean mFrozenLayoutRequestPending;

    private double mDIPScale;

    // Was our height larger than the AT_MOST constraint the last time onMeasure was called?
    private boolean mHeightMeasurementLimited;
    // If mHeightMeasurementLimited is true then this contains the height limit.
    private int mHeightMeasurementLimit;

    // The most recent width and height seen in onSizeChanged.
    private int mLastWidth;
    private int mLastHeight;

    // Used to prevent sending multiple setFixedLayoutSize notifications with the same values.
    private int mLastSentFixedLayoutSizeWidth = -1;
    private int mLastSentFixedLayoutSizeHeight = -1;

    // Callback object for interacting with the View.
    private Delegate mDelegate;

    public interface Delegate {
        void requestLayout();
        void setMeasuredDimension(int measuredWidth, int measuredHeight);
        void setFixedLayoutSize(int widthDip, int heightDip);
        boolean isLayoutParamsHeightWrapContent();
    }

    /**
     * Default constructor. Note: both setDelegate and setDIPScale must be called before the class
     * is ready for use.
     */
    public AwLayoutSizer() {
    }

    public void setDelegate(Delegate delegate) {
        mDelegate = delegate;
    }

    public void setDIPScale(double dipScale) {
        mDIPScale = dipScale;
    }

    /**
     * Postpone requesting layouts till unfreezeLayoutRequests is called.
     */
    public void freezeLayoutRequests() {
        mFreezeLayoutRequests = true;
        mFrozenLayoutRequestPending = false;
    }

    /**
     * Stop postponing layout requests and request layout if such a request would have been made
     * had the freezeLayoutRequests method not been called before.
     */
    public void unfreezeLayoutRequests() {
        mFreezeLayoutRequests = false;
        if (mFrozenLayoutRequestPending) {
            mFrozenLayoutRequestPending = false;
            mDelegate.requestLayout();
        }
    }

    /**
     * Update the contents size.
     * This should be called whenever the content size changes (due to DOM manipulation or page
     * load, for example).
     * The width and height should be in CSS pixels.
     */
    public void onContentSizeChanged(int widthCss, int heightCss) {
        doUpdate(widthCss, heightCss, mPageScaleFactor);
    }

    /**
     * Update the contents page scale.
     * This should be called whenever the content page scale factor changes (due to pinch zoom, for
     * example).
     */
    public void onPageScaleChanged(float pageScaleFactor) {
        doUpdate(mContentWidthCss, mContentHeightCss, pageScaleFactor);
    }

    private void doUpdate(int widthCss, int heightCss, float pageScaleFactor) {
        // We want to request layout only if the size or scale change, however if any of the
        // measurements are 'fixed', then changing the underlying size won't have any effect, so we
        // ignore changes to dimensions that are 'fixed'.
        final int heightPix = (int) (heightCss * mPageScaleFactor * mDIPScale);
        boolean pageScaleChanged = mPageScaleFactor != pageScaleFactor;
        boolean contentHeightChangeMeaningful = !mHeightMeasurementIsFixed &&
            (!mHeightMeasurementLimited || heightPix < mHeightMeasurementLimit);
        boolean pageScaleChangeMeaningful =
            !mWidthMeasurementIsFixed || contentHeightChangeMeaningful;
        boolean layoutNeeded = (mContentWidthCss != widthCss && !mWidthMeasurementIsFixed) ||
            (mContentHeightCss != heightCss && contentHeightChangeMeaningful) ||
            (pageScaleChanged && pageScaleChangeMeaningful);

        mContentWidthCss = widthCss;
        mContentHeightCss = heightCss;
        mPageScaleFactor = pageScaleFactor;

        if (layoutNeeded) {
            if (mFreezeLayoutRequests) {
                mFrozenLayoutRequestPending = true;
            } else {
                mDelegate.requestLayout();
            }
        } else if (pageScaleChanged && mLastWidth != 0) {
            // Because the fixed layout size is directly impacted by the pageScaleFactor we must
            // update it even if the physical size of the view doesn't change.
            updateFixedLayoutSize(mLastWidth, mLastHeight, mPageScaleFactor);
        }
    }

    /**
     * Calculate the size of the view.
     * This is designed to be used to implement the android.view.View#onMeasure() method.
     */
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int contentHeightPix = (int) (mContentHeightCss * mPageScaleFactor * mDIPScale);
        int contentWidthPix = (int) (mContentWidthCss * mPageScaleFactor * mDIPScale);

        int measuredHeight = contentHeightPix;
        int measuredWidth = contentWidthPix;

        mLastMeasuredPageScaleFactor = mPageScaleFactor;

        // Always use the given size unless unspecified. This matches WebViewClassic behavior.
        mWidthMeasurementIsFixed = (widthMode != MeasureSpec.UNSPECIFIED);
        mHeightMeasurementIsFixed = (heightMode == MeasureSpec.EXACTLY);
        mHeightMeasurementLimited =
            (heightMode == MeasureSpec.AT_MOST) && (contentHeightPix > heightSize);
        mHeightMeasurementLimit = heightSize;

        if (mHeightMeasurementIsFixed || mHeightMeasurementLimited) {
            measuredHeight = heightSize;
        }

        if (mWidthMeasurementIsFixed) {
            measuredWidth = widthSize;
        }

        if (measuredHeight < contentHeightPix) {
            measuredHeight |= View.MEASURED_STATE_TOO_SMALL;
        }

        if (measuredWidth < contentWidthPix) {
            measuredWidth |= View.MEASURED_STATE_TOO_SMALL;
        }

        mDelegate.setMeasuredDimension(measuredWidth, measuredHeight);
    }

    /**
     * Notify the AwLayoutSizer that the size of the view has changed.
     * This should be called by the Android view system after onMeasure if the view's size has
     * changed.
     */
    public void onSizeChanged(int w, int h, int ow, int oh) {
        mLastWidth = w;
        mLastHeight = h;
        updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
    }

    /**
     * Notify the AwLayoutSizer that the layout pass requested via Delegate.requestLayout has
     * completed.
     * This should be called after onSizeChanged regardless of whether the size has changed or not.
     */
    public void onLayoutChange() {
        updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
    }

    private void setFixedLayoutSize(int widthDip, int heightDip) {
        if (widthDip == mLastSentFixedLayoutSizeWidth &&
                heightDip == mLastSentFixedLayoutSizeHeight)
            return;
        mLastSentFixedLayoutSizeWidth = widthDip;
        mLastSentFixedLayoutSizeHeight = heightDip;

        mDelegate.setFixedLayoutSize(widthDip, heightDip);
    }

    // This needs to be called every time either the physical size of the view is changed or the
    // pageScale is changed.  Since we need to ensure that this is called immediately after
    // onSizeChanged we can't just wait for onLayoutChange. At the same time we can't only make this
    // call from onSizeChanged, since onSizeChanged won't fire if the view's physical size doesn't
    // change.
    private void updateFixedLayoutSize(int w, int h, float pageScaleFactor) {
        boolean wrapContentForHeight = mDelegate.isLayoutParamsHeightWrapContent();
        // If the WebView's size in the Android view system depends on the size of its contents then
        // the viewport size cannot be directly calculated from the WebView's physical size as that
        // can result in the layout being unstable (for example loading the following contents
        //   <div style="height:150%">a</a>
        // would cause the WebView to indefinitely attempt to increase its height by 50%).
        // If both the width and height are fixed (specified by the parent View) then content size
        // changes will not cause subsequent layout passes and so we don't need to do anything
        // special.
        // We assume the width is 'fixed' if the parent View specified an EXACT or an AT_MOST
        // measureSpec for the width (in which case the AT_MOST upper bound is the width).
        // That means that the WebView will ignore LayoutParams.width set to WRAP_CONTENT and will
        // instead try to take up as much width as possible. This is necessary because it's not
        // practical to do web layout without a set width.
        // For height the behavior is different because for a given width it is possible to
        // calculate the minimum height required to display all of the content. As such the WebView
        // can size itself vertically to match the content height. Because certain container views
        // (LinearLayout with a WRAP_CONTENT height, for example) can result in onMeasure calls with
        // both EXACTLY and AT_MOST height measureSpecs it is not possible to infer the sizing
        // policy for the whole subtree based on the parameters passed to the onMeasure call.
        // For that reason the LayoutParams.height property of the WebView is used. This behaves
        // more predictably and means that toggling the fixedLayoutSize mode (which can have
        // significant impact on how the web contents is laid out) is a direct consequence of the
        // developer's choice. The downside is that it could result in the Android layout being
        // unstable if a parent of the WebView has a wrap_content height while the WebView itself
        // has height set to match_parent. Unfortunately addressing this edge case is costly so it
        // will have to stay as is (this is compatible with Classic behavior).
        if ((mWidthMeasurementIsFixed && !wrapContentForHeight) || pageScaleFactor == 0) {
            setFixedLayoutSize(0, 0);
            return;
        }

        final double dipAndPageScale = pageScaleFactor * mDIPScale;
        final int contentWidthPix = (int) (mContentWidthCss * dipAndPageScale);

        int widthDip = (int) Math.ceil(w / dipAndPageScale);

        // Make sure that we don't introduce rounding errors if the viewport is to be exactly as
        // wide as the contents.
        if (w == contentWidthPix) {
            widthDip = mContentWidthCss;
        }

        // This is workaround due to the fact that in wrap content mode we need to use a fixed
        // layout size independent of view height, otherwise things like <div style="height:120%">
        // cause the webview to grow indefinitely. We need to use a height independent of the
        // webview's height. 0 is the value used in WebViewClassic.
        setFixedLayoutSize(widthDip, FIXED_LAYOUT_HEIGHT);
    }
}

/* [<][>][^][v][top][bottom][index][help] */