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