/*
* 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.
*
* 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:
*
* - topPanel: exactly wrap_content
* - contentPanel OR customPanel: at most fill_parent, first priority for
* extra space
* - buttonPanel: at least minHeight, at most wrap_content, second
* priority for extra space
*
*/
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.
*
* 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);
}
}