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