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