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.LIBRARY_GROUP; 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(LIBRARY_GROUP) 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 = View.combineMeasuredStates(childState, topPanel.getMeasuredState()); 110 } 111 112 int buttonHeight = 0; 113 int buttonWantsHeight = 0; 114 if (buttonPanel != null) { 115 buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); 116 buttonHeight = resolveMinimumHeight(buttonPanel); 117 buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight; 118 119 usedHeight += buttonHeight; 120 childState = View.combineMeasuredStates(childState, buttonPanel.getMeasuredState()); 121 } 122 123 int middleHeight = 0; 124 if (middlePanel != null) { 125 final int childHeightSpec; 126 if (heightMode == MeasureSpec.UNSPECIFIED) { 127 childHeightSpec = MeasureSpec.UNSPECIFIED; 128 } else { 129 childHeightSpec = MeasureSpec.makeMeasureSpec( 130 Math.max(0, heightSize - usedHeight), heightMode); 131 } 132 133 middlePanel.measure(widthMeasureSpec, childHeightSpec); 134 middleHeight = middlePanel.getMeasuredHeight(); 135 136 usedHeight += middleHeight; 137 childState = View.combineMeasuredStates(childState, middlePanel.getMeasuredState()); 138 } 139 140 int remainingHeight = heightSize - usedHeight; 141 142 // Time for the "real" button measure pass. If we have remaining space, 143 // make the button pane bigger up to its target height. Otherwise, 144 // just remeasure the button at whatever height it needs. 145 if (buttonPanel != null) { 146 usedHeight -= buttonHeight; 147 148 final int heightToGive = Math.min(remainingHeight, buttonWantsHeight); 149 if (heightToGive > 0) { 150 remainingHeight -= heightToGive; 151 buttonHeight += heightToGive; 152 } 153 154 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 155 buttonHeight, MeasureSpec.EXACTLY); 156 buttonPanel.measure(widthMeasureSpec, childHeightSpec); 157 158 usedHeight += buttonPanel.getMeasuredHeight(); 159 childState = View.combineMeasuredStates(childState, buttonPanel.getMeasuredState()); 160 } 161 162 // If we still have remaining space, make the middle pane bigger up 163 // to the maximum height. 164 if (middlePanel != null && remainingHeight > 0) { 165 usedHeight -= middleHeight; 166 167 final int heightToGive = remainingHeight; 168 remainingHeight -= heightToGive; 169 middleHeight += heightToGive; 170 171 // Pass the same height mode as we're using for the dialog itself. 172 // If it's EXACTLY, then the middle pane MUST use the entire 173 // height. 174 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 175 middleHeight, heightMode); 176 middlePanel.measure(widthMeasureSpec, childHeightSpec); 177 178 usedHeight += middlePanel.getMeasuredHeight(); 179 childState = View.combineMeasuredStates(childState, middlePanel.getMeasuredState()); 180 } 181 182 // Compute desired width as maximum child width. 183 int maxWidth = 0; 184 for (int i = 0; i < count; i++) { 185 final View child = getChildAt(i); 186 if (child.getVisibility() != View.GONE) { 187 maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); 188 } 189 } 190 191 maxWidth += getPaddingLeft() + getPaddingRight(); 192 193 final int widthSizeAndState = View.resolveSizeAndState( 194 maxWidth, widthMeasureSpec, childState); 195 final int heightSizeAndState = View.resolveSizeAndState( 196 usedHeight, heightMeasureSpec, 0); 197 setMeasuredDimension(widthSizeAndState, heightSizeAndState); 198 199 // If the children weren't already measured EXACTLY, we need to run 200 // another measure pass to for MATCH_PARENT widths. 201 if (widthMode != MeasureSpec.EXACTLY) { 202 forceUniformWidth(count, heightMeasureSpec); 203 } 204 205 return true; 206 } 207 208 /** 209 * Remeasures child views to exactly match the layout's measured width. 210 * 211 * @param count the number of child views 212 * @param heightMeasureSpec the original height measure spec 213 */ 214 private void forceUniformWidth(int count, int heightMeasureSpec) { 215 // Pretend that the linear layout has an exact size. 216 final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec( 217 getMeasuredWidth(), MeasureSpec.EXACTLY); 218 219 for (int i = 0; i < count; i++) { 220 final View child = getChildAt(i); 221 if (child.getVisibility() != GONE) { 222 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 223 if (lp.width == LayoutParams.MATCH_PARENT) { 224 // Temporarily force children to reuse their old measured 225 // height. 226 final int oldHeight = lp.height; 227 lp.height = child.getMeasuredHeight(); 228 229 // Remeasure with new dimensions. 230 measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); 231 lp.height = oldHeight; 232 } 233 } 234 } 235 } 236 237 /** 238 * Attempts to resolve the minimum height of a view. 239 * <p> 240 * If the view doesn't have a minimum height set and only contains a single 241 * child, attempts to resolve the minimum height of the child view. 242 * 243 * @param v the view whose minimum height to resolve 244 * @return the minimum height 245 */ 246 private static int resolveMinimumHeight(View v) { 247 final int minHeight = ViewCompat.getMinimumHeight(v); 248 if (minHeight > 0) { 249 return minHeight; 250 } 251 252 if (v instanceof ViewGroup) { 253 final ViewGroup vg = (ViewGroup) v; 254 if (vg.getChildCount() == 1) { 255 return resolveMinimumHeight(vg.getChildAt(0)); 256 } 257 } 258 259 return 0; 260 } 261 262 @Override 263 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 264 final int paddingLeft = getPaddingLeft(); 265 266 // Where right end of child should go 267 final int width = right - left; 268 final int childRight = width - getPaddingRight(); 269 270 // Space available for child 271 final int childSpace = width - paddingLeft - getPaddingRight(); 272 273 final int totalLength = getMeasuredHeight(); 274 final int count = getChildCount(); 275 final int gravity = getGravity(); 276 final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 277 final int minorGravity = gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK; 278 279 int childTop; 280 switch (majorGravity) { 281 case Gravity.BOTTOM: 282 // totalLength contains the padding already 283 childTop = getPaddingTop() + bottom - top - totalLength; 284 break; 285 286 // totalLength contains the padding already 287 case Gravity.CENTER_VERTICAL: 288 childTop = getPaddingTop() + (bottom - top - totalLength) / 2; 289 break; 290 291 case Gravity.TOP: 292 default: 293 childTop = getPaddingTop(); 294 break; 295 } 296 297 final Drawable dividerDrawable = getDividerDrawable(); 298 final int dividerHeight = dividerDrawable == null ? 299 0 : dividerDrawable.getIntrinsicHeight(); 300 301 for (int i = 0; i < count; i++) { 302 final View child = getChildAt(i); 303 if (child != null && child.getVisibility() != GONE) { 304 final int childWidth = child.getMeasuredWidth(); 305 final int childHeight = child.getMeasuredHeight(); 306 307 final LinearLayoutCompat.LayoutParams lp = 308 (LinearLayoutCompat.LayoutParams) child.getLayoutParams(); 309 310 int layoutGravity = lp.gravity; 311 if (layoutGravity < 0) { 312 layoutGravity = minorGravity; 313 } 314 final int layoutDirection = ViewCompat.getLayoutDirection(this); 315 final int absoluteGravity = GravityCompat.getAbsoluteGravity( 316 layoutGravity, layoutDirection); 317 318 final int childLeft; 319 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 320 case Gravity.CENTER_HORIZONTAL: 321 childLeft = paddingLeft + ((childSpace - childWidth) / 2) 322 + lp.leftMargin - lp.rightMargin; 323 break; 324 325 case Gravity.RIGHT: 326 childLeft = childRight - childWidth - lp.rightMargin; 327 break; 328 329 case Gravity.LEFT: 330 default: 331 childLeft = paddingLeft + lp.leftMargin; 332 break; 333 } 334 335 if (hasDividerBeforeChildAt(i)) { 336 childTop += dividerHeight; 337 } 338 339 childTop += lp.topMargin; 340 setChildFrame(child, childLeft, childTop, childWidth, childHeight); 341 childTop += childHeight + lp.bottomMargin; 342 } 343 } 344 } 345 346 private void setChildFrame(View child, int left, int top, int width, int height) { 347 child.layout(left, top, left + width, top + height); 348 } 349}