361 lines
13 KiB
Java
361 lines
13 KiB
Java
/*
|
|
* 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.compat.annotation.UnsupportedAppUsage;
|
|
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);
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
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);
|
|
}
|
|
}
|