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 com.android.internal.widget; 18 19import android.annotation.AttrRes; 20import android.annotation.Nullable; 21import android.annotation.StyleRes; 22import android.content.Context; 23import android.util.AttributeSet; 24import android.view.View; 25import android.view.ViewGroup; 26import android.widget.LinearLayout; 27 28import com.android.internal.R; 29 30/** 31 * Special implementation of linear layout that's capable of laying out alert 32 * dialog components. 33 * <p> 34 * A dialog consists of up to three panels. All panels are optional, and a 35 * dialog may contain only a single panel. The panels are laid out according 36 * to the following guidelines: 37 * <ul> 38 * <li>topPanel: exactly wrap_content</li> 39 * <li>contentPanel OR customPanel: at most fill_parent, first priority for 40 * extra space</li> 41 * <li>buttonPanel: at least minHeight, at most wrap_content, second 42 * priority for extra space</li> 43 * </ul> 44 */ 45public class AlertDialogLayout extends LinearLayout { 46 47 public AlertDialogLayout(@Nullable Context context) { 48 super(context); 49 } 50 51 public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) { 52 super(context, attrs); 53 } 54 55 public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, 56 @AttrRes int defStyleAttr) { 57 super(context, attrs, defStyleAttr); 58 } 59 60 public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, 61 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 62 super(context, attrs, defStyleAttr, defStyleRes); 63 } 64 65 @Override 66 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 67 if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) { 68 // Failed to perform custom measurement, let superclass handle it. 69 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 70 } 71 } 72 73 private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { 74 View topPanel = null; 75 View buttonPanel = null; 76 View middlePanel = null; 77 78 final int count = getChildCount(); 79 for (int i = 0; i < count; i++) { 80 final View child = getChildAt(i); 81 if (child.getVisibility() == View.GONE) { 82 continue; 83 } 84 85 final int id = child.getId(); 86 switch (id) { 87 case R.id.topPanel: 88 topPanel = child; 89 break; 90 case R.id.buttonPanel: 91 buttonPanel = child; 92 break; 93 case R.id.contentPanel: 94 case R.id.customPanel: 95 if (middlePanel != null) { 96 // Both the content and custom are visible. Abort! 97 return false; 98 } 99 middlePanel = child; 100 break; 101 default: 102 // Unknown top-level child. Abort! 103 return false; 104 } 105 } 106 107 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 108 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 109 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 110 111 int childState = 0; 112 int usedHeight = getPaddingTop() + getPaddingBottom(); 113 114 if (topPanel != null) { 115 topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); 116 117 usedHeight += topPanel.getMeasuredHeight(); 118 childState = combineMeasuredStates(childState, topPanel.getMeasuredState()); 119 } 120 121 int buttonHeight = 0; 122 int buttonWantsHeight = 0; 123 if (buttonPanel != null) { 124 buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); 125 buttonHeight = resolveMinimumHeight(buttonPanel); 126 buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight; 127 128 usedHeight += buttonHeight; 129 childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); 130 } 131 132 int middleHeight = 0; 133 if (middlePanel != null) { 134 final int childHeightSpec; 135 if (heightMode == MeasureSpec.UNSPECIFIED) { 136 childHeightSpec = MeasureSpec.UNSPECIFIED; 137 } else { 138 childHeightSpec = MeasureSpec.makeMeasureSpec( 139 Math.max(0, heightSize - usedHeight), heightMode); 140 } 141 142 middlePanel.measure(widthMeasureSpec, childHeightSpec); 143 middleHeight = middlePanel.getMeasuredHeight(); 144 145 usedHeight += middleHeight; 146 childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); 147 } 148 149 int remainingHeight = heightSize - usedHeight; 150 151 // Time for the "real" button measure pass. If we have remaining space, 152 // make the button pane bigger up to its target height. Otherwise, 153 // just remeasure the button at whatever height it needs. 154 if (buttonPanel != null) { 155 usedHeight -= buttonHeight; 156 157 final int heightToGive = Math.min(remainingHeight, buttonWantsHeight); 158 if (heightToGive > 0) { 159 remainingHeight -= heightToGive; 160 buttonHeight += heightToGive; 161 } 162 163 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 164 buttonHeight, MeasureSpec.EXACTLY); 165 buttonPanel.measure(widthMeasureSpec, childHeightSpec); 166 167 usedHeight += buttonPanel.getMeasuredHeight(); 168 childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); 169 } 170 171 // If we still have remaining space, make the middle pane bigger up 172 // to the maximum height. 173 if (middlePanel != null && remainingHeight > 0) { 174 usedHeight -= middleHeight; 175 176 final int heightToGive = remainingHeight; 177 remainingHeight -= heightToGive; 178 middleHeight += heightToGive; 179 180 // Pass the same height mode as we're using for the dialog itself. 181 // If it's EXACTLY, then the middle pane MUST use the entire 182 // height. 183 final int childHeightSpec = MeasureSpec.makeMeasureSpec( 184 middleHeight, heightMode); 185 middlePanel.measure(widthMeasureSpec, childHeightSpec); 186 187 usedHeight += middlePanel.getMeasuredHeight(); 188 childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); 189 } 190 191 // Compute desired width as maximum child width. 192 int maxWidth = 0; 193 for (int i = 0; i < count; i++) { 194 final View child = getChildAt(i); 195 if (child.getVisibility() != View.GONE) { 196 maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); 197 } 198 } 199 200 maxWidth += getPaddingLeft() + getPaddingRight(); 201 202 final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState); 203 final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0); 204 setMeasuredDimension(widthSizeAndState, heightSizeAndState); 205 206 // If the children weren't already measured EXACTLY, we need to run 207 // another measure pass to for MATCH_PARENT widths. 208 if (widthMode != MeasureSpec.EXACTLY) { 209 forceUniformWidth(count, heightMeasureSpec); 210 } 211 212 return true; 213 } 214 215 /** 216 * Remeasures child views to exactly match the layout's measured width. 217 * 218 * @param count the number of child views 219 * @param heightMeasureSpec the original height measure spec 220 */ 221 private void forceUniformWidth(int count, int heightMeasureSpec) { 222 // Pretend that the linear layout has an exact size. 223 final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec( 224 getMeasuredWidth(), MeasureSpec.EXACTLY); 225 226 for (int i = 0; i < count; i++) { 227 final View child = getChildAt(i); 228 if (child.getVisibility() != GONE) { 229 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 230 if (lp.width == LayoutParams.MATCH_PARENT) { 231 // Temporarily force children to reuse their old measured 232 // height. 233 final int oldHeight = lp.height; 234 lp.height = child.getMeasuredHeight(); 235 236 // Remeasure with new dimensions. 237 measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); 238 lp.height = oldHeight; 239 } 240 } 241 } 242 } 243 244 /** 245 * Attempts to resolve the minimum height of a view. 246 * <p> 247 * If the view doesn't have a minimum height set and only contains a single 248 * child, attempts to resolve the minimum height of the child view. 249 * 250 * @param v the view whose minimum height to resolve 251 * @return the minimum height 252 */ 253 private int resolveMinimumHeight(View v) { 254 final int minHeight = v.getMinimumHeight(); 255 if (minHeight > 0) { 256 return minHeight; 257 } 258 259 if (v instanceof ViewGroup) { 260 final ViewGroup vg = (ViewGroup) v; 261 if (vg.getChildCount() == 1) { 262 return resolveMinimumHeight(vg.getChildAt(0)); 263 } 264 } 265 266 return 0; 267 } 268} 269