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