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