- add sources.
[platform/framework/web/crosswalk.git] / src / android_webview / java / src / org / chromium / android_webview / AwLayoutSizer.java
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.
4
5 package org.chromium.android_webview;
6
7 import android.util.Pair;
8 import android.view.View.MeasureSpec;
9 import android.view.View;
10
11 import org.chromium.content.browser.ContentViewCore;
12
13 /**
14  * Helper methods used to manage the layout of the View that contains AwContents.
15  */
16 public class AwLayoutSizer {
17     public static final int FIXED_LAYOUT_HEIGHT = 0;
18
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;
23
24     // Size of the rendered content, as reported by native.
25     private int mContentHeightCss;
26     private int mContentWidthCss;
27
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;
33
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;
38
39     private double mDIPScale;
40
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;
45
46     // The most recent width and height seen in onSizeChanged.
47     private int mLastWidth;
48     private int mLastHeight;
49
50     // Used to prevent sending multiple setFixedLayoutSize notifications with the same values.
51     private int mLastSentFixedLayoutSizeWidth = -1;
52     private int mLastSentFixedLayoutSizeHeight = -1;
53
54     // Callback object for interacting with the View.
55     private Delegate mDelegate;
56
57     public interface Delegate {
58         void requestLayout();
59         void setMeasuredDimension(int measuredWidth, int measuredHeight);
60         void setFixedLayoutSize(int widthDip, int heightDip);
61         boolean isLayoutParamsHeightWrapContent();
62     }
63
64     /**
65      * Default constructor. Note: both setDelegate and setDIPScale must be called before the class
66      * is ready for use.
67      */
68     public AwLayoutSizer() {
69     }
70
71     public void setDelegate(Delegate delegate) {
72         mDelegate = delegate;
73     }
74
75     public void setDIPScale(double dipScale) {
76         mDIPScale = dipScale;
77     }
78
79     /**
80      * Postpone requesting layouts till unfreezeLayoutRequests is called.
81      */
82     public void freezeLayoutRequests() {
83         mFreezeLayoutRequests = true;
84         mFrozenLayoutRequestPending = false;
85     }
86
87     /**
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.
90      */
91     public void unfreezeLayoutRequests() {
92         mFreezeLayoutRequests = false;
93         if (mFrozenLayoutRequestPending) {
94             mFrozenLayoutRequestPending = false;
95             mDelegate.requestLayout();
96         }
97     }
98
99     /**
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.
104      */
105     public void onContentSizeChanged(int widthCss, int heightCss) {
106         doUpdate(widthCss, heightCss, mPageScaleFactor);
107     }
108
109     /**
110      * Update the contents page scale.
111      * This should be called whenever the content page scale factor changes (due to pinch zoom, for
112      * example).
113      */
114     public void onPageScaleChanged(float pageScaleFactor) {
115         doUpdate(mContentWidthCss, mContentHeightCss, pageScaleFactor);
116     }
117
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);
131
132         mContentWidthCss = widthCss;
133         mContentHeightCss = heightCss;
134         mPageScaleFactor = pageScaleFactor;
135
136         if (layoutNeeded) {
137             if (mFreezeLayoutRequests) {
138                 mFrozenLayoutRequestPending = true;
139             } else {
140                 mDelegate.requestLayout();
141             }
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);
146         }
147     }
148
149     /**
150      * Calculate the size of the view.
151      * This is designed to be used to implement the android.view.View#onMeasure() method.
152      */
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);
158
159         int contentHeightPix = (int) (mContentHeightCss * mPageScaleFactor * mDIPScale);
160         int contentWidthPix = (int) (mContentWidthCss * mPageScaleFactor * mDIPScale);
161
162         int measuredHeight = contentHeightPix;
163         int measuredWidth = contentWidthPix;
164
165         mLastMeasuredPageScaleFactor = mPageScaleFactor;
166
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;
173
174         if (mHeightMeasurementIsFixed || mHeightMeasurementLimited) {
175             measuredHeight = heightSize;
176         }
177
178         if (mWidthMeasurementIsFixed) {
179             measuredWidth = widthSize;
180         }
181
182         if (measuredHeight < contentHeightPix) {
183             measuredHeight |= View.MEASURED_STATE_TOO_SMALL;
184         }
185
186         if (measuredWidth < contentWidthPix) {
187             measuredWidth |= View.MEASURED_STATE_TOO_SMALL;
188         }
189
190         mDelegate.setMeasuredDimension(measuredWidth, measuredHeight);
191     }
192
193     /**
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
196      * changed.
197      */
198     public void onSizeChanged(int w, int h, int ow, int oh) {
199         mLastWidth = w;
200         mLastHeight = h;
201         updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
202     }
203
204     /**
205      * Notify the AwLayoutSizer that the layout pass requested via Delegate.requestLayout has
206      * completed.
207      * This should be called after onSizeChanged regardless of whether the size has changed or not.
208      */
209     public void onLayoutChange() {
210         updateFixedLayoutSize(mLastWidth, mLastHeight, mLastMeasuredPageScaleFactor);
211     }
212
213     private void setFixedLayoutSize(int widthDip, int heightDip) {
214         if (widthDip == mLastSentFixedLayoutSizeWidth &&
215                 heightDip == mLastSentFixedLayoutSizeHeight)
216             return;
217         mLastSentFixedLayoutSizeWidth = widthDip;
218         mLastSentFixedLayoutSizeHeight = heightDip;
219
220         mDelegate.setFixedLayoutSize(widthDip, heightDip);
221     }
222
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
227     // change.
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
237         // special.
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);
258             return;
259         }
260
261         final double dipAndPageScale = pageScaleFactor * mDIPScale;
262         final int contentWidthPix = (int) (mContentWidthCss * dipAndPageScale);
263
264         int widthDip = (int) Math.ceil(w / dipAndPageScale);
265
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;
270         }
271
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);
277     }
278 }