package com.afollestad.materialdialogs.internal;
import android.annotation.TargetApi;
import android.content.Context;
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.LinearLayoutManager;
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.MaterialDialog;
import com.afollestad.materialdialogs.R;
import com.afollestad.materialdialogs.util.DialogUtils;
/**
* @author Kevin Barry (teslacoil) 4/02/2015
* <p/>
* 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 View mTitleBar;
private View mContent;
private static final int INDEX_NEUTRAL = 0;
private static final int INDEX_NEGATIVE = 1;
private static final int INDEX_POSITIVE = 2;
private boolean mDrawTopDivider = false;
private boolean mDrawBottomDivider = false;
private MDButton[] mButtons = new MDButton[3];
private boolean mForceStack = false;
private boolean mIsStacked = false;
private boolean mUseFullPadding = true;
private boolean mReducePaddingNoTitleNoButtons;
private boolean mNoTitleNoPadding;
private int mNoTitlePaddingFull;
private int mButtonPaddingFull;
private int mButtonBarHeight;
private GravityEnum mButtonGravity = GravityEnum.START;
/* Margin from dialog frame to first button */
private int mButtonHorizontalEdgeMargin;
private Paint mDividerPaint;
private ViewTreeObserver.OnScrollChangedListener mTopOnScrollChangedListener;
private ViewTreeObserver.OnScrollChangedListener mBottomOnScrollChangedListener;
private int mDividerWidth;
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 void init(Context context, AttributeSet attrs, int defStyleAttr) {
Resources r = context.getResources();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MDRootLayout, defStyleAttr, 0);
mReducePaddingNoTitleNoButtons = a.getBoolean(R.styleable.MDRootLayout_md_reduce_padding_no_title_no_buttons, true);
a.recycle();
mNoTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding);
mButtonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding);
mButtonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side);
mButtonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height);
mDividerPaint = new Paint();
mDividerWidth = r.getDimensionPixelSize(R.dimen.md_divider_height);
mDividerPaint.setColor(DialogUtils.resolveColor(context, R.attr.md_divider_color));
setWillNotDraw(false);
}
public void noTitleNoPadding() {
mNoTitleNoPadding = true;
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
if (v.getId() == R.id.titleFrame) {
mTitleBar = v;
} else if (v.getId() == R.id.buttonDefaultNeutral) {
mButtons[INDEX_NEUTRAL] = (MDButton) v;
} else if (v.getId() == R.id.buttonDefaultNegative) {
mButtons[INDEX_NEGATIVE] = (MDButton) v;
} else if (v.getId() == R.id.buttonDefaultPositive) {
mButtons[INDEX_POSITIVE] = (MDButton) v;
} else {
mContent = v;
}
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
mUseFullPadding = true;
boolean hasButtons = false;
final boolean stacked;
if (!mForceStack) {
int buttonsWidth = 0;
for (MDButton button : mButtons) {
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;
} else {
stacked = true;
}
int stackedHeight = 0;
mIsStacked = stacked;
if (stacked) {
for (MDButton button : mButtons) {
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 (mIsStacked) {
availableHeight -= stackedHeight;
fullPadding += 2 * mButtonPaddingFull;
minPadding += 2 * mButtonPaddingFull;
} else {
availableHeight -= mButtonBarHeight;
fullPadding += 2 * mButtonPaddingFull;
/* No minPadding */
}
} else {
/* Content has 8dp, we add 16dp and get 24dp, the frame margin */
fullPadding += 2 * mButtonPaddingFull;
}
if (isVisible(mTitleBar)) {
mTitleBar.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.UNSPECIFIED);
availableHeight -= mTitleBar.getMeasuredHeight();
} else if(!mNoTitleNoPadding) {
fullPadding += mNoTitlePaddingFull;
}
if (isVisible(mContent)) {
mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST));
if (mContent.getMeasuredHeight() <= availableHeight - fullPadding) {
if (!mReducePaddingNoTitleNoButtons || isVisible(mTitleBar) || hasButtons) {
mUseFullPadding = true;
availableHeight -= mContent.getMeasuredHeight() + fullPadding;
} else {
mUseFullPadding = false;
availableHeight -= mContent.getMeasuredHeight() + minPadding;
}
} else {
mUseFullPadding = false;
availableHeight = 0;
}
}
setMeasuredDimension(width, height - availableHeight);
}
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;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mContent != null) {
if (mDrawTopDivider) {
int y = mContent.getTop();
canvas.drawRect(0, y - mDividerWidth, getMeasuredWidth(), y, mDividerPaint);
}
if (mDrawBottomDivider) {
int y = mContent.getBottom();
canvas.drawRect(0, y, getMeasuredWidth(), y + mDividerWidth, mDividerPaint);
}
}
}
@Override
protected void onLayout(boolean changed, final int l, int t, final int r, int b) {
if (isVisible(mTitleBar)) {
int height = mTitleBar.getMeasuredHeight();
mTitleBar.layout(l, t, r, t + height);
t += height;
} else if (!mNoTitleNoPadding && mUseFullPadding) {
t += mNoTitlePaddingFull;
}
if (isVisible(mContent))
mContent.layout(l, t, r, t + mContent.getMeasuredHeight());
if (mIsStacked) {
b -= mButtonPaddingFull;
for (MDButton mButton : mButtons) {
if (isVisible(mButton)) {
mButton.layout(l, b - mButton.getMeasuredHeight(), r, b);
b -= mButton.getMeasuredHeight();
}
}
} else {
int barTop;
int barBottom = b;
if (mUseFullPadding)
barBottom -= mButtonPaddingFull;
barTop = barBottom - mButtonBarHeight;
/* 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 = mButtonHorizontalEdgeMargin;
/* Used with CENTER gravity */
int neutralLeft = -1;
int neutralRight = -1;
if (isVisible(mButtons[INDEX_POSITIVE])) {
int bl, br;
if (mButtonGravity == GravityEnum.END) {
bl = l + offset;
br = bl + mButtons[INDEX_POSITIVE].getMeasuredWidth();
} else { /* START || CENTER */
br = r - offset;
bl = br - mButtons[INDEX_POSITIVE].getMeasuredWidth();
neutralRight = bl;
}
mButtons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom);
offset += mButtons[INDEX_POSITIVE].getMeasuredWidth();
}
if (isVisible(mButtons[INDEX_NEGATIVE])) {
int bl, br;
if (mButtonGravity == GravityEnum.END) {
bl = l + offset;
br = bl + mButtons[INDEX_NEGATIVE].getMeasuredWidth();
} else if (mButtonGravity == GravityEnum.START) {
br = r - offset;
bl = br - mButtons[INDEX_NEGATIVE].getMeasuredWidth();
} else { /* CENTER */
bl = l + mButtonHorizontalEdgeMargin;
br = bl + mButtons[INDEX_NEGATIVE].getMeasuredWidth();
neutralLeft = br;
}
mButtons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom);
}
if (isVisible(mButtons[INDEX_NEUTRAL])) {
int bl, br;
if (mButtonGravity == GravityEnum.END) {
br = r - mButtonHorizontalEdgeMargin;
bl = br - mButtons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (mButtonGravity == GravityEnum.START) {
bl = l + mButtonHorizontalEdgeMargin;
br = bl + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
} else { /* CENTER */
if (neutralLeft == -1 && neutralRight != -1) {
neutralLeft = neutralRight - mButtons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (neutralRight == -1 && neutralLeft != -1) {
neutralRight = neutralLeft + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
} else if (neutralRight == -1) {
neutralLeft = (r - l) / 2 - mButtons[INDEX_NEUTRAL].getMeasuredWidth() / 2;
neutralRight = neutralLeft + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
}
bl = neutralLeft;
br = neutralRight;
}
mButtons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom);
}
}
setUpDividersVisibility(mContent, true, true);
}
public void setForceStack(boolean forceStack) {
mForceStack = forceStack;
invalidate();
}
public void setDividerColor(int color) {
mDividerPaint.setColor(color);
invalidate();
}
public void setButtonGravity(GravityEnum gravity) {
mButtonGravity = gravity;
}
public void setButtonStackedGravity(GravityEnum gravity) {
for (MDButton mButton : mButtons) {
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)
mDrawTopDivider = false;
if (setForBottom)
mDrawBottomDivider = false;
}
} else if (view instanceof AdapterView) {
final AdapterView sv = (AdapterView) view;
if (canAdapterViewScroll(sv)) {
addScrollListener(sv, setForTop, setForBottom);
} else {
if (setForTop)
mDrawTopDivider = false;
if (setForBottom)
mDrawBottomDivider = 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)
mDrawTopDivider = false;
if (setForBottom)
mDrawBottomDivider = false;
} else {
addScrollListener((ViewGroup) view, setForTop, setForBottom);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
} else if (view instanceof RecyclerView) {
/* Scroll offset detection for RecyclerView isn't reliable b/c the OnScrollChangedListener
isn't always called on scroll. We can't set a OnScrollListener either because that will
override the user's OnScrollListener if they set one.*/
boolean canScroll = canRecyclerViewScroll((RecyclerView) view);
if (setForTop)
mDrawTopDivider = canScroll;
if (setForBottom)
mDrawBottomDivider = canScroll;
} 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 && mTopOnScrollChangedListener == null
|| (setForBottom && mBottomOnScrollChangedListener == null))) {
ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
boolean hasButtons = false;
for (MDButton button : mButtons) {
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) {
mTopOnScrollChangedListener = onScrollChangedListener;
vg.getViewTreeObserver().addOnScrollChangedListener(mTopOnScrollChangedListener);
} else {
mBottomOnScrollChangedListener = onScrollChangedListener;
vg.getViewTreeObserver().addOnScrollChangedListener(mBottomOnScrollChangedListener);
}
onScrollChangedListener.onScrollChanged();
}
}
private void invalidateDividersForScrollingView(ViewGroup view, final boolean setForTop, boolean setForBottom, boolean hasButtons) {
if (setForTop && view.getChildCount() > 0) {
mDrawTopDivider = mTitleBar != null &&
mTitleBar.getVisibility() != View.GONE &&
//Not scrolled to the top.
view.getScrollY() + view.getPaddingTop() > view.getChildAt(0).getTop();
}
if (setForBottom && view.getChildCount() > 0) {
mDrawBottomDivider = 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) {
mDrawTopDivider = mTitleBar != null &&
mTitleBar.getVisibility() != View.GONE &&
//Not scrolled to the top.
view.getScrollY() + view.getPaddingTop() > 0;
}
if (setForBottom) {
//noinspection deprecation
mDrawBottomDivider = hasButtons &&
view.getScrollY() + view.getMeasuredHeight() - view.getPaddingBottom() < view.getContentHeight() * view.getScale();
}
}
public static boolean canRecyclerViewScroll(RecyclerView view) {
final RecyclerView.LayoutManager lm = view.getLayoutManager();
final int count = view.getAdapter().getItemCount();
int lastVisible;
if (lm instanceof LinearLayoutManager) {
LinearLayoutManager llm = (LinearLayoutManager) lm;
lastVisible = llm.findLastVisibleItemPosition();
} else {
throw new MaterialDialog.NotImplementedException("Material Dialogs currently only supports LinearLayoutManager. Please report any new layout managers.");
}
if (lastVisible == -1)
return false;
/* We scroll if the last item is not visible */
final boolean lastItemVisible = lastVisible == count - 1;
return !lastItemVisible ||
(view.getChildCount() > 0 && view.getChildAt(view.getChildCount() - 1).getBottom() > view.getHeight() - view.getPaddingBottom());
}
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;
}
}