package com.afollestad.materialdialogs.internal;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.webkit.WebView;
import android.widget.AdapterView;
import android.widget.ScrollView;
import com.afollestad.materialdialogs.GravityEnum;
import com.afollestad.materialdialogs.R;
import com.afollestad.materialdialogs.StackingBehavior;
import com.afollestad.materialdialogs.util.DialogUtils;
/**
* @author Kevin Barry (teslacoil) 4/02/2015 This is the top level view for all MaterialDialogs It
* handles the layout of: titleFrame (md_stub_titleframe) content (text, custom view, listview,
* etc) buttonDefault... (either stacked or horizontal)
*/
public class MDRootLayout extends ViewGroup {
private static final int INDEX_NEUTRAL = 0;
private static final int INDEX_NEGATIVE = 1;
private static final int INDEX_POSITIVE = 2;
private final MDButton[] buttons = new MDButton[3];
private int maxHeight;
private View titleBar;
private View content;
private boolean drawTopDivider = false;
private boolean drawBottomDivider = false;
private StackingBehavior stackBehavior = StackingBehavior.ADAPTIVE;
private boolean isStacked = false;
private boolean useFullPadding = true;
private boolean reducePaddingNoTitleNoButtons;
private boolean noTitleNoPadding;
private int noTitlePaddingFull;
private int buttonPaddingFull;
private int buttonBarHeight;
private GravityEnum buttonGravity = GravityEnum.START;
/* Margin from dialog frame to first button */
private int buttonHorizontalEdgeMargin;
private Paint dividerPaint;
private ViewTreeObserver.OnScrollChangedListener topOnScrollChangedListener;
private ViewTreeObserver.OnScrollChangedListener bottomOnScrollChangedListener;
private int dividerWidth;
public MDRootLayout(Context context) {
super(context);
init(context, null, 0);
}
public MDRootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private static boolean isVisible(View v) {
boolean visible = v != null && v.getVisibility() != View.GONE;
if (visible && v instanceof MDButton) {
visible = ((MDButton) v).getText().toString().trim().length() > 0;
}
return visible;
}
public static boolean canRecyclerViewScroll(RecyclerView view) {
return view != null
&& view.getLayoutManager() != null
&& view.getLayoutManager().canScrollVertically();
}
private static boolean canScrollViewScroll(ScrollView sv) {
if (sv.getChildCount() == 0) {
return false;
}
final int childHeight = sv.getChildAt(0).getMeasuredHeight();
return sv.getMeasuredHeight() - sv.getPaddingTop() - sv.getPaddingBottom() < childHeight;
}
private static boolean canWebViewScroll(WebView view) {
//noinspection deprecation
return view.getMeasuredHeight() < view.getContentHeight() * view.getScale();
}
private static boolean canAdapterViewScroll(AdapterView lv) {
/* Force it to layout it's children */
if (lv.getLastVisiblePosition() == -1) {
return false;
}
/* We can scroll if the first or last item is not visible */
boolean firstItemVisible = lv.getFirstVisiblePosition() == 0;
boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1;
if (firstItemVisible && lastItemVisible && lv.getChildCount() > 0) {
/* Or the first item's top is above or own top */
if (lv.getChildAt(0).getTop() < lv.getPaddingTop()) {
return true;
}
/* or the last item's bottom is beyond our own bottom */
return lv.getChildAt(lv.getChildCount() - 1).getBottom()
> lv.getHeight() - lv.getPaddingBottom();
}
return true;
}
/**
* Find the view touching the bottom of this ViewGroup. Non visible children are ignored, however
* getChildDrawingOrder is not taking into account for simplicity and because it behaves
* inconsistently across platform versions.
*
* @return View touching the bottom of this ViewGroup or null
*/
@Nullable
private static View getBottomView(ViewGroup viewGroup) {
if (viewGroup == null || viewGroup.getChildCount() == 0) {
return null;
}
View bottomView = null;
for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
View child = viewGroup.getChildAt(i);
if (child.getVisibility() == View.VISIBLE
&& child.getBottom() == viewGroup.getMeasuredHeight()) {
bottomView = child;
break;
}
}
return bottomView;
}
@Nullable
private static View getTopView(ViewGroup viewGroup) {
if (viewGroup == null || viewGroup.getChildCount() == 0) {
return null;
}
View topView = null;
for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
View child = viewGroup.getChildAt(i);
if (child.getVisibility() == View.VISIBLE && child.getTop() == 0) {
topView = child;
break;
}
}
return topView;
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
Resources r = context.getResources();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MDRootLayout, defStyleAttr, 0);
reducePaddingNoTitleNoButtons =
a.getBoolean(R.styleable.MDRootLayout_md_reduce_padding_no_title_no_buttons, true);
a.recycle();
noTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding);
buttonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding);
buttonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side);
buttonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height);
dividerPaint = new Paint();
dividerWidth = r.getDimensionPixelSize(R.dimen.md_divider_height);
dividerPaint.setColor(DialogUtils.resolveColor(context, R.attr.md_divider_color));
setWillNotDraw(false);
}
public void setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
}
public void noTitleNoPadding() {
noTitleNoPadding = true;
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
if (v.getId() == R.id.md_titleFrame) {
titleBar = v;
} else if (v.getId() == R.id.md_buttonDefaultNeutral) {
buttons[INDEX_NEUTRAL] = (MDButton) v;
} else if (v.getId() == R.id.md_buttonDefaultNegative) {
buttons[INDEX_NEGATIVE] = (MDButton) v;
} else if (v.getId() == R.id.md_buttonDefaultPositive) {
buttons[INDEX_POSITIVE] = (MDButton) v;
} else {
content = v;
}
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (height > maxHeight) {
height = maxHeight;
}
useFullPadding = true;
boolean hasButtons = false;
final boolean stacked;
if (stackBehavior == StackingBehavior.ALWAYS) {
stacked = true;
} else if (stackBehavior == StackingBehavior.NEVER) {
stacked = false;
} else {
int buttonsWidth = 0;
for (MDButton button : buttons) {
if (button != null && isVisible(button)) {
button.setStacked(false, false);
measureChild(button, widthMeasureSpec, heightMeasureSpec);
buttonsWidth += button.getMeasuredWidth();
hasButtons = true;
}
}
int buttonBarPadding =
getContext().getResources().getDimensionPixelSize(R.dimen.md_neutral_button_margin);
final int buttonFrameWidth = width - 2 * buttonBarPadding;
stacked = buttonsWidth > buttonFrameWidth;
}
int stackedHeight = 0;
isStacked = stacked;
if (stacked) {
for (MDButton button : buttons) {
if (button != null && isVisible(button)) {
button.setStacked(true, false);
measureChild(button, widthMeasureSpec, heightMeasureSpec);
stackedHeight += button.getMeasuredHeight();
hasButtons = true;
}
}
}
int availableHeight = height;
int fullPadding = 0;
int minPadding = 0;
if (hasButtons) {
if (isStacked) {
availableHeight -= stackedHeight;
fullPadding += 2 * buttonPaddingFull;
minPadding += 2 * buttonPaddingFull;
} else {
availableHeight -= buttonBarHeight;
fullPadding += 2 * buttonPaddingFull;
/* No minPadding */
}
} else {
/* Content has 8dp, we add 16dp and get 24dp, the frame margin */
fullPadding += 2 * buttonPaddingFull;
}
if (isVisible(titleBar)) {
titleBar.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);
availableHeight -= titleBar.getMeasuredHeight();
} else if (!noTitleNoPadding) {
fullPadding += noTitlePaddingFull;
}
if (isVisible(content)) {
content.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST));
if (content.getMeasuredHeight() <= availableHeight - fullPadding) {
if (!reducePaddingNoTitleNoButtons || isVisible(titleBar) || hasButtons) {
useFullPadding = true;
availableHeight -= content.getMeasuredHeight() + fullPadding;
} else {
useFullPadding = false;
availableHeight -= content.getMeasuredHeight() + minPadding;
}
} else {
useFullPadding = false;
availableHeight = 0;
}
}
setMeasuredDimension(width, height - availableHeight);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (content != null) {
if (drawTopDivider) {
int y = content.getTop();
canvas.drawRect(0, y - dividerWidth, getMeasuredWidth(), y, dividerPaint);
}
if (drawBottomDivider) {
int y = content.getBottom();
canvas.drawRect(0, y, getMeasuredWidth(), y + dividerWidth, dividerPaint);
}
}
}
@Override
protected void onLayout(boolean changed, final int l, int t, final int r, int b) {
if (isVisible(titleBar)) {
int height = titleBar.getMeasuredHeight();
titleBar.layout(l, t, r, t + height);
t += height;
} else if (!noTitleNoPadding && useFullPadding) {
t += noTitlePaddingFull;
}
if (isVisible(content)) {
content.layout(l, t, r, t + content.getMeasuredHeight());
}
if (isStacked) {
b -= buttonPaddingFull;
for (MDButton mButton : buttons) {
if (isVisible(mButton)) {
mButton.layout(l, b - mButton.getMeasuredHeight(), r, b);
b -= mButton.getMeasuredHeight();
}
}
} else {
int barTop;
int barBottom = b;
if (useFullPadding) {
barBottom -= buttonPaddingFull;
}
barTop = barBottom - buttonBarHeight;
/* START:
Neutral Negative Positive
CENTER:
Negative Neutral Positive
END:
Positive Negative Neutral
(With no Positive, Negative takes it's place except for CENTER)
*/
int offset = buttonHorizontalEdgeMargin;
/* Used with CENTER gravity */
int neutralLeft = -1;
int neutralRight = -1;
if (isVisible(buttons[INDEX_POSITIVE])) {
int bl, br;
if (buttonGravity == GravityEnum.END) {
bl = l + offset;
br = bl + buttons[INDEX_POSITIVE].getMeasuredWidth();
} else {
/* START || CENTER */
br = r - offset;
bl = br - buttons[INDEX_POSITIVE].getMeasuredWidth();
neutralRight = bl;
}
buttons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom);
offset += buttons[INDEX_POSITIVE].getMeasuredWidth();
}
if (isVisible(buttons[INDEX_NEGATIVE])) {
int bl, br;
if (buttonGravity == GravityEnum.END) {
bl = l + offset;
br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth();
} else if (buttonGravity == GravityEnum.START) {
br = r - offset;
bl = br - buttons[INDEX_NEGATIVE].getMeasuredWidth();
} else {
/* CENTER */
bl = l + buttonHorizontalEdgeMargin;
br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth();
neutralLeft = br;
}
buttons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom);
}
if (isVisible(buttons[INDEX_NEUTRAL])) {
int bl, br;
if (buttonGravity == GravityEnum.END) {
br = r - buttonHorizontalEdgeMargin;
bl = br - buttons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (buttonGravity == GravityEnum.START) {
bl = l + buttonHorizontalEdgeMargin;
br = bl + buttons[INDEX_NEUTRAL].getMeasuredWidth();
} else {
/* CENTER */
if (neutralLeft == -1 && neutralRight != -1) {
neutralLeft = neutralRight - buttons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (neutralRight == -1 && neutralLeft != -1) {
neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (neutralRight == -1) {
neutralLeft = (r - l) / 2 - buttons[INDEX_NEUTRAL].getMeasuredWidth() / 2;
neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth();
}
bl = neutralLeft;
br = neutralRight;
}
buttons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom);
}
}
setUpDividersVisibility(content, true, true);
}
public void setStackingBehavior(StackingBehavior behavior) {
stackBehavior = behavior;
invalidate();
}
public void setDividerColor(int color) {
dividerPaint.setColor(color);
invalidate();
}
public void setButtonGravity(GravityEnum gravity) {
buttonGravity = gravity;
invertGravityIfNecessary();
}
private void invertGravityIfNecessary() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return;
}
Configuration config = getResources().getConfiguration();
if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
switch (buttonGravity) {
case START:
buttonGravity = GravityEnum.END;
break;
case END:
buttonGravity = GravityEnum.START;
break;
}
}
}
public void setButtonStackedGravity(GravityEnum gravity) {
for (MDButton mButton : buttons) {
if (mButton != null) {
mButton.setStackedGravity(gravity);
}
}
}
private void setUpDividersVisibility(
final View view, final boolean setForTop, final boolean setForBottom) {
if (view == null) {
return;
}
if (view instanceof ScrollView) {
final ScrollView sv = (ScrollView) view;
if (canScrollViewScroll(sv)) {
addScrollListener(sv, setForTop, setForBottom);
} else {
if (setForTop) {
drawTopDivider = false;
}
if (setForBottom) {
drawBottomDivider = false;
}
}
} else if (view instanceof AdapterView) {
final AdapterView sv = (AdapterView) view;
if (canAdapterViewScroll(sv)) {
addScrollListener(sv, setForTop, setForBottom);
} else {
if (setForTop) {
drawTopDivider = false;
}
if (setForBottom) {
drawBottomDivider = false;
}
}
} else if (view instanceof WebView) {
view.getViewTreeObserver()
.addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (view.getMeasuredHeight() != 0) {
if (!canWebViewScroll((WebView) view)) {
if (setForTop) {
drawTopDivider = false;
}
if (setForBottom) {
drawBottomDivider = false;
}
} else {
addScrollListener((ViewGroup) view, setForTop, setForBottom);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
} else if (view instanceof RecyclerView) {
boolean canScroll = canRecyclerViewScroll((RecyclerView) view);
if (setForTop) {
drawTopDivider = canScroll;
}
if (setForBottom) {
drawBottomDivider = canScroll;
}
if (canScroll) {
addScrollListener((ViewGroup) view, setForTop, setForBottom);
}
} else if (view instanceof ViewGroup) {
View topView = getTopView((ViewGroup) view);
setUpDividersVisibility(topView, setForTop, setForBottom);
View bottomView = getBottomView((ViewGroup) view);
if (bottomView != topView) {
setUpDividersVisibility(bottomView, false, true);
}
}
}
private void addScrollListener(
final ViewGroup vg, final boolean setForTop, final boolean setForBottom) {
if ((!setForBottom && topOnScrollChangedListener == null
|| (setForBottom && bottomOnScrollChangedListener == null))) {
if (vg instanceof RecyclerView) {
RecyclerView.OnScrollListener scrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
boolean hasButtons = false;
for (MDButton button : buttons) {
if (button != null && button.getVisibility() != View.GONE) {
hasButtons = true;
break;
}
}
invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons);
invalidate();
}
};
((RecyclerView) vg).addOnScrollListener(scrollListener);
scrollListener.onScrolled((RecyclerView) vg, 0, 0);
} else {
ViewTreeObserver.OnScrollChangedListener onScrollChangedListener =
new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
boolean hasButtons = false;
for (MDButton button : buttons) {
if (button != null && button.getVisibility() != View.GONE) {
hasButtons = true;
break;
}
}
if (vg instanceof WebView) {
invalidateDividersForWebView((WebView) vg, setForTop, setForBottom, hasButtons);
} else {
invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons);
}
invalidate();
}
};
if (!setForBottom) {
topOnScrollChangedListener = onScrollChangedListener;
vg.getViewTreeObserver().addOnScrollChangedListener(topOnScrollChangedListener);
} else {
bottomOnScrollChangedListener = onScrollChangedListener;
vg.getViewTreeObserver().addOnScrollChangedListener(bottomOnScrollChangedListener);
}
onScrollChangedListener.onScrollChanged();
}
}
}
private void invalidateDividersForScrollingView(
ViewGroup view, final boolean setForTop, boolean setForBottom, boolean hasButtons) {
if (setForTop && view.getChildCount() > 0) {
drawTopDivider =
titleBar != null
&& titleBar.getVisibility() != View.GONE
&&
//Not scrolled to the top.
view.getScrollY() + view.getPaddingTop() > view.getChildAt(0).getTop();
}
if (setForBottom && view.getChildCount() > 0) {
drawBottomDivider =
hasButtons
&& view.getScrollY() + view.getHeight() - view.getPaddingBottom()
< view.getChildAt(view.getChildCount() - 1).getBottom();
}
}
private void invalidateDividersForWebView(
WebView view, final boolean setForTop, boolean setForBottom, boolean hasButtons) {
if (setForTop) {
drawTopDivider =
titleBar != null
&& titleBar.getVisibility() != View.GONE
&&
//Not scrolled to the top.
view.getScrollY() + view.getPaddingTop() > 0;
}
if (setForBottom) {
//noinspection deprecation
drawBottomDivider =
hasButtons
&& view.getScrollY() + view.getMeasuredHeight() - view.getPaddingBottom()
< view.getContentHeight() * view.getScale();
}
}
}