AlertDialogLayout.java revision 2c96011dee46e09d6ab766a42a98b299927d633b
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 20 21import android.content.Context; 22import android.graphics.drawable.Drawable; 23import android.support.annotation.Nullable; 24import android.support.annotation.RestrictTo; 25import android.support.v4.view.GravityCompat; 26import android.support.v4.view.ViewCompat; 27import android.support.v7.appcompat.R; 28import android.util.AttributeSet; 29import android.view.Gravity; 30import android.view.View; 31import android.view.ViewGroup; 32 33/** 34 * Special implementation of linear layout that's capable of laying out alert 35 * dialog components. 36 * <p> 37 * A dialog consists of up to three panels. All panels are optional, and a 38 * dialog may contain only a single panel. The panels are laid out according 39 * to the following guidelines: 40 * <ul> 41 * <li>topPanel: exactly wrap_content</li> 42 * <li>contentPanel OR customPanel: at most fill_parent, first priority for 43 * extra space</li> 44 * <li>buttonPanel: at least minHeight, at most wrap_content, second 45 * priority for extra space</li> 46 * </ul> 47 * 48 * @hide 49 */ 50@RestrictTo(GROUP_ID) 51public class AlertDialogLayout extends LinearLayoutCompat { 52 53 public AlertDialogLayout(@Nullable Context context) { 54 super(context); 55 } 56 57 public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) { 58 super(context, attrs); 59 } 60 61 @Override 62 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 63 if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) { 64 // Failed to perform custom measurement, let superclass handle it. 65 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 66 } 67 } 68 69 private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { 70 View topPanel = null; 71 View buttonPanel = null; 72 View middlePanel = null; 73 74 final int count = getChildCount(); 75 for (int i = 0; i < count; i++) { 76 final View child = getChildAt(i); 77 if (child.getVisibility() == View.GONE) { 78 continue; 79 } 80 81 final int id = child.getId(); 82 if (id == R.id.topPanel) { 83 topPanel = child; 84 } else if (id == R.id.buttonPanel) { 85 buttonPanel = child; 86 } else if (id == R.id.contentPanel || id == R.id.customPanel) { 87 if (middlePanel != null) { 88 // Both the content and custom are visible. Abort! 89 return false; 90 } 91 middlePanel = child; 92 } else { 93 // Unknown top-level child. Abort! 94 return false; 95 } 96 } 97 98 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 99 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 100 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 101 102 int childState = 0; 103 int usedHeight = getPaddingTop() + getPaddingBottom(); 104 105 if (topPanel != null) { 106 topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); 107 108 usedHeight += topPanel.getMeasuredHeight(); 109 childState = ViewCompat.combineMeasuredStates(childState, 110 ViewCompat.getMeasuredState(topPanel)); 111 } 112 113 int buttonHeight = 0; 114 int buttonWantsHeight = 0; 115 if (buttonPanel != null) { 116 buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); 117 buttonHeight = resolveMinimumHeight(buttonPanel); 118 buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight; 119 120 usedHeight += buttonHeight; 121 childState = ViewCompat.combineMeasuredStates(childState, 122 ViewCompat.getMeasuredState(buttonPanel)); 123 } 124 125 int middleHeight = 0; 126 if (middlePanel != null) { 127 final int childHeightSpec; 128 if (heightMode == MeasureSpec.UNSPECIFIED) { 129 childHeightSpec = MeasureSpec.UNSPECIFIED; 130 } else { 131 childHeightSpec = MeasureSpec.makeMeasureSpec( 132 Math.max(0, heightSize - usedHeight), heightMode); 133 } 134 135 middlePanel.measure(widthMeasureSpec, childHeightSpec); 136 middleHeight = middlePanel.getMeasuredHeight(); 137 138 usedHeight += middleHeight; 139 childState = ViewCompat.combineMeasuredStates(childState, 140 ViewCompat.getMeasuredState(middlePanel)); 141 } 142 143 int remainingHeight = heightSize - usedHeight; 144 145 // Time for the "real" button measure pass. If we have remaining space, 146 // make the button pane bigger up to its target height. Otherwise, 147 // just remeasure the button at whatever height it needs. 148 if (buttonPanel != null) { 149 usedHeight -= buttonHeight; 150 151 final int heightToGive = Math.min(remainingHeight, buttonWantsHeight); 152 if (heightToGive > 0) { 153 remainingHeight -= heightToGive; 154 buttonHeight += heightToGive; 155 } 156 157 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 158 buttonHeight, MeasureSpec.EXACTLY); 159 buttonPanel.measure(widthMeasureSpec, childHeightSpec); 160 161 usedHeight += buttonPanel.getMeasuredHeight(); 162 childState = ViewCompat.combineMeasuredStates(childState, 163 ViewCompat.getMeasuredState(buttonPanel)); 164 } 165 166 // If we still have remaining space, make the middle pane bigger up 167 // to the maximum height. 168 if (middlePanel != null && remainingHeight > 0) { 169 usedHeight -= middleHeight; 170 171 final int heightToGive = remainingHeight; 172 remainingHeight -= heightToGive; 173 middleHeight += heightToGive; 174 175 // Pass the same height mode as we're using for the dialog itself. 176 // If it's EXACTLY, then the middle pane MUST use the entire 177 // height. 178 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 179 middleHeight, heightMode); 180 middlePanel.measure(widthMeasureSpec, childHeightSpec); 181 182 usedHeight += middlePanel.getMeasuredHeight(); 183 childState = ViewCompat.combineMeasuredStates(childState, 184 ViewCompat.getMeasuredState(middlePanel)); 185 } 186 187 // Compute desired width as maximum child width. 188 int maxWidth = 0; 189 for (int i = 0; i < count; i++) { 190 final View child = getChildAt(i); 191 if (child.getVisibility() != View.GONE) { 192 maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); 193 } 194 } 195 196 maxWidth += getPaddingLeft() + getPaddingRight(); 197 198 final int widthSizeAndState = ViewCompat.resolveSizeAndState( 199 maxWidth, widthMeasureSpec, childState); 200 final int heightSizeAndState = ViewCompat.resolveSizeAndState( 201 usedHeight, heightMeasureSpec, 0); 202 setMeasuredDimension(widthSizeAndState, heightSizeAndState); 203 204 // If the children weren't already measured EXACTLY, we need to run 205 // another measure pass to for MATCH_PARENT widths. 206 if (widthMode != MeasureSpec.EXACTLY) { 207 forceUniformWidth(count, heightMeasureSpec); 208 } 209 210 return true; 211 } 212 213 /** 214 * Remeasures child views to exactly match the layout's measured width. 215 * 216 * @param count the number of child views 217 * @param heightMeasureSpec the original height measure spec 218 */ 219 private void forceUniformWidth(int count, int heightMeasureSpec) { 220 // Pretend that the linear layout has an exact size. 221 final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec( 222 getMeasuredWidth(), MeasureSpec.EXACTLY); 223 224 for (int i = 0; i < count; i++) { 225 final View child = getChildAt(i); 226 if (child.getVisibility() != GONE) { 227 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 228 if (lp.width == LayoutParams.MATCH_PARENT) { 229 // Temporarily force children to reuse their old measured 230 // height. 231 final int oldHeight = lp.height; 232 lp.height = child.getMeasuredHeight(); 233 234 // Remeasure with new dimensions. 235 measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); 236 lp.height = oldHeight; 237 } 238 } 239 } 240 } 241 242 /** 243 * Attempts to resolve the minimum height of a view. 244 * <p> 245 * If the view doesn't have a minimum height set and only contains a single 246 * child, attempts to resolve the minimum height of the child view. 247 * 248 * @param v the view whose minimum height to resolve 249 * @return the minimum height 250 */ 251 private static int resolveMinimumHeight(View v) { 252 final int minHeight = ViewCompat.getMinimumHeight(v); 253 if (minHeight > 0) { 254 return minHeight; 255 } 256 257 if (v instanceof ViewGroup) { 258 final ViewGroup vg = (ViewGroup) v; 259 if (vg.getChildCount() == 1) { 260 return resolveMinimumHeight(vg.getChildAt(0)); 261 } 262 } 263 264 return 0; 265 } 266 267 @Override 268 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 269 final int paddingLeft = getPaddingLeft(); 270 271 // Where right end of child should go 272 final int width = right - left; 273 final int childRight = width - getPaddingRight(); 274 275 // Space available for child 276 final int childSpace = width - paddingLeft - getPaddingRight(); 277 278 final int totalLength = getMeasuredHeight(); 279 final int count = getChildCount(); 280 final int gravity = getGravity(); 281 final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 282 final int minorGravity = gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK; 283 284 int childTop; 285 switch (majorGravity) { 286 case Gravity.BOTTOM: 287 // totalLength contains the padding already 288 childTop = getPaddingTop() + bottom - top - totalLength; 289 break; 290 291 // totalLength contains the padding already 292 case Gravity.CENTER_VERTICAL: 293 childTop = getPaddingTop() + (bottom - top - totalLength) / 2; 294 break; 295 296 case Gravity.TOP: 297 default: 298 childTop = getPaddingTop(); 299 break; 300 } 301 302 final Drawable dividerDrawable = getDividerDrawable(); 303 final int dividerHeight = dividerDrawable == null ? 304 0 : dividerDrawable.getIntrinsicHeight(); 305 306 for (int i = 0; i < count; i++) { 307 final View child = getChildAt(i); 308 if (child != null && child.getVisibility() != GONE) { 309 final int childWidth = child.getMeasuredWidth(); 310 final int childHeight = child.getMeasuredHeight(); 311 312 final LinearLayoutCompat.LayoutParams lp = 313 (LinearLayoutCompat.LayoutParams) child.getLayoutParams(); 314 315 int layoutGravity = lp.gravity; 316 if (layoutGravity < 0) { 317 layoutGravity = minorGravity; 318 } 319 final int layoutDirection = ViewCompat.getLayoutDirection(this); 320 final int absoluteGravity = GravityCompat.getAbsoluteGravity( 321 layoutGravity, layoutDirection); 322 323 final int childLeft; 324 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 325 case Gravity.CENTER_HORIZONTAL: 326 childLeft = paddingLeft + ((childSpace - childWidth) / 2) 327 + lp.leftMargin - lp.rightMargin; 328 break; 329 330 case Gravity.RIGHT: 331 childLeft = childRight - childWidth - lp.rightMargin; 332 break; 333 334 case Gravity.LEFT: 335 default: 336 childLeft = paddingLeft + lp.leftMargin; 337 break; 338 } 339 340 if (hasDividerBeforeChildAt(i)) { 341 childTop += dividerHeight; 342 } 343 344 childTop += lp.topMargin; 345 setChildFrame(child, childLeft, childTop, childWidth, childHeight); 346 childTop += childHeight + lp.bottomMargin; 347 } 348 } 349 } 350 351 private void setChildFrame(View child, int left, int top, int width, int height) { 352 child.layout(left, top, left + width, top + height); 353 } 354}