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