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