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