Home | History | Annotate | Download | only in widget
      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 
     54     public AlertDialogLayout(@Nullable Context context) {
     55         super(context);
     56     }
     57 
     58     public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
     59         super(context, attrs);
     60     }
     61 
     62     @Override
     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 
     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      */
    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      */
    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
    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 
    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 }