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