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