package com.afollestad.materialdialogs; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.ArrayRes; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; import com.afollestad.materialdialogs.base.DialogBase; import com.afollestad.materialdialogs.views.MeasureCallbackListView; import com.afollestad.materialdialogs.views.MeasureCallbackScrollView; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author Aidan Follestad (afollestad) */ public class MaterialDialog extends DialogBase implements View.OnClickListener, MeasureCallbackScrollView.Callback, MeasureCallbackListView.Callback { private ImageView icon; private TextView title; private View titleFrame; private final int defaultItemColor; 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 ListView listView; private int positiveColor; private int negativeColor; private int neutralColor; private ButtonCallback callback; private ListCallback listCallback; private ListCallback listCallbackSingle; private ListCallbackMulti listCallbackMulti; private View customView; private CharSequence[] items; private boolean isStacked; private int selectedIndex; private Integer[] selectedIndices; private boolean mMeasuredScrollView; private Typeface mediumFont; private Typeface regularFont; private boolean autoDismiss; private ListAdapter adapter; private ListType listType; private List selectedIndicesList; private boolean forceStacking; protected static ContextThemeWrapper getTheme(Builder builder) { TypedArray a = builder.context.getTheme().obtainStyledAttributes(new int[]{R.attr.md_dark_theme}); boolean darkTheme = builder.theme == Theme.DARK; if (!darkTheme) { try { darkTheme = a.getBoolean(0, false); } finally { a.recycle(); } } return new ContextThemeWrapper(builder.context, darkTheme ? R.style.MD_Dark : R.style.MD_Light); } protected MaterialDialog(Builder builder) { super(getTheme(builder)); this.regularFont = builder.regularFont; if (this.regularFont == null) this.regularFont = TypefaceHelper.get(getContext(), "Roboto-Regular"); this.mediumFont = builder.mediumFont; if (this.mediumFont == null) this.mediumFont = TypefaceHelper.get(getContext(), "Roboto-Medium"); mContext = builder.context; this.view = LayoutInflater.from(getContext()).inflate(R.layout.md_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.items = builder.items; this.setCancelable(builder.cancelable); this.selectedIndex = builder.selectedIndex; this.selectedIndices = builder.selectedIndices; this.autoDismiss = builder.autoDismiss; this.adapter = builder.adapter; this.forceStacking = builder.forceStacking; this.positiveColor = builder.positiveColor; this.negativeColor = builder.negativeColor; this.neutralColor = builder.neutralColor; final int mdAccentColor = DialogUtils.resolveColor(mContext, R.attr.md_accent_color); if (mdAccentColor != 0) { this.positiveColor = mdAccentColor; this.negativeColor = mdAccentColor; this.neutralColor = mdAccentColor; } title = (TextView) view.findViewById(R.id.title); icon = (ImageView) view.findViewById(R.id.icon); titleFrame = view.findViewById(R.id.titleFrame); final TextView content = (TextView) view.findViewById(R.id.content); content.setText(builder.content); content.setMovementMethod(new LinkMovementMethod()); setTypeface(content, regularFont); content.setLineSpacing(0f, builder.contentLineSpacingMultiplier); if (this.positiveColor == 0) { content.setLinkTextColor(DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary)); } 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 (builder.contentColor != -1) { content.setTextColor(builder.contentColor); } else { final int fallback = DialogUtils.resolveColor(getContext(), android.R.attr.textColorSecondary); final int contentColor = DialogUtils.resolveColor(getContext(), R.attr.md_content_color, fallback); content.setTextColor(contentColor); } if (builder.theme == Theme.LIGHT) { defaultItemColor = Color.BLACK; } else { defaultItemColor = Color.WHITE; } if (customView != null) { title = (TextView) view.findViewById(R.id.titleCustomView); icon = (ImageView) view.findViewById(R.id.iconCustomView); titleFrame = view.findViewById(R.id.titleFrameCustomView); invalidateCustomViewAssociations(); ((LinearLayout) view.findViewById(R.id.customViewFrame)).addView(customView); } else { invalidateCustomViewAssociations(); } boolean adapterProvided = adapter != null; if (items != null && items.length > 0 || adapterProvided) { title = (TextView) view.findViewById(R.id.titleCustomView); icon = (ImageView) view.findViewById(R.id.iconCustomView); titleFrame = view.findViewById(R.id.titleFrameCustomView); listView = (ListView) view.findViewById(R.id.contentListView); listView.setSelector(DialogUtils.resolveDrawable(getContext(), R.attr.md_selector)); ((MeasureCallbackListView) listView).setCallback(this); if (!adapterProvided) { // Determine list type if (listCallbackSingle != null) { listType = ListType.SINGLE; } else if (listCallbackMulti != null) { listType = ListType.MULTI; if (selectedIndices != null) { selectedIndicesList = new ArrayList<>(Arrays.asList(selectedIndices)); } else { selectedIndicesList = new ArrayList<>(); } } else { listType = ListType.REGULAR; } adapter = new MaterialDialogAdapter(mContext, ListType.getLayoutForType(listType), R.id.title, items); } adapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { super.onChanged(); listView.post(new Runnable() { @Override public void run() { invalidateCustomViewAssociations(); } }); } }); } if (builder.icon != null) { icon.setVisibility(View.VISIBLE); icon.setImageDrawable(builder.icon); } else { Drawable d = DialogUtils.resolveDrawable(mContext, R.attr.md_icon); if (d != null) { icon.setVisibility(View.VISIBLE); icon.setImageDrawable(d); } else { icon.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().length() == 0) { titleFrame.setVisibility(View.GONE); if (customView == null) view.findViewById(R.id.titleFrameCustomView).setVisibility(View.GONE); } else { title.setText(builder.title); setTypeface(title, mediumFont); if (builder.titleColor != -1) { title.setTextColor(builder.titleColor); } else { final int fallback = DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary); title.setTextColor(DialogUtils.resolveColor(getContext(), R.attr.md_title_color, fallback)); } if (builder.titleAlignment == Alignment.CENTER) { title.setGravity(Gravity.CENTER_HORIZONTAL); } else if (builder.titleAlignment == Alignment.RIGHT) { title.setGravity(Gravity.RIGHT); } } invalidateActions(); setOnShowListenerInternal(); setViewInternal(view); if (builder.theme == Theme.LIGHT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { setInverseBackgroundForced(true); title.setTextColor(Color.BLACK); content.setTextColor(Color.BLACK); } } @Override public void onShow(DialogInterface dialog) { super.onShow(dialog); // calls any external show listeners checkIfStackingNeeded(); invalidateCustomViewAssociations(); } /** * Invalidates visibility of views for the presence of a custom view or list content */ private void invalidateCustomViewAssociations() { if (customView != null || (items != null && items.length > 0) || adapter != null) { view.findViewById(R.id.mainFrame).setVisibility(View.GONE); view.findViewById(R.id.customViewScrollParent).setVisibility(View.VISIBLE); if (!mMeasuredScrollView && listView == null) { // Wait until it's measured ((MeasureCallbackScrollView) view.findViewById(R.id.customViewScroll)).setCallback(this); return; } if (canCustomViewScroll()) { view.findViewById(R.id.customViewDivider).setVisibility(View.VISIBLE); view.findViewById(R.id.customViewDivider).setBackgroundColor(DialogUtils.resolveColor(getContext(), R.attr.md_divider)); setMargin(view.findViewById(R.id.buttonStackedFrame), -1, 0, -1, -1); setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, 0, -1, -1); if (items != null && items.length > 0) { View customFrame = view.findViewById(R.id.customViewFrame); Resources r = getContext().getResources(); int bottomPadding = view.findViewById(R.id.titleCustomView).getVisibility() == View.VISIBLE ? (int) r.getDimension(R.dimen.md_main_frame_margin) : (int) r.getDimension(R.dimen.md_dialog_frame_margin); customFrame.setPadding(customFrame.getPaddingLeft(), customFrame.getPaddingTop(), customFrame.getPaddingRight(), bottomPadding); } } else { view.findViewById(R.id.customViewDivider).setVisibility(View.GONE); final int bottomMargin = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_frame_bottom); setMargin(view.findViewById(R.id.buttonStackedFrame), -1, bottomMargin, -1, -1); setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, bottomMargin, -1, -1); } } else { view.findViewById(R.id.mainFrame).setVisibility(View.VISIBLE); view.findViewById(R.id.customViewScrollParent).setVisibility(View.GONE); view.findViewById(R.id.customViewDivider).setVisibility(View.GONE); if (!mMeasuredScrollView) { // Wait until it's measured ((MeasureCallbackScrollView) view.findViewById(R.id.contentScrollView)).setCallback(this); return; } if (canContentScroll()) { view.findViewById(R.id.customViewDivider).setVisibility(View.VISIBLE); view.findViewById(R.id.customViewDivider).setBackgroundColor(DialogUtils.resolveColor(getContext(), R.attr.md_divider)); setMargin(view.findViewById(R.id.mainFrame), -1, 0, -1, -1); setMargin(view.findViewById(R.id.buttonStackedFrame), -1, 0, -1, -1); setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, 0, -1, -1); final int conPadding = (int) getContext().getResources().getDimension(R.dimen.md_main_frame_margin); View con = view.findViewById(R.id.content); con.setPadding(con.getPaddingLeft(), 0, con.getPaddingRight(), conPadding); } else { View con = view.findViewById(R.id.content); con.setPadding(con.getPaddingLeft(), 0, con.getPaddingRight(), 0); } } } /** * 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) { newSelection++; final LinearLayout list = (LinearLayout) view.findViewById(R.id.customViewFrame); for (int i = 1; i < list.getChildCount(); i++) { View v = list.getChildAt(i); @SuppressLint("WrongViewCast") RadioButton rb = (RadioButton) v.findViewById(R.id.control); if (newSelection != i) { rb.setChecked(false); rb.clearFocus(); } } } /** * Constructs the dialog's list content and sets up click listeners. */ private void invalidateList() { if ((items == null || items.length == 0) && adapter == null) return; // Hide content view.findViewById(R.id.contentScrollView).setVisibility(View.GONE); // Show custom frame container but hide the scrollview view.findViewById(R.id.customViewScrollParent).setVisibility(View.VISIBLE); view.findViewById(R.id.customViewScroll).setVisibility(View.GONE); // Set up list with adapter LinearLayout listViewContainer = (LinearLayout) view.findViewById(R.id.list_view_container); listViewContainer.setVisibility(View.VISIBLE); listView.setAdapter(adapter); if (listType != null) { // Only set listener for 1st-party adapter, leave custom adapter implementation to user with getListView() listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (listType == ListType.MULTI) { // Keep our selected items up to date boolean isChecked = !((CheckBox) view.findViewById(R.id.control)).isChecked(); // Inverted because the view's click listener is called before the check is toggled boolean previouslySelected = selectedIndicesList.contains(position); if (isChecked) { if (!previouslySelected) { selectedIndicesList.add(position); } } else if (previouslySelected) { selectedIndicesList.remove(Integer.valueOf(position)); } } else if (listType == ListType.SINGLE) { // Keep our selected item up to date if (selectedIndex != position) { selectedIndex = position; ((MaterialDialogAdapter) adapter).notifyDataSetChanged(); } } onClick(view); } }); } final int dialogFramePadding = (int) mContext.getResources().getDimension(R.dimen.md_dialog_frame_margin); if (titleFrame.getVisibility() == View.VISIBLE || icon.getVisibility() == View.VISIBLE) { int bottomPadding = (int) getContext().getResources().getDimension(R.dimen.md_title_margin_plainlist); setMargin(titleFrame, dialogFramePadding, bottomPadding, dialogFramePadding, dialogFramePadding); ((ViewGroup) titleFrame.getParent()).removeView(titleFrame); listViewContainer.addView(titleFrame, 0); } else { listView.setPadding(listView.getPaddingLeft(), 0, listView.getPaddingRight(), listView.getPaddingBottom()); } } private int calculateMaxButtonWidth() { /** * Max button width = (DialogWidth - Side margins) / [Number of buttons] * From: http://www.google.com/design/spec/components/dialogs.html#dialogs-specs */ final int dialogWidth = getWindow().getDecorView().getMeasuredWidth(); final int margins = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_frame_side); return (dialogWidth - 2 * margins) / numberOfActionButtons(); } /** * Detects whether or not the custom view or list content can be scrolled. */ private boolean canCustomViewScroll() { if (listView != null) { return listView.getLastVisiblePosition() != -1 && listView.getLastVisiblePosition() < (listView.getCount() - 1); } final ScrollView scrollView = (ScrollView) view.findViewById(R.id.customViewScroll); final int childHeight = view.findViewById(R.id.customViewFrame).getMeasuredHeight(); return scrollView.getMeasuredHeight() < childHeight; } /** * Detects whether or not the content TextView can be scrolled. */ private boolean canContentScroll() { final ScrollView scrollView = (ScrollView) view.findViewById(R.id.contentScrollView); final int childHeight = view.findViewById(R.id.content).getMeasuredHeight(); return scrollView.getMeasuredHeight() < childHeight; } /** * Measures the action button's and their text to decide whether or not the button should be stacked. */ private void checkIfStackingNeeded() { if (numberOfActionButtons() <= 1) { return; } else if (forceStacking) { isStacked = true; invalidateActions(); return; } final int maxWidth = calculateMaxButtonWidth(); isStacked = false; if (this.positiveText != null) { final int positiveWidth = positiveButton.getWidth(); isStacked = positiveWidth > maxWidth; } if (!isStacked && this.neutralText != null) { final int neutralWidth = neutralButton.getWidth(); isStacked = neutralWidth > maxWidth; } if (!isStacked && this.negativeText != null) { final int negativeWidth = negativeButton.getWidth(); isStacked = negativeWidth > maxWidth; } 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 boolean invalidateActions() { if (!hasActionButtons()) { // 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); invalidateList(); return false; } if (isStacked) { view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE); view.findViewById(R.id.buttonStackedFrame).setVisibility(View.VISIBLE); } else { view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.VISIBLE); view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE); } positiveButton = (TextView) view.findViewById( isStacked ? R.id.buttonStackedPositive : R.id.buttonDefaultPositive); if (this.positiveText != null) { setTypeface(positiveButton, mediumFont); positiveButton.setText(this.positiveText); positiveButton.setTextColor(getActionTextStateList(this.positiveColor)); setBackgroundCompat(positiveButton, DialogUtils.resolveDrawable(getContext(), isStacked ? R.attr.md_selector : R.attr.md_btn_selector)); positiveButton.setTag(POSITIVE); positiveButton.setOnClickListener(this); } else { positiveButton.setVisibility(View.GONE); } neutralButton = (TextView) view.findViewById( isStacked ? R.id.buttonStackedNeutral : R.id.buttonDefaultNeutral); if (this.neutralText != null) { setTypeface(neutralButton, mediumFont); neutralButton.setVisibility(View.VISIBLE); neutralButton.setTextColor(getActionTextStateList(this.neutralColor)); setBackgroundCompat(neutralButton, DialogUtils.resolveDrawable(getContext(), isStacked ? R.attr.md_selector : R.attr.md_btn_selector)); 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); if (this.negativeText != null) { setTypeface(negativeButton, mediumFont); negativeButton.setVisibility(View.VISIBLE); negativeButton.setTextColor(getActionTextStateList(this.negativeColor)); setBackgroundCompat(negativeButton, DialogUtils.resolveDrawable(getContext(), isStacked ? R.attr.md_selector : R.attr.md_btn_selector)); negativeButton.setText(this.negativeText); negativeButton.setTag(NEGATIVE); negativeButton.setOnClickListener(this); if (!isStacked) { RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, (int) getContext().getResources().getDimension(R.dimen.md_button_height)); if (this.positiveText != null) { params.addRule(RelativeLayout.LEFT_OF, R.id.buttonDefaultPositive); } else { params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); } negativeButton.setLayoutParams(params); } } else { negativeButton.setVisibility(View.GONE); } invalidateList(); return true; } private void sendSingleChoiceCallback(View v) { CharSequence text = null; if (selectedIndex >= 0) { text = items[selectedIndex]; } listCallbackSingle.onSelection(this, v, selectedIndex, text); } private void sendMultichoiceCallback() { List selectedTitles = new ArrayList(); for (Integer i : selectedIndicesList) { selectedTitles.add(items[i]); } listCallbackMulti.onSelection(this, selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]), selectedTitles.toArray(new CharSequence[selectedTitles.size()])); } @Override public final void onClick(View v) { String tag = (String) v.getTag(); switch (tag) { case POSITIVE: { if (callback != null) callback.onPositive(this); if (listCallbackSingle != null) sendSingleChoiceCallback(v); if (listCallbackMulti != null) sendMultichoiceCallback(); if (autoDismiss) dismiss(); break; } case NEGATIVE: { if (callback != null) callback.onNegative(this); if (autoDismiss) dismiss(); break; } case NEUTRAL: { if (callback != null) callback.onNeutral(this); if (autoDismiss) dismiss(); break; } default: { String[] split = tag.split(":"); int index = Integer.parseInt(split[0]); if (listCallback != null) { if (autoDismiss) { dismiss(); listCallback.onSelection(this, v, index, split[1]); } } else if (listCallbackSingle != null) { RadioButton cb = (RadioButton) ((LinearLayout) v).getChildAt(0); if (!cb.isChecked()) cb.setChecked(true); invalidateSingleChoice(index); if (autoDismiss && positiveText == null) { dismiss(); sendSingleChoiceCallback(v); } } else if (listCallbackMulti != null) { CheckBox cb = (CheckBox) ((LinearLayout) v).getChildAt(0); cb.setChecked(!cb.isChecked()); } else if (autoDismiss) dismiss(); break; } } } @Override public void onMeasureScroll(ScrollView view) { if (view.getMeasuredWidth() > 0) { mMeasuredScrollView = true; invalidateCustomViewAssociations(); } } @Override public void onMeasureList(ListView view) { invalidateCustomViewAssociations(); } /** * The class used to construct a MaterialDialog. */ public static class Builder { protected Context context; protected CharSequence title; protected Alignment titleAlignment = Alignment.LEFT; protected Alignment contentAlignment = Alignment.LEFT; protected int titleColor = -1; protected int contentColor = -1; protected CharSequence content; protected CharSequence[] items; protected CharSequence positiveText; protected CharSequence neutralText; protected CharSequence negativeText; protected View customView; protected int positiveColor; protected int negativeColor; protected int neutralColor; protected ButtonCallback callback; protected ListCallback listCallback; protected ListCallback listCallbackSingle; protected ListCallbackMulti listCallbackMulti; protected Theme theme = Theme.LIGHT; protected boolean cancelable = true; protected float contentLineSpacingMultiplier = 1.3f; protected int selectedIndex = -1; protected Integer[] selectedIndices = null; protected boolean autoDismiss = true; protected Typeface regularFont; protected Typeface mediumFont; protected Drawable icon; protected ListAdapter adapter; private OnDismissListener dismissListener; private OnCancelListener cancelListener; private OnShowListener showListener; protected boolean forceStacking; public Builder(@NonNull Context context) { this.context = context; final int materialBlue = context.getResources().getColor(R.color.md_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); this.negativeColor = a.getColor(0, materialBlue); this.neutralColor = a.getColor(0, materialBlue); } catch (Exception e) { this.positiveColor = materialBlue; this.negativeColor = materialBlue; this.neutralColor = materialBlue; } finally { a.recycle(); } } else { TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{R.attr.colorAccent}); try { this.positiveColor = a.getColor(0, materialBlue); this.negativeColor = a.getColor(0, materialBlue); this.neutralColor = a.getColor(0, materialBlue); } catch (Exception e) { this.positiveColor = materialBlue; this.negativeColor = materialBlue; this.neutralColor = materialBlue; } finally { a.recycle(); } } } 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; } /** * Sets the fonts used in the dialog. * * @param medium The font used on titles and action buttons. Null uses the default. * @param regular The font used everywhere else, like on the content and list items. Null uses the default. * @return The Builder instance so you can chain calls to it. */ public Builder typeface(Typeface medium, Typeface regular) { this.mediumFont = medium; this.regularFont = regular; return this; } public Builder titleColor(int color) { this.titleColor = color; return this; } public Builder icon(Drawable icon) { this.icon = icon; return this; } public Builder icon(@DrawableRes int icon) { this.icon = context.getResources().getDrawable(icon); return this; } public Builder iconAttr(int iconAttr) { this.icon = DialogUtils.resolveDrawable(context, iconAttr); return this; } public Builder contentColor(int color) { this.contentColor = color; return this; } public Builder contentColorRes(@ColorRes int colorRes) { contentColor(this.context.getResources().getColor(colorRes)); 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(CharSequence[] items) { this.items = items; return this; } public Builder itemsCallback(ListCallback callback) { this.listCallback = callback; this.listCallbackSingle = null; this.listCallbackMulti = null; return this; } /** * Pass anything below 0 (such as -1) for the selected index to leave all options unselected initially. * Otherwise pass the index of an item that will be selected initially. * * @param selectedIndex The checkbox index that will be selected initially. * @param callback The callback that will be called when the presses the positive button. * @return The Builder instance so you can chain calls to it. */ public Builder itemsCallbackSingleChoice(int selectedIndex, ListCallback callback) { this.selectedIndex = selectedIndex; this.listCallback = null; this.listCallbackSingle = callback; this.listCallbackMulti = null; return this; } /** * Pass null for the selected indices to leave all options unselected initially. Otherwise pass * an array of indices that will be selected initially. * * @param selectedIndices The radio button indices that will be selected initially. * @param callback The callback that will be called when the presses the positive button. * @return The Builder instance so you can chain calls to it. */ public Builder itemsCallbackMultiChoice(Integer[] selectedIndices, ListCallbackMulti callback) { this.selectedIndices = selectedIndices; 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 negativeColorRes(@ColorRes int colorRes) { negativeColor(this.context.getResources().getColor(colorRes)); return this; } public Builder negativeColor(int color) { this.negativeColor = color; return this; } public Builder neutralColorRes(@ColorRes int colorRes) { neutralColor(this.context.getResources().getColor(colorRes)); return this; } public Builder neutralColor(int color) { this.neutralColor = color; return this; } public Builder callback(ButtonCallback 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; } /** * This defaults to true. If set to false, the dialog will not automatically be dismissed * when an action button is pressed, and not automatically dismissed when the user selects * a list item. * * @param dismiss Whether or not to dismiss the dialog automatically. * @return The Builder instance so you can chain calls to it. */ public Builder autoDismiss(boolean dismiss) { this.autoDismiss = dismiss; return this; } /** * Sets a custom {@link android.widget.ListAdapter} for the dialog's list * * @return This Builder object to allow for chaining of calls to set methods */ public Builder adapter(ListAdapter adapter) { this.adapter = adapter; return this; } public Builder showListener(OnShowListener listener) { this.showListener = listener; return this; } public Builder dismissListener(OnDismissListener listener) { this.dismissListener = listener; return this; } public Builder cancelListener(OnCancelListener listener) { this.cancelListener = listener; return this; } public Builder forceStacking(boolean stacked) { this.forceStacking = stacked; return this; } public MaterialDialog build() { MaterialDialog dialog = new MaterialDialog(this); if (this.showListener != null) { dialog.setOnShowListener(this.showListener); } if (this.cancelListener != null) { dialog.setOnCancelListener(this.cancelListener); } if (this.dismissListener != null) { dialog.setOnDismissListener(this.dismissListener); } return dialog; } public MaterialDialog show() { MaterialDialog dialog = build(); dialog.show(); return dialog; } } private ColorStateList getActionTextStateList(int newPrimaryColor) { final int fallBackButtonColor = DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary); if (newPrimaryColor == 0) newPrimaryColor = fallBackButtonColor; int[][] states = new int[][]{ new int[]{-android.R.attr.state_enabled}, // disabled new int[]{} // enabled }; int[] colors = new int[]{ DialogUtils.adjustAlpha(newPrimaryColor, 0.4f), 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. * * @param which The action button of which to get the view for. * @return The view from the dialog's layout representing this action button. */ public final Button getActionButton(DialogAction which) { if (view == null) return null; if (isStacked) { switch (which) { default: return (Button) view.findViewById(R.id.buttonStackedPositive); case NEUTRAL: return (Button) view.findViewById(R.id.buttonStackedNeutral); case NEGATIVE: return (Button) view.findViewById(R.id.buttonStackedNegative); } } else { switch (which) { default: return (Button) view.findViewById(R.id.buttonDefaultPositive); case NEUTRAL: return (Button) view.findViewById(R.id.buttonDefaultNeutral); case NEGATIVE: return (Button) view.findViewById(R.id.buttonDefaultNegative); } } } /** * @deprecated Use getActionButton(com.afollestad.materialdialogs.DialogAction)} instead. */ @Override public Button getButton(int whichButton) { switch (whichButton) { case BUTTON_POSITIVE: return getActionButton(DialogAction.POSITIVE); case BUTTON_NEUTRAL: return getActionButton(DialogAction.NEUTRAL); case BUTTON_NEGATIVE: return getActionButton(DialogAction.NEGATIVE); default: return null; } } /** * Retrieves the frame view containing the title and icon. You can manually change visibility and retrieve children. */ public final View getTitleFrame() { return titleFrame; } /** * Retrieves the custom view that was inflated or set to the MaterialDialog during building. * * @return The custom view that was passed into the Builder. */ public final View getCustomView() { return customView; } /** * 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, getContext().getString(titleRes)); } /** * Gets whether or not the positive, neutral, or negative action button is visible. * * @return Whether or not 1 or more action buttons is visible. */ public final boolean hasActionButtons() { return numberOfActionButtons() > 0; } /** * Gets the number of visible action buttons. * * @return 0 through 3, depending on how many should be or are visible. */ public final int numberOfActionButtons() { int number = 0; if (positiveText != null) number++; if (neutralText != null) number++; if (negativeText != null) number++; return number; } /** * Updates the dialog's title. */ public final void setTitle(CharSequence title) { this.title.setText(title); } @Override public void setIcon(int resId) { icon.setImageResource(resId); icon.setVisibility(resId != 0 ? View.VISIBLE : View.GONE); } @Override public void setIcon(Drawable d) { icon.setImageDrawable(d); icon.setVisibility(d != null ? View.VISIBLE : View.GONE); } @Override public void setIconAttribute(int attrId) { Drawable d = DialogUtils.resolveDrawable(mContext, attrId); icon.setImageDrawable(d); icon.setVisibility(d != null ? View.VISIBLE : View.GONE); } public final void setContent(CharSequence content) { ((TextView) view.findViewById(R.id.content)).setText(content); } public final void setItems(CharSequence[] items) { if (adapter == null) throw new IllegalStateException("This MaterialDialog instance does not yet have an adapter set to it. You cannot use setItems()."); if (adapter instanceof MaterialDialogAdapter) { adapter = new MaterialDialogAdapter(mContext, ListType.getLayoutForType(listType), R.id.title, items); } else { throw new IllegalStateException("When using a custom adapter, setItems() cannot be used. Set items through the adapter instead."); } this.items = items; listView.setAdapter(adapter); invalidateCustomViewAssociations(); } /** * Use this to customize any list-specific logic for this dialog (OnItemClickListener, OnLongItemClickListener, etc.) * * @return The ListView instance used by this dialog, or null if not using a list. */ @Nullable public ListView getListView() { return listView; } /** * Convenience method for getting the currently selected index of a single choice list * * @return Currently selected index of a single choice list, or -1 if not showing a single choice list */ public int getSelectedIndex() { if (listCallbackSingle != null) { return selectedIndex; } else { return -1; } } /** * Convenience method for getting the currently selected indices of a multi choice list * * @return Currently selected index of a multi choice list, or null if not showing a multi choice list */ @Nullable public Integer[] getSelectedIndices() { if (listCallbackMulti != null) { return selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]); } else { return null; } } private class MaterialDialogAdapter extends ArrayAdapter { final int itemColor; public MaterialDialogAdapter(Context context, int resource, int textViewResourceId, CharSequence[] objects) { super(context, resource, textViewResourceId, objects); itemColor = DialogUtils.resolveColor(getContext(), R.attr.md_item_color, defaultItemColor); } @Override public boolean hasStableIds() { return true; } @Override public long getItemId(int position) { return position; } @SuppressLint("WrongViewCast") @Override public View getView(final int index, View convertView, ViewGroup parent) { final View view = super.getView(index, convertView, parent); TextView tv = (TextView) view.findViewById(R.id.title); switch (listType) { case SINGLE: RadioButton radio = (RadioButton) view.findViewById(R.id.control); radio.setChecked(selectedIndex == index); break; case MULTI: if (selectedIndices != null) { CheckBox checkbox = (CheckBox) view.findViewById(R.id.control); checkbox.setChecked(selectedIndicesList.contains(index)); } break; } tv.setText(items[index]); tv.setTextColor(itemColor); setTypeface(tv, regularFont); view.setTag(index + ":" + items[index]); return view; } } private static enum ListType { REGULAR, SINGLE, MULTI; public static int getLayoutForType(ListType type) { switch (type) { case REGULAR: return R.layout.md_listitem; case SINGLE: return R.layout.md_listitem_singlechoice; case MULTI: return R.layout.md_listitem_multichoice; default: // Shouldn't be possible throw new IllegalArgumentException("Not a valid list type"); } } } public static interface ListCallback { void onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text); } public static interface ListCallbackMulti { void onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text); } /** * @deprecated Use the new {@link com.afollestad.materialdialogs.MaterialDialog.ButtonCallback} */ public abstract static class SimpleCallback extends ButtonCallback { @Override public abstract void onPositive(MaterialDialog dialog); } /** * @deprecated Use the new {@link com.afollestad.materialdialogs.MaterialDialog.ButtonCallback} */ public abstract static class Callback extends SimpleCallback { @Override public abstract void onNegative(MaterialDialog dialog); } /** * @deprecated Use the new {@link com.afollestad.materialdialogs.MaterialDialog.ButtonCallback} */ public abstract static class FullCallback extends Callback { @Override public abstract void onNeutral(MaterialDialog dialog); } /** * Override these as needed, so no needing to sub empty methods from an interface */ public static abstract class ButtonCallback { public void onPositive(MaterialDialog dialog) { } public void onNegative(MaterialDialog dialog) { } public void onNeutral(MaterialDialog dialog) { } public ButtonCallback() { super(); } @Override protected final Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public final boolean equals(Object o) { return super.equals(o); } @Override protected final void finalize() throws Throwable { super.finalize(); } @Override public final int hashCode() { return super.hashCode(); } @Override public final String toString() { return super.toString(); } } }