1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.android_webview;
7 import android.util.Pair;
8 import android.view.View.MeasureSpec;
9 import android.view.View;
11 import org.chromium.content.browser.ContentViewCore;
14 * Helper methods used to manage the layout of the View that contains AwContents.
16 public class AwLayoutSizer {
17 public static final int FIXED_LAYOUT_HEIGHT = 0;
19 // These are used to prevent a re-layout if the content size changes within a dimension that is
20 // fixed by the view system.
21 private boolean mWidthMeasurementIsFixed;
22 private boolean mHeightMeasurementIsFixed;
24 // Size of the rendered content, as reported by native.
25 private int mContentHeightCss;
26 private int mContentWidthCss;
28 // Page scale factor. This is set to zero initially so that we don't attempt to do a layout if
29 // we get the content size change notification first and a page scale change second.
30 private float mPageScaleFactor = 0.0f;
31 // The page scale factor that was used in the most recent onMeasure call.
32 private float mLastMeasuredPageScaleFactor = 0.0f;
34 // Whether to postpone layout requests.
35 private boolean mFreezeLayoutRequests;
36 // Did we try to request a layout since the last time mPostponeLayoutRequests was set to true.
37 private boolean mFrozenLayoutRequestPending;
39 private double mDIPScale;
41 // Was our height larger than the AT_MOST constraint the last time onMeasure was called?
42 private boolean mHeightMeasurementLimited;
43 // If mHeightMeasurementLimited is true then this contains the height limit.
44 private int mHeightMeasurementLimit;
46 // The most recent width and height seen in onSizeChanged.
47 private int mLastWidth;
48 private int mLastHeight;
50 // Used to prevent sending multiple setFixedLayoutSize notifications with the same values.
51 private int mLastSentFixedLayoutSizeWidth = -1;
52 private int mLastSentFixedLayoutSizeHeight = -1;
54 // Callback object for interacting with the View.
55 private Delegate mDelegate;
57 public interface Delegate {
59 void setMeasuredDimension(int measuredWidth, int measuredHeight);
60 void setFixedLayoutSize(int widthDip, int heightDip);
61 boolean isLayoutParamsHeightWrapContent();
65 * Default constructor. Note: both setDelegate and setDIPScale must be called before the class
68 public AwLayoutSizer() {
71 public void setDelegate(Delegate delegate) {
75 public void setDIPScale(double dipScale) {
80 * Postpone requesting layouts till unfreezeLayoutRequests is called.
82 public void freezeLayoutRequests() {
83 mFreezeLayoutRequests = true;
84 mFrozenLayoutRequestPending = false;
88 * Stop postponing layout requests and request layout if such a request would have been made
89 * had the freezeLayoutRequests method not been called before.
91 public void unfreezeLayoutRequests() {
92 mFreezeLayoutRequests = false;
93 if (mFrozenLayoutRequestPending) {
94 mFrozenLayoutRequestPending = false;
95 mDelegate.requestLayout();
100 * Update the contents size.
101 * This should be called whenever the content size changes (due to DOM manipulation or page
102 * load, for example).
103 * The width and height should be in CSS pixels.
105 public void onContentSizeChanged(int widthCss, int heightCss) {
106 doUpdate(widthCss, heightCss, mPageScaleFactor);
110 * Update the contents page scale.
111 * This should be called whenever the content page scale factor changes (due to pinch zoom, for
114 public void onPageScaleChanged(float pageScaleFactor) {
115 doUpdate(mContentWidthCss, mContentHeightCss, pageScaleFactor);
118 private void doUpdate(int widthCss, int heightCss, float pageScaleFactor) {
119 // We want to request layout only if the size or scale change, however if any of the
120 // measurements are 'fixed', then changing the underlying size won't have any effect, so we
121 // ignore changes to dimensions that are 'fixed'.
122 final int heightPix = (int) (heightCss * mPageScaleFactor * mDIPScale);
123 boolean pageScaleChanged = mPageScaleFactor != pageScaleFactor;
124 boolean contentHeightChangeMeaningful = !mHeightMeasurementIsFixed &&
125 (!mHeightMeasurementLimited || heightPix < mHeightMeasurementLimit);
126 boolean pageScaleChangeMeaningful =
127 !mWidthMeasurementIsFixed || contentHeightChangeMeaningful;
128 boolean layoutNeeded = (mContentWidthCss != widthCss && !mWidthMeasurementIsFixed) ||
129 (mContentHeightCss != heightCss && contentHeightChangeMeaningful) ||
130 (pageScaleChanged && pageScaleChangeMeaningful);
132 mContentWidthCss = widthCss;
133 mContentHeightCss = heightCss;
134 mPageScaleFactor = pageScaleFactor;
137 if (mFreezeLayoutRequests) {
138 mFrozenLayoutRequestPending = true;
140 mDelegate.requestLayout();
142 } else if (pageScaleChanged && mLastWidth != 0) {
143 // Because the fixed layout size is directly impacted by the pageScaleFactor we must
144 // update it even if the physical size of the view doesn't change.
145 updateFixedLayoutSize(mLastWidth, mLastHeight, mPageScaleFactor);
150 * Calculate the size of the view.
151 * This is designed to be used to implement the android.view.View#onMeasure() method.
153 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
154 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
155 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
156 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
157 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
159 int contentHeightPix = (int) (mContentHeightCss * mPageScaleFactor * mDIPScale);
160 int contentWidthPix = (int) (mContentWidthCss * mPageScaleFactor * mDIPScale);
162 int measuredHeight = contentHeightPix;
163 int measuredWidth = contentWidthPix;
165 mLastMeasuredPageScaleFactor = mPageScaleFactor;
167 // Always use the given size unless unspecified. This matches WebViewClassic behavior.
168 mWidthMeasurementIsFixed = (widthMode != MeasureSpec.UNSPECIFIED);
169 mHeightMeasurementIsFixed = (heightMode == MeasureSpec.EXACTLY);
170 mHeightMeasurementLimited =
171 (heightMode == MeasureSpec.AT_MOST) && (contentHeightPix > heightSize);
172 mHeightMeasurementLimit = heightSize;
174 if (mHeightMeasurementIsFixed || mHeightMeasurementLimited) {
175 measuredHeight = heightSize;
178 if (mWidthMeasurementIsFixed) {
179 measuredWidth = widthSize;
182 if (measuredHeight < contentHeightPix) {
183 measuredHeight |= View.MEASURED_STATE_TOO_SMALL;
186 if (measuredWidth < contentWidthPix) {
187 measuredWidth |= View.MEASURED_STATE_TOO_SMALL;
190 mDelegate.setMeasuredDimension(measuredWidth, measuredHeight);
194 * Notify the AwLayoutSizer that the size of the view has changed.
195 * This should be called by the Android view system after onMeasure if the view's size has
198 public void onSizeChanged(int w, int h, int ow, int oh) {
201 updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
205 * Notify the AwLayoutSizer that the layout pass requested via Delegate.requestLayout has
207 * This should be called after onSizeChanged regardless of whether the size has changed or not.
209 public void onLayoutChange() {
210 updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
213 private void setFixedLayoutSize(int widthDip, int heightDip) {
214 if (widthDip == mLastSentFixedLayoutSizeWidth &&
215 heightDip == mLastSentFixedLayoutSizeHeight)
217 mLastSentFixedLayoutSizeWidth = widthDip;
218 mLastSentFixedLayoutSizeHeight = heightDip;
220 mDelegate.setFixedLayoutSize(widthDip, heightDip);
223 // This needs to be called every time either the physical size of the view is changed or the
224 // pageScale is changed. Since we need to ensure that this is called immediately after
225 // onSizeChanged we can't just wait for onLayoutChange. At the same time we can't only make this
226 // call from onSizeChanged, since onSizeChanged won't fire if the view's physical size doesn't
228 private void updateFixedLayoutSize(int w, int h, float pageScaleFactor) {
229 boolean wrapContentForHeight = mDelegate.isLayoutParamsHeightWrapContent();
230 // If the WebView's size in the Android view system depends on the size of its contents then
231 // the viewport size cannot be directly calculated from the WebView's physical size as that
232 // can result in the layout being unstable (for example loading the following contents
233 // <div style="height:150%">a</a>
234 // would cause the WebView to indefinitely attempt to increase its height by 50%).
235 // If both the width and height are fixed (specified by the parent View) then content size
236 // changes will not cause subsequent layout passes and so we don't need to do anything
238 // We assume the width is 'fixed' if the parent View specified an EXACT or an AT_MOST
239 // measureSpec for the width (in which case the AT_MOST upper bound is the width).
240 // That means that the WebView will ignore LayoutParams.width set to WRAP_CONTENT and will
241 // instead try to take up as much width as possible. This is necessary because it's not
242 // practical to do web layout without a set width.
243 // For height the behavior is different because for a given width it is possible to
244 // calculate the minimum height required to display all of the content. As such the WebView
245 // can size itself vertically to match the content height. Because certain container views
246 // (LinearLayout with a WRAP_CONTENT height, for example) can result in onMeasure calls with
247 // both EXACTLY and AT_MOST height measureSpecs it is not possible to infer the sizing
248 // policy for the whole subtree based on the parameters passed to the onMeasure call.
249 // For that reason the LayoutParams.height property of the WebView is used. This behaves
250 // more predictably and means that toggling the fixedLayoutSize mode (which can have
251 // significant impact on how the web contents is laid out) is a direct consequence of the
252 // developer's choice. The downside is that it could result in the Android layout being
253 // unstable if a parent of the WebView has a wrap_content height while the WebView itself
254 // has height set to match_parent. Unfortunately addressing this edge case is costly so it
255 // will have to stay as is (this is compatible with Classic behavior).
256 if ((mWidthMeasurementIsFixed && !wrapContentForHeight) || pageScaleFactor == 0) {
257 setFixedLayoutSize(0, 0);
261 final double dipAndPageScale = pageScaleFactor * mDIPScale;
262 final int contentWidthPix = (int) (mContentWidthCss * dipAndPageScale);
264 int widthDip = (int) Math.ceil(w / dipAndPageScale);
266 // Make sure that we don't introduce rounding errors if the viewport is to be exactly as
267 // wide as the contents.
268 if (w == contentWidthPix) {
269 widthDip = mContentWidthCss;
272 // This is workaround due to the fact that in wrap content mode we need to use a fixed
273 // layout size independent of view height, otherwise things like <div style="height:120%">
274 // cause the webview to grow indefinitely. We need to use a height independent of the
275 // webview's height. 0 is the value used in WebViewClassic.
276 setFixedLayoutSize(widthDip, FIXED_LAYOUT_HEIGHT);