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.LIBRARY_GROUP;
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(LIBRARY_GROUP)
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 = View.combineMeasuredStates(childState, topPanel.getMeasuredState());
110        }
111
112        int buttonHeight = 0;
113        int buttonWantsHeight = 0;
114        if (buttonPanel != null) {
115            buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
116            buttonHeight = resolveMinimumHeight(buttonPanel);
117            buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight;
118
119            usedHeight += buttonHeight;
120            childState = View.combineMeasuredStates(childState, buttonPanel.getMeasuredState());
121        }
122
123        int middleHeight = 0;
124        if (middlePanel != null) {
125            final int childHeightSpec;
126            if (heightMode == MeasureSpec.UNSPECIFIED) {
127                childHeightSpec = MeasureSpec.UNSPECIFIED;
128            } else {
129                childHeightSpec = MeasureSpec.makeMeasureSpec(
130                        Math.max(0, heightSize - usedHeight), heightMode);
131            }
132
133            middlePanel.measure(widthMeasureSpec, childHeightSpec);
134            middleHeight = middlePanel.getMeasuredHeight();
135
136            usedHeight += middleHeight;
137            childState = View.combineMeasuredStates(childState, middlePanel.getMeasuredState());
138        }
139
140        int remainingHeight = heightSize - usedHeight;
141
142        // Time for the "real" button measure pass. If we have remaining space,
143        // make the button pane bigger up to its target height. Otherwise,
144        // just remeasure the button at whatever height it needs.
145        if (buttonPanel != null) {
146            usedHeight -= buttonHeight;
147
148            final int heightToGive = Math.min(remainingHeight, buttonWantsHeight);
149            if (heightToGive > 0) {
150                remainingHeight -= heightToGive;
151                buttonHeight += heightToGive;
152            }
153
154            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
155                    buttonHeight, MeasureSpec.EXACTLY);
156            buttonPanel.measure(widthMeasureSpec, childHeightSpec);
157
158            usedHeight += buttonPanel.getMeasuredHeight();
159            childState = View.combineMeasuredStates(childState, buttonPanel.getMeasuredState());
160        }
161
162        // If we still have remaining space, make the middle pane bigger up
163        // to the maximum height.
164        if (middlePanel != null && remainingHeight > 0) {
165            usedHeight -= middleHeight;
166
167            final int heightToGive = remainingHeight;
168            remainingHeight -= heightToGive;
169            middleHeight += heightToGive;
170
171            // Pass the same height mode as we're using for the dialog itself.
172            // If it's EXACTLY, then the middle pane MUST use the entire
173            // height.
174            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
175                    middleHeight, heightMode);
176            middlePanel.measure(widthMeasureSpec, childHeightSpec);
177
178            usedHeight += middlePanel.getMeasuredHeight();
179            childState = View.combineMeasuredStates(childState, middlePanel.getMeasuredState());
180        }
181
182        // Compute desired width as maximum child width.
183        int maxWidth = 0;
184        for (int i = 0; i < count; i++) {
185            final View child = getChildAt(i);
186            if (child.getVisibility() != View.GONE) {
187                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
188            }
189        }
190
191        maxWidth += getPaddingLeft() + getPaddingRight();
192
193        final int widthSizeAndState = View.resolveSizeAndState(
194                maxWidth, widthMeasureSpec, childState);
195        final int heightSizeAndState = View.resolveSizeAndState(
196                usedHeight, heightMeasureSpec, 0);
197        setMeasuredDimension(widthSizeAndState, heightSizeAndState);
198
199        // If the children weren't already measured EXACTLY, we need to run
200        // another measure pass to for MATCH_PARENT widths.
201        if (widthMode != MeasureSpec.EXACTLY) {
202            forceUniformWidth(count, heightMeasureSpec);
203        }
204
205        return true;
206    }
207
208    /**
209     * Remeasures child views to exactly match the layout's measured width.
210     *
211     * @param count the number of child views
212     * @param heightMeasureSpec the original height measure spec
213     */
214    private void forceUniformWidth(int count, int heightMeasureSpec) {
215        // Pretend that the linear layout has an exact size.
216        final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(
217                getMeasuredWidth(), MeasureSpec.EXACTLY);
218
219        for (int i = 0; i < count; i++) {
220            final View child = getChildAt(i);
221            if (child.getVisibility() != GONE) {
222                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
223                if (lp.width == LayoutParams.MATCH_PARENT) {
224                    // Temporarily force children to reuse their old measured
225                    // height.
226                    final int oldHeight = lp.height;
227                    lp.height = child.getMeasuredHeight();
228
229                    // Remeasure with new dimensions.
230                    measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
231                    lp.height = oldHeight;
232                }
233            }
234        }
235    }
236
237    /**
238     * Attempts to resolve the minimum height of a view.
239     * <p>
240     * If the view doesn't have a minimum height set and only contains a single
241     * child, attempts to resolve the minimum height of the child view.
242     *
243     * @param v the view whose minimum height to resolve
244     * @return the minimum height
245     */
246    private static int resolveMinimumHeight(View v) {
247        final int minHeight = ViewCompat.getMinimumHeight(v);
248        if (minHeight > 0) {
249            return minHeight;
250        }
251
252        if (v instanceof ViewGroup) {
253            final ViewGroup vg = (ViewGroup) v;
254            if (vg.getChildCount() == 1) {
255                return resolveMinimumHeight(vg.getChildAt(0));
256            }
257        }
258
259        return 0;
260    }
261
262    @Override
263    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
264        final int paddingLeft = getPaddingLeft();
265
266        // Where right end of child should go
267        final int width = right - left;
268        final int childRight = width - getPaddingRight();
269
270        // Space available for child
271        final int childSpace = width - paddingLeft - getPaddingRight();
272
273        final int totalLength = getMeasuredHeight();
274        final int count = getChildCount();
275        final int gravity = getGravity();
276        final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
277        final int minorGravity = gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK;
278
279        int childTop;
280        switch (majorGravity) {
281            case Gravity.BOTTOM:
282                // totalLength contains the padding already
283                childTop = getPaddingTop() + bottom - top - totalLength;
284                break;
285
286            // totalLength contains the padding already
287            case Gravity.CENTER_VERTICAL:
288                childTop = getPaddingTop() + (bottom - top - totalLength) / 2;
289                break;
290
291            case Gravity.TOP:
292            default:
293                childTop = getPaddingTop();
294                break;
295        }
296
297        final Drawable dividerDrawable = getDividerDrawable();
298        final int dividerHeight = dividerDrawable == null ?
299                0 : dividerDrawable.getIntrinsicHeight();
300
301        for (int i = 0; i < count; i++) {
302            final View child = getChildAt(i);
303            if (child != null && child.getVisibility() != GONE) {
304                final int childWidth = child.getMeasuredWidth();
305                final int childHeight = child.getMeasuredHeight();
306
307                final LinearLayoutCompat.LayoutParams lp =
308                        (LinearLayoutCompat.LayoutParams) child.getLayoutParams();
309
310                int layoutGravity = lp.gravity;
311                if (layoutGravity < 0) {
312                    layoutGravity = minorGravity;
313                }
314                final int layoutDirection = ViewCompat.getLayoutDirection(this);
315                final int absoluteGravity = GravityCompat.getAbsoluteGravity(
316                        layoutGravity, layoutDirection);
317
318                final int childLeft;
319                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
320                    case Gravity.CENTER_HORIZONTAL:
321                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
322                                + lp.leftMargin - lp.rightMargin;
323                        break;
324
325                    case Gravity.RIGHT:
326                        childLeft = childRight - childWidth - lp.rightMargin;
327                        break;
328
329                    case Gravity.LEFT:
330                    default:
331                        childLeft = paddingLeft + lp.leftMargin;
332                        break;
333                }
334
335                if (hasDividerBeforeChildAt(i)) {
336                    childTop += dividerHeight;
337                }
338
339                childTop += lp.topMargin;
340                setChildFrame(child, childLeft, childTop, childWidth, childHeight);
341                childTop += childHeight + lp.bottomMargin;
342            }
343        }
344    }
345
346    private void setChildFrame(View child, int left, int top, int width, int height) {
347        child.layout(left, top, left + width, top + height);
348    }
349}