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 com.android.internal.widget;
18
19import android.annotation.AttrRes;
20import android.annotation.Nullable;
21import android.annotation.StyleRes;
22import android.content.Context;
23import android.graphics.drawable.Drawable;
24import android.util.AttributeSet;
25import android.view.Gravity;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.LinearLayout;
29
30import 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 */
47public class AlertDialogLayout extends LinearLayout {
48
49    public AlertDialogLayout(@Nullable Context context) {
50        super(context);
51    }
52
53    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
54        super(context, attrs);
55    }
56
57    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
58            @AttrRes int defStyleAttr) {
59        super(context, attrs, defStyleAttr);
60    }
61
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
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
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     */
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     */
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
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
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