package com.afollestad.materialdialogs;
import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.ArrayRes;
import android.support.annotation.ColorRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.text.method.LinkMovementMethod;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.TextView;
import com.afollestad.materialdialogs.base.DialogBase;
import java.util.ArrayList;
import java.util.List;
/**
* @author Aidan Follestad (afollestad)
*/
public class MaterialDialog extends DialogBase implements View.OnClickListener {
private Context mContext;
private CharSequence positiveText;
private TextView positiveButton;
private CharSequence neutralText;
private TextView neutralButton;
private CharSequence negativeText;
private TextView negativeButton;
private View view;
private int positiveColor;
private SimpleCallback callback;
private ListCallback listCallback;
private ListCallback listCallbackSingle;
private ListCallbackMulti listCallbackMulti;
private View customView;
private float buttonHeight;
private String[] items;
private boolean isStacked;
private Typeface regularFont;
private Typeface mediumFont;
MaterialDialog(Builder builder) {
super(new ContextThemeWrapper(builder.context, builder.theme == Theme.LIGHT ? R.style.Light : R.style.Dark));
this.mContext = builder.context;
this.view = LayoutInflater.from(builder.context).inflate(R.layout.material_dialog, null);
this.customView = builder.customView;
this.callback = builder.callback;
this.listCallback = builder.listCallback;
this.listCallbackSingle = builder.listCallbackSingle;
this.listCallbackMulti = builder.listCallbackMulti;
this.positiveText = builder.positiveText;
this.neutralText = builder.neutralText;
this.negativeText = builder.negativeText;
this.positiveColor = builder.positiveColor;
this.items = builder.items;
this.setCancelable(builder.cancelable);
this.regularFont = Typeface.createFromAsset(getContext().getResources().getAssets(), "Roboto-Regular.ttf");
this.mediumFont = Typeface.createFromAsset(getContext().getResources().getAssets(), "Roboto-Medium.ttf");
TextView title = (TextView) view.findViewById(R.id.title);
TextView content = (TextView) view.findViewById(R.id.content);
content.setText(builder.content);
content.setMovementMethod(new LinkMovementMethod());
content.setVisibility(View.VISIBLE);
content.setTypeface(regularFont);
content.setTextColor(Utils.resolveColor(getContext(), R.attr.content_color));
content.setLineSpacing(0f, builder.contentLineSpacingMultiplier);
if (this.positiveColor == 0) {
content.setLinkTextColor(Utils.resolveColor(getContext(), R.attr.button_color));
} else {
content.setLinkTextColor(this.positiveColor);
}
if (builder.contentAlignment == Alignment.CENTER) {
content.setGravity(Gravity.CENTER_HORIZONTAL);
} else if (builder.contentAlignment == Alignment.RIGHT) {
content.setGravity(Gravity.RIGHT);
}
if (customView != null) {
title = (TextView) view.findViewById(R.id.titleCustomView);
buttonHeight = mContext.getResources().getDimension(R.dimen.button_height_customview);
view.findViewById(R.id.mainFrame).setVisibility(View.GONE);
view.findViewById(R.id.customViewScroll).setVisibility(View.VISIBLE);
view.findViewById(R.id.customViewDivider).setVisibility(View.VISIBLE);
view.findViewById(R.id.customViewDivider).setBackgroundColor(Utils.resolveColor(getContext(), R.attr.divider_color));
View firstChild = customView;
View lastChild = customView;
final int frameMargin = (int) mContext.getResources().getDimension(R.dimen.dialog_frame_margin);
if (customView instanceof ViewGroup) {
ViewGroup group = (ViewGroup) customView;
firstChild = group.getChildAt(0);
lastChild = group.getChildAt(group.getChildCount() - 1);
}
if (builder.title == null || builder.title.toString().trim().isEmpty())
setMargin(firstChild, frameMargin, -1, -1, -1);
setMargin(lastChild, -1, frameMargin, -1, -1);
((LinearLayout) view.findViewById(R.id.customViewFrame)).addView(customView);
} else {
buttonHeight = mContext.getResources().getDimension(R.dimen.button_height);
view.findViewById(R.id.mainFrame).setVisibility(View.VISIBLE);
view.findViewById(R.id.customViewScroll).setVisibility(View.GONE);
view.findViewById(R.id.customViewDivider).setVisibility(View.GONE);
}
// Title is set after it's determined whether to use first title or custom view title
if (builder.title == null || builder.title.toString().trim().isEmpty()) {
title.setVisibility(View.GONE);
} else {
title.setText(builder.title);
title.setTypeface(mediumFont);
if (builder.titleColor != -1) {
title.setTextColor(builder.titleColor);
} else {
title.setTextColor(Utils.resolveColor(getContext(), R.attr.title_color));
}
if (builder.titleAlignment == Alignment.CENTER) {
title.setGravity(Gravity.CENTER_HORIZONTAL);
} else if (builder.titleAlignment == Alignment.RIGHT) {
title.setGravity(Gravity.RIGHT);
}
}
invalidateList();
invalidateActions();
checkIfStackingNeeded();
setViewInternal(view);
}
/**
* Invalidates the radio buttons in the single choice mode list so that only the radio button that
* was previous selected is checked.
*/
private void invalidateSingleChoice(int newSelection) {
LinearLayout list = (LinearLayout) view.findViewById(R.id.listFrame);
for (int i = 0; i < list.getChildCount(); i++) {
View v = list.getChildAt(i);
RadioButton rb = (RadioButton) v.findViewById(R.id.control);
rb.setChecked(newSelection == i);
}
}
/**
* Constructs the dialog's list content and sets up click listeners.
*/
private void invalidateList() {
if (items == null || items.length == 0) return;
view.findViewById(R.id.content).setVisibility(View.GONE);
View title = view.findViewById(R.id.title);
setMargin(title, -1, (int) mContext.getResources().getDimension(R.dimen.button_padding), 0, 0);
View mainFrame = view.findViewById(R.id.mainFrame);
int dpPadding = (int) mContext.getResources().getDimension(R.dimen.button_frame_margin);
setMargin(mainFrame, dpPadding, 0, dpPadding, dpPadding);
LinearLayout list = (LinearLayout) view.findViewById(R.id.listFrame);
list.setVisibility(View.VISIBLE);
LayoutInflater li = LayoutInflater.from(mContext);
final int itemColor = Utils.resolveColor(getContext(), R.attr.item_color);
for (int index = 0; index < items.length; index++) {
View il;
if (listCallbackSingle != null) {
il = li.inflate(R.layout.dialog_listitem_singlechoice, null);
} else if (listCallbackMulti != null) {
il = li.inflate(R.layout.dialog_listitem_multichoice, null);
} else {
il = li.inflate(R.layout.dialog_listitem, null);
}
TextView tv = (TextView) il.findViewById(R.id.title);
tv.setText(items[index]);
tv.setTextColor(itemColor);
il.setTag(index + ":" + items[index]);
il.setOnClickListener(this);
list.addView(il);
}
}
/**
* Measures the action button's and their text to decide whether or not the button should be stacked.
*/
private void checkIfStackingNeeded() {
if (((negativeButton == null || negativeButton.getVisibility() == View.GONE) &&
(neutralButton == null || neutralButton.getVisibility() == View.GONE))) {
// Stacking isn't necessary if you only have one button
return;
}
Paint paint = positiveButton.getPaint();
float buttonMinWidth = mContext.getResources().getDimension(R.dimen.button_min_width);
float totalWidth = paint.measureText(positiveButton.getText().toString());
if (this.neutralText != null)
totalWidth += paint.measureText(neutralButton.getText().toString());
if (this.negativeText != null)
totalWidth += paint.measureText(negativeButton.getText().toString());
isStacked = totalWidth > (buttonMinWidth * 3);
invalidateActions();
}
/**
* Invalidates the positive/neutral/negative action buttons. Decides whether they should be visible
* and sets their properties (such as height, text color, etc.).
*/
private void invalidateActions() {
if (items != null && listCallbackSingle == null && listCallbackMulti == null) {
// If the dialog is a plain list dialog, no buttons are shown.
view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
return;
}
final int buttonFrameMargin = (int) mContext.getResources().getDimension(R.dimen.button_frame_margin);
if (isStacked) {
view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
view.findViewById(R.id.buttonStackedFrame).setVisibility(View.VISIBLE);
if (customView == null)
setMargin(view.findViewById(R.id.buttonStackedFrame), -1, buttonFrameMargin, -1, -1);
} else {
view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.VISIBLE);
view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
if (customView == null)
setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, buttonFrameMargin, -1, -1);
}
positiveButton = (TextView) view.findViewById(
isStacked ? R.id.buttonStackedPositive : R.id.buttonDefaultPositive);
setHeight(positiveButton, buttonHeight);
positiveButton.setTypeface(mediumFont);
if (this.positiveText == null)
this.positiveText = mContext.getString(R.string.accept);
positiveButton.setText(this.positiveText);
positiveButton.setTextColor(getActionTextStateList(this.positiveColor));
positiveButton.setTag(POSITIVE);
positiveButton.setOnClickListener(this);
neutralButton = (TextView) view.findViewById(
isStacked ? R.id.buttonStackedNeutral : R.id.buttonDefaultNeutral);
setHeight(neutralButton, buttonHeight);
neutralButton.setTypeface(mediumFont);
if (this.neutralText != null) {
neutralButton.setVisibility(View.VISIBLE);
neutralButton.setTextColor(getActionTextStateList(0));
neutralButton.setText(this.neutralText);
neutralButton.setTag(NEUTRAL);
neutralButton.setOnClickListener(this);
} else {
neutralButton.setVisibility(View.GONE);
}
negativeButton = (TextView) view.findViewById(
isStacked ? R.id.buttonStackedNegative : R.id.buttonDefaultNegative);
setHeight(negativeButton, buttonHeight);
negativeButton.setTypeface(mediumFont);
if (this.negativeText != null) {
negativeButton.setVisibility(View.VISIBLE);
negativeButton.setTextColor(getActionTextStateList(0));
negativeButton.setText(this.negativeText);
negativeButton.setTag(NEGATIVE);
negativeButton.setOnClickListener(this);
} else {
negativeButton.setVisibility(View.GONE);
}
}
@Override
public final void onClick(View v) {
String tag = (String) v.getTag();
if (tag.equals(POSITIVE)) {
if (listCallbackSingle != null) {
dismiss();
LinearLayout list = (LinearLayout) view.findViewById(R.id.listFrame);
for (int i = 0; i < list.getChildCount(); i++) {
View itemView = list.getChildAt(i);
RadioButton rb = (RadioButton) itemView.findViewById(R.id.control);
if (rb.isChecked()) {
listCallbackSingle.onSelection(this, i, ((TextView) itemView.findViewById(R.id.title)).getText().toString());
break;
}
}
} else if (listCallbackMulti != null) {
dismiss();
List<Integer> selectedIndices = new ArrayList<Integer>();
List<String> selectedTitles = new ArrayList<String>();
LinearLayout list = (LinearLayout) view.findViewById(R.id.listFrame);
for (int i = 0; i < list.getChildCount(); i++) {
View itemView = list.getChildAt(i);
CheckBox rb = (CheckBox) itemView.findViewById(R.id.control);
if (rb.isChecked()) {
selectedIndices.add(i);
selectedTitles.add(((TextView) itemView.findViewById(R.id.title)).getText().toString());
}
}
listCallbackMulti.onSelection(this,
selectedIndices.toArray(new Integer[selectedIndices.size()]),
selectedTitles.toArray(new String[selectedTitles.size()]));
} else if (callback != null) {
dismiss();
callback.onPositive(this);
}
} else if (tag.equals(NEGATIVE)) {
if (callback != null && callback instanceof Callback) {
dismiss();
((Callback) callback).onNegative(this);
}
} else if (tag.equals(NEUTRAL)) {
if (callback != null && callback instanceof FullCallback) {
dismiss();
((FullCallback) callback).onNeutral(this);
}
} else {
String[] split = tag.split(":");
int index = Integer.parseInt(split[0]);
if (listCallback != null) {
dismiss();
listCallback.onSelection(this, index, split[1]);
} else if (listCallbackSingle != null) {
RadioButton cb = (RadioButton) ((LinearLayout) v).getChildAt(0);
cb.performClick();
invalidateSingleChoice(index);
} else if (listCallbackMulti != null) {
CheckBox cb = (CheckBox) ((LinearLayout) v).getChildAt(0);
cb.performClick();
}
}
}
/**
* The class used to construct a MaterialDialog.
*/
public static class Builder {
protected Activity context;
protected CharSequence title;
protected Alignment titleAlignment = Alignment.LEFT;
protected Alignment contentAlignment = Alignment.LEFT;
protected int titleColor = -1;
protected CharSequence content;
protected String[] items;
protected CharSequence positiveText;
protected CharSequence neutralText;
protected CharSequence negativeText;
protected View customView;
protected int positiveColor;
protected SimpleCallback callback;
protected ListCallback listCallback;
protected ListCallback listCallbackSingle;
private ListCallbackMulti listCallbackMulti;
protected Theme theme = Theme.LIGHT;
protected boolean cancelable = true;
protected float contentLineSpacingMultiplier = 1.0f;
public Builder(@NonNull Activity context) {
this.context = context;
this.positiveText = context.getString(R.string.accept);
final int materialBlue = context.getResources().getColor(R.color.material_blue_500);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
try {
this.positiveColor = a.getColor(0, materialBlue);
} finally {
a.recycle();
}
} else {
this.positiveColor = materialBlue;
}
}
public Builder title(@StringRes int titleRes) {
title(this.context.getString(titleRes));
return this;
}
public Builder title(CharSequence title) {
this.title = title;
return this;
}
public Builder titleAlignment(Alignment align) {
this.titleAlignment = align;
return this;
}
public Builder titleColorRes(@ColorRes int colorRes) {
titleColor(this.context.getResources().getColor(colorRes));
return this;
}
public Builder titleColor(int color) {
this.titleColor = color;
return this;
}
public Builder content(@StringRes int contentRes) {
content(this.context.getString(contentRes));
return this;
}
public Builder content(CharSequence content) {
this.content = content;
return this;
}
public Builder content(@StringRes int contentRes, Object... formatArgs) {
content(this.context.getString(contentRes, formatArgs));
return this;
}
public Builder contentAlignment(Alignment align) {
this.contentAlignment = align;
return this;
}
public Builder contentLineSpacing(float multiplier) {
this.contentLineSpacingMultiplier = multiplier;
return this;
}
public Builder items(@ArrayRes int itemsRes) {
items(this.context.getResources().getStringArray(itemsRes));
return this;
}
public Builder items(String[] items) {
this.items = items;
return this;
}
public Builder itemsCallback(ListCallback callback) {
this.listCallback = callback;
this.listCallbackSingle = null;
this.listCallbackMulti = null;
return this;
}
public Builder itemsCallbackSingleChoice(ListCallback callback) {
this.listCallback = null;
this.listCallbackSingle = callback;
this.listCallbackMulti = null;
return this;
}
public Builder itemsCallbackMultiChoice(ListCallbackMulti callback) {
this.listCallback = null;
this.listCallbackSingle = null;
this.listCallbackMulti = callback;
return this;
}
public Builder positiveText(@StringRes int postiveRes) {
positiveText(this.context.getString(postiveRes));
return this;
}
public Builder positiveText(CharSequence message) {
this.positiveText = message;
return this;
}
public Builder neutralText(@StringRes int neutralRes) {
neutralText(this.context.getString(neutralRes));
return this;
}
public Builder neutralText(CharSequence message) {
this.neutralText = message;
return this;
}
public Builder negativeText(@StringRes int negativeRes) {
negativeText(this.context.getString(negativeRes));
return this;
}
public Builder negativeText(CharSequence message) {
this.negativeText = message;
return this;
}
public Builder customView(@LayoutRes int layoutRes) {
LayoutInflater li = LayoutInflater.from(this.context);
customView(li.inflate(layoutRes, null));
return this;
}
public Builder customView(View view) {
this.customView = view;
return this;
}
public Builder positiveColorRes(@ColorRes int colorRes) {
positiveColor(this.context.getResources().getColor(colorRes));
return this;
}
public Builder positiveColor(int color) {
this.positiveColor = color;
return this;
}
public Builder callback(SimpleCallback callback) {
this.callback = callback;
return this;
}
public Builder theme(Theme theme) {
this.theme = theme;
return this;
}
public Builder cancelable(boolean cancelable) {
this.cancelable = cancelable;
return this;
}
public MaterialDialog build() {
return new MaterialDialog(this);
}
}
private ColorStateList getActionTextStateList(int newPrimaryColor) {
final int buttonColor = Utils.resolveColor(getContext(), R.attr.button_color);
if (newPrimaryColor == 0) newPrimaryColor = buttonColor;
int[][] states = new int[][]{
new int[]{-android.R.attr.state_enabled}, // disabled
new int[]{} // enabled
};
int[] colors = new int[]{
Utils.adjustAlpha(buttonColor, 0.6f),
newPrimaryColor
};
return new ColorStateList(states, colors);
}
/**
* Retrieves the view of an action button, allowing you to modify properties such as whether or not it's enabled.
*/
public final View getActionButton(DialogAction which) {
if (view == null) return null;
if (isStacked) {
switch (which) {
default:
return view.findViewById(R.id.buttonStackedPositive);
case NEUTRAL:
return view.findViewById(R.id.buttonStackedNeutral);
case NEGATIVE:
return view.findViewById(R.id.buttonStackedNegative);
}
} else {
switch (which) {
default:
return view.findViewById(R.id.buttonDefaultPositive);
case NEUTRAL:
return view.findViewById(R.id.buttonDefaultNeutral);
case NEGATIVE:
return view.findViewById(R.id.buttonDefaultNegative);
}
}
}
/**
* Updates an action button's title, causing invalidation to check if the action buttons should be stacked.
*
* @param which The action button to update.
* @param title The new title of the action button.
*/
public final void setActionButton(DialogAction which, CharSequence title) {
switch (which) {
default:
this.positiveText = title;
break;
case NEUTRAL:
this.neutralText = title;
break;
case NEGATIVE:
this.negativeText = title;
break;
}
invalidateActions();
}
/**
* Updates an action button's title, causing invalidation to check if the action buttons should be stacked.
*
* @param which The action button to update.
* @param titleRes The string resource of the new title of the action button.
*/
public final void setActionButton(DialogAction which, @StringRes int titleRes) {
setActionButton(which, mContext.getString(titleRes));
}
/**
* Retrieves the custom view that was inflated or set to the MaterialDialog during building.
*/
public final View getCustomView() {
return customView;
}
public static interface ListCallback {
void onSelection(MaterialDialog dialog, int which, String text);
}
public static interface ListCallbackMulti {
void onSelection(MaterialDialog dialog, Integer[] which, String[] text);
}
public static interface SimpleCallback {
void onPositive(MaterialDialog dialog);
}
public static interface Callback extends SimpleCallback {
void onPositive(MaterialDialog dialog);
void onNegative(MaterialDialog dialog);
}
public static interface FullCallback extends Callback {
void onNeutral(MaterialDialog dialog);
}
}