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