/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.annotation.AttrRes; import android.annotation.Nullable; import android.annotation.StyleRes; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.internal.R; /** * Special implementation of linear layout that's capable of laying out alert * dialog components. * <p> * A dialog consists of up to three panels. All panels are optional, and a * dialog may contain only a single panel. The panels are laid out according * to the following guidelines: * <ul> * <li>topPanel: exactly wrap_content</li> * <li>contentPanel OR customPanel: at most fill_parent, first priority for * extra space</li> * <li>buttonPanel: at least minHeight, at most wrap_content, second * priority for extra space</li> * </ul> */ public class AlertDialogLayout extends LinearLayout { public AlertDialogLayout(@Nullable Context context) { super(context); } public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); } public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) { // Failed to perform custom measurement, let superclass handle it. super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { View topPanel = null; View buttonPanel = null; View middlePanel = null; final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } final int id = child.getId(); switch (id) { case R.id.topPanel: topPanel = child; break; case R.id.buttonPanel: buttonPanel = child; break; case R.id.contentPanel: case R.id.customPanel: if (middlePanel != null) { // Both the content and custom are visible. Abort! return false; } middlePanel = child; break; default: // Unknown top-level child. Abort! return false; } } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); int childState = 0; int usedHeight = getPaddingTop() + getPaddingBottom(); if (topPanel != null) { topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); usedHeight += topPanel.getMeasuredHeight(); childState = combineMeasuredStates(childState, topPanel.getMeasuredState()); } int buttonHeight = 0; int buttonWantsHeight = 0; if (buttonPanel != null) { buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); buttonHeight = resolveMinimumHeight(buttonPanel); buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight; usedHeight += buttonHeight; childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); } int middleHeight = 0; if (middlePanel != null) { final int childHeightSpec; if (heightMode == MeasureSpec.UNSPECIFIED) { childHeightSpec = MeasureSpec.UNSPECIFIED; } else { childHeightSpec = MeasureSpec.makeMeasureSpec( Math.max(0, heightSize - usedHeight), heightMode); } middlePanel.measure(widthMeasureSpec, childHeightSpec); middleHeight = middlePanel.getMeasuredHeight(); usedHeight += middleHeight; childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); } int remainingHeight = heightSize - usedHeight; // Time for the "real" button measure pass. If we have remaining space, // make the button pane bigger up to its target height. Otherwise, // just remeasure the button at whatever height it needs. if (buttonPanel != null) { usedHeight -= buttonHeight; final int heightToGive = Math.min(remainingHeight, buttonWantsHeight); if (heightToGive > 0) { remainingHeight -= heightToGive; buttonHeight += heightToGive; } final int childHeightSpec = MeasureSpec.makeMeasureSpec( buttonHeight, MeasureSpec.EXACTLY); buttonPanel.measure(widthMeasureSpec, childHeightSpec); usedHeight += buttonPanel.getMeasuredHeight(); childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); } // If we still have remaining space, make the middle pane bigger up // to the maximum height. if (middlePanel != null && remainingHeight > 0) { usedHeight -= middleHeight; final int heightToGive = remainingHeight; remainingHeight -= heightToGive; middleHeight += heightToGive; // Pass the same height mode as we're using for the dialog itself. // If it's EXACTLY, then the middle pane MUST use the entire // height. final int childHeightSpec = MeasureSpec.makeMeasureSpec( middleHeight, heightMode); middlePanel.measure(widthMeasureSpec, childHeightSpec); usedHeight += middlePanel.getMeasuredHeight(); childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); } // Compute desired width as maximum child width. int maxWidth = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != View.GONE) { maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); } } maxWidth += getPaddingLeft() + getPaddingRight(); final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState); final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0); setMeasuredDimension(widthSizeAndState, heightSizeAndState); // If the children weren't already measured EXACTLY, we need to run // another measure pass to for MATCH_PARENT widths. if (widthMode != MeasureSpec.EXACTLY) { forceUniformWidth(count, heightMeasureSpec); } return true; } /** * Remeasures child views to exactly match the layout's measured width. * * @param count the number of child views * @param heightMeasureSpec the original height measure spec */ private void forceUniformWidth(int count, int heightMeasureSpec) { // Pretend that the linear layout has an exact size. final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth(), MeasureSpec.EXACTLY); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.width == LayoutParams.MATCH_PARENT) { // Temporarily force children to reuse their old measured // height. final int oldHeight = lp.height; lp.height = child.getMeasuredHeight(); // Remeasure with new dimensions. measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); lp.height = oldHeight; } } } } /** * Attempts to resolve the minimum height of a view. * <p> * If the view doesn't have a minimum height set and only contains a single * child, attempts to resolve the minimum height of the child view. * * @param v the view whose minimum height to resolve * @return the minimum height */ private int resolveMinimumHeight(View v) { final int minHeight = v.getMinimumHeight(); if (minHeight > 0) { return minHeight; } if (v instanceof ViewGroup) { final ViewGroup vg = (ViewGroup) v; if (vg.getChildCount() == 1) { return resolveMinimumHeight(vg.getChildAt(0)); } } return 0; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; // Where right end of child should go final int width = right - left; final int childRight = width - mPaddingRight; // Space available for child final int childSpace = width - paddingLeft - mPaddingRight; final int totalLength = getMeasuredHeight(); final int count = getChildCount(); final int gravity = getGravity(); final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; int childTop; switch (majorGravity) { case Gravity.BOTTOM: // totalLength contains the padding already childTop = mPaddingTop + bottom - top - totalLength; break; // totalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (bottom - top - totalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } final Drawable dividerDrawable = getDividerDrawable(); final int dividerHeight = dividerDrawable == null ? 0 : dividerDrawable.getIntrinsicHeight(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child != null && child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int layoutGravity = lp.gravity; if (layoutGravity < 0) { layoutGravity = minorGravity; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity( layoutGravity, layoutDirection); final int childLeft; switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } if (hasDividerBeforeChildAt(i)) { childTop += dividerHeight; } childTop += lp.topMargin; setChildFrame(child, childLeft, childTop, childWidth, childHeight); childTop += childHeight + lp.bottomMargin; } } } private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } }