MaterialDialog.java 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. package com.afollestad.materialdialogs;
  2. import android.annotation.SuppressLint;
  3. import android.content.Context;
  4. import android.content.DialogInterface;
  5. import android.content.res.ColorStateList;
  6. import android.content.res.Resources;
  7. import android.content.res.TypedArray;
  8. import android.database.DataSetObserver;
  9. import android.graphics.Color;
  10. import android.graphics.Paint;
  11. import android.graphics.Typeface;
  12. import android.graphics.drawable.Drawable;
  13. import android.os.Build;
  14. import android.support.annotation.ArrayRes;
  15. import android.support.annotation.ColorRes;
  16. import android.support.annotation.DrawableRes;
  17. import android.support.annotation.LayoutRes;
  18. import android.support.annotation.NonNull;
  19. import android.support.annotation.Nullable;
  20. import android.support.annotation.StringRes;
  21. import android.text.method.LinkMovementMethod;
  22. import android.view.ContextThemeWrapper;
  23. import android.view.Gravity;
  24. import android.view.LayoutInflater;
  25. import android.view.View;
  26. import android.view.ViewGroup;
  27. import android.widget.AdapterView;
  28. import android.widget.ArrayAdapter;
  29. import android.widget.CheckBox;
  30. import android.widget.ImageView;
  31. import android.widget.LinearLayout;
  32. import android.widget.ListAdapter;
  33. import android.widget.ListView;
  34. import android.widget.RadioButton;
  35. import android.widget.RelativeLayout;
  36. import android.widget.ScrollView;
  37. import android.widget.TextView;
  38. import com.afollestad.materialdialogs.base.DialogBase;
  39. import com.afollestad.materialdialogs.views.MeasureCallbackListView;
  40. import com.afollestad.materialdialogs.views.MeasureCallbackScrollView;
  41. import java.util.ArrayList;
  42. import java.util.Arrays;
  43. import java.util.List;
  44. /**
  45. * @author Aidan Follestad (afollestad)
  46. */
  47. public class MaterialDialog extends DialogBase implements View.OnClickListener, MeasureCallbackScrollView.Callback, MeasureCallbackListView.Callback {
  48. private ImageView icon;
  49. private TextView title;
  50. private View titleFrame;
  51. private Context mContext;
  52. private CharSequence positiveText;
  53. private TextView positiveButton;
  54. private CharSequence neutralText;
  55. private TextView neutralButton;
  56. private CharSequence negativeText;
  57. private TextView negativeButton;
  58. private View view;
  59. private ListView listView;
  60. private int positiveColor;
  61. private int negativeColor;
  62. private int neutralColor;
  63. private SimpleCallback callback;
  64. private ListCallback listCallback;
  65. private ListCallback listCallbackSingle;
  66. private ListCallbackMulti listCallbackMulti;
  67. private View customView;
  68. private CharSequence[] items;
  69. private boolean isStacked;
  70. private int selectedIndex;
  71. private Integer[] selectedIndices;
  72. private boolean mMeasuredScrollView;
  73. private Typeface mediumFont;
  74. private Typeface regularFont;
  75. private boolean autoDismiss;
  76. private ListAdapter adapter;
  77. private ListType listType;
  78. private List<Integer> selectedIndicesList;
  79. protected static ContextThemeWrapper getTheme(Builder builder) {
  80. TypedArray a = builder.context.getTheme().obtainStyledAttributes(new int[]{R.attr.md_dark_theme});
  81. boolean darkTheme = builder.theme == Theme.DARK;
  82. if (!darkTheme) {
  83. try {
  84. darkTheme = a.getBoolean(0, false);
  85. } finally {
  86. a.recycle();
  87. }
  88. }
  89. return new ContextThemeWrapper(builder.context, darkTheme ? R.style.MD_Dark : R.style.MD_Light);
  90. }
  91. protected MaterialDialog(Builder builder) {
  92. super(getTheme(builder));
  93. this.regularFont = builder.regularFont;
  94. if (this.regularFont == null)
  95. this.regularFont = Typeface.createFromAsset(getContext().getResources().getAssets(), "Roboto-Regular.ttf");
  96. this.mediumFont = builder.mediumFont;
  97. if (this.mediumFont == null)
  98. this.mediumFont = Typeface.createFromAsset(getContext().getResources().getAssets(), "Roboto-Medium.ttf");
  99. mContext = builder.context;
  100. this.view = LayoutInflater.from(getContext()).inflate(R.layout.md_dialog, null);
  101. this.customView = builder.customView;
  102. this.callback = builder.callback;
  103. this.listCallback = builder.listCallback;
  104. this.listCallbackSingle = builder.listCallbackSingle;
  105. this.listCallbackMulti = builder.listCallbackMulti;
  106. this.positiveText = builder.positiveText;
  107. this.neutralText = builder.neutralText;
  108. this.negativeText = builder.negativeText;
  109. this.positiveColor = builder.positiveColor;
  110. this.negativeColor = builder.negativeColor;
  111. this.neutralColor = builder.neutralColor;
  112. this.items = builder.items;
  113. this.setCancelable(builder.cancelable);
  114. this.selectedIndex = builder.selectedIndex;
  115. this.selectedIndices = builder.selectedIndicies;
  116. this.autoDismiss = builder.autoDismiss;
  117. this.adapter = builder.adapter;
  118. title = (TextView) view.findViewById(R.id.title);
  119. icon = (ImageView) view.findViewById(R.id.icon);
  120. titleFrame = view.findViewById(R.id.titleFrame);
  121. final TextView content = (TextView) view.findViewById(R.id.content);
  122. content.setText(builder.content);
  123. content.setMovementMethod(new LinkMovementMethod());
  124. setTypeface(content, regularFont);
  125. content.setLineSpacing(0f, builder.contentLineSpacingMultiplier);
  126. if (this.positiveColor == 0) {
  127. content.setLinkTextColor(DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary));
  128. } else {
  129. content.setLinkTextColor(this.positiveColor);
  130. }
  131. if (builder.contentAlignment == Alignment.CENTER) {
  132. content.setGravity(Gravity.CENTER_HORIZONTAL);
  133. } else if (builder.contentAlignment == Alignment.RIGHT) {
  134. content.setGravity(Gravity.RIGHT);
  135. }
  136. if (customView != null) {
  137. title = (TextView) view.findViewById(R.id.titleCustomView);
  138. icon = (ImageView) view.findViewById(R.id.iconCustomView);
  139. titleFrame = view.findViewById(R.id.titleFrameCustomView);
  140. invalidateCustomViewAssociations();
  141. ((LinearLayout) view.findViewById(R.id.customViewFrame)).addView(customView);
  142. } else {
  143. invalidateCustomViewAssociations();
  144. }
  145. if (builder.icon != null) {
  146. icon.setVisibility(View.VISIBLE);
  147. icon.setImageDrawable(builder.icon);
  148. } else {
  149. icon.setVisibility(View.GONE);
  150. }
  151. boolean adapterProvided = adapter != null;
  152. if (items != null && items.length > 0 || adapterProvided) {
  153. title = (TextView) view.findViewById(R.id.titleCustomView);
  154. listView = (ListView) view.findViewById(R.id.contentListView);
  155. ((MeasureCallbackListView) listView).setCallback(this);
  156. if (!adapterProvided) {
  157. // Determine list type
  158. if (listCallbackSingle != null) {
  159. listType = ListType.SINGLE;
  160. } else if (listCallbackMulti != null) {
  161. listType = ListType.MULTI;
  162. selectedIndicesList = new ArrayList<>(Arrays.asList(selectedIndices));
  163. } else {
  164. listType = ListType.REGULAR;
  165. }
  166. adapter = new MaterialDialogAdapter(mContext, ListType.getLayoutForType(listType), R.id.title, items);
  167. }
  168. adapter.registerDataSetObserver(new DataSetObserver() {
  169. @Override
  170. public void onChanged() {
  171. super.onChanged();
  172. listView.post(new Runnable() {
  173. @Override
  174. public void run() {
  175. invalidateCustomViewAssociations();
  176. }
  177. });
  178. }
  179. });
  180. }
  181. // Title is set after it's determined whether to use first title or custom view title
  182. if (builder.title == null || builder.title.toString().trim().isEmpty()) {
  183. titleFrame.setVisibility(View.GONE);
  184. if (customView == null)
  185. view.findViewById(R.id.titleFrameCustomView).setVisibility(View.GONE);
  186. } else {
  187. title.setText(builder.title);
  188. setTypeface(title, mediumFont);
  189. if (builder.titleColor != -1) {
  190. title.setTextColor(builder.titleColor);
  191. } else {
  192. title.setTextColor(DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary));
  193. }
  194. if (builder.titleAlignment == Alignment.CENTER) {
  195. title.setGravity(Gravity.CENTER_HORIZONTAL);
  196. } else if (builder.titleAlignment == Alignment.RIGHT) {
  197. title.setGravity(Gravity.RIGHT);
  198. }
  199. }
  200. //Content color is set here.
  201. if (builder.contentColor != -1) {
  202. content.setTextColor(builder.contentColor);
  203. } else {
  204. content.setTextColor(DialogUtils.resolveColor(getContext(), android.R.attr.textColorSecondary));
  205. }
  206. invalidateActions();
  207. setOnShowListenerInternal();
  208. setViewInternal(view);
  209. if (builder.theme == Theme.LIGHT && Build.VERSION.SDK_INT <=
  210. Build.VERSION_CODES.GINGERBREAD_MR1) {
  211. setInverseBackgroundForced(true);
  212. title.setTextColor(Color.BLACK);
  213. content.setTextColor(Color.BLACK);
  214. }
  215. }
  216. @Override
  217. public void onShow(DialogInterface dialog) {
  218. super.onShow(dialog); // calls any external show listeners
  219. checkIfStackingNeeded();
  220. invalidateCustomViewAssociations();
  221. }
  222. /**
  223. * Invalidates visibility of views for the presence of a custom view or list content
  224. */
  225. private void invalidateCustomViewAssociations() {
  226. if (customView != null || (items != null && items.length > 0) || adapter != null) {
  227. view.findViewById(R.id.mainFrame).setVisibility(View.GONE);
  228. view.findViewById(R.id.customViewScrollParent).setVisibility(View.VISIBLE);
  229. if (!mMeasuredScrollView && listView == null) {
  230. // Wait until it's measured
  231. ((MeasureCallbackScrollView) view.findViewById(R.id.customViewScroll)).setCallback(this);
  232. return;
  233. }
  234. if (canCustomViewScroll()) {
  235. view.findViewById(R.id.customViewDivider).setVisibility(View.VISIBLE);
  236. view.findViewById(R.id.customViewDivider).setBackgroundColor(DialogUtils.resolveColor(getContext(), R.attr.md_divider));
  237. setMargin(view.findViewById(R.id.buttonStackedFrame), -1, 0, -1, -1);
  238. setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, 0, -1, -1);
  239. if (items != null && items.length > 0) {
  240. View customFrame = view.findViewById(R.id.customViewFrame);
  241. Resources r = getContext().getResources();
  242. int bottomPadding = view.findViewById(R.id.titleCustomView).getVisibility() == View.VISIBLE ?
  243. (int) r.getDimension(R.dimen.md_main_frame_margin) : (int) r.getDimension(R.dimen.md_dialog_frame_margin);
  244. customFrame.setPadding(customFrame.getPaddingLeft(), customFrame.getPaddingTop(),
  245. customFrame.getPaddingRight(), bottomPadding);
  246. }
  247. } else {
  248. view.findViewById(R.id.customViewDivider).setVisibility(View.GONE);
  249. final int bottomMargin = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_frame_bottom);
  250. setMargin(view.findViewById(R.id.buttonStackedFrame), -1, bottomMargin, -1, -1);
  251. setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, bottomMargin, -1, -1);
  252. }
  253. } else {
  254. view.findViewById(R.id.mainFrame).setVisibility(View.VISIBLE);
  255. view.findViewById(R.id.customViewScrollParent).setVisibility(View.GONE);
  256. view.findViewById(R.id.customViewDivider).setVisibility(View.GONE);
  257. if (!mMeasuredScrollView) {
  258. // Wait until it's measured
  259. ((MeasureCallbackScrollView) view.findViewById(R.id.contentScrollView)).setCallback(this);
  260. return;
  261. }
  262. if (canContentScroll()) {
  263. view.findViewById(R.id.customViewDivider).setVisibility(View.VISIBLE);
  264. view.findViewById(R.id.customViewDivider).setBackgroundColor(DialogUtils.resolveColor(getContext(), R.attr.md_divider));
  265. setMargin(view.findViewById(R.id.mainFrame), -1, 0, -1, -1);
  266. setMargin(view.findViewById(R.id.buttonStackedFrame), -1, 0, -1, -1);
  267. setMargin(view.findViewById(R.id.buttonDefaultFrame), -1, 0, -1, -1);
  268. final int conPadding = (int) getContext().getResources().getDimension(R.dimen.md_main_frame_margin);
  269. View con = view.findViewById(R.id.content);
  270. con.setPadding(con.getPaddingLeft(), 0, con.getPaddingRight(), conPadding);
  271. } else {
  272. View con = view.findViewById(R.id.content);
  273. con.setPadding(con.getPaddingLeft(), 0, con.getPaddingRight(), 0);
  274. }
  275. }
  276. }
  277. /**
  278. * Invalidates the radio buttons in the single choice mode list so that only the radio button that
  279. * was previous selected is checked.
  280. */
  281. private void invalidateSingleChoice(int newSelection) {
  282. newSelection++;
  283. final LinearLayout list = (LinearLayout) view.findViewById(R.id.customViewFrame);
  284. for (int i = 1; i < list.getChildCount(); i++) {
  285. View v = list.getChildAt(i);
  286. @SuppressLint("WrongViewCast")
  287. RadioButton rb = (RadioButton) v.findViewById(R.id.control);
  288. if (newSelection != i) {
  289. rb.setChecked(false);
  290. rb.clearFocus();
  291. }
  292. }
  293. }
  294. /**
  295. * Constructs the dialog's list content and sets up click listeners.
  296. */
  297. private void invalidateList() {
  298. if ((items == null || items.length == 0) && adapter == null) return;
  299. // Hide content
  300. view.findViewById(R.id.contentScrollView).setVisibility(View.GONE);
  301. // Show custom frame container but hide the scrollview
  302. view.findViewById(R.id.customViewScrollParent).setVisibility(View.VISIBLE);
  303. view.findViewById(R.id.customViewScroll).setVisibility(View.GONE);
  304. // Setup margins on custom view frame
  305. LinearLayout customFrame = (LinearLayout) view.findViewById(R.id.customViewFrame);
  306. setMargin(customFrame, -1, -1, 0, 0);
  307. // Set up list with adapter
  308. LinearLayout listViewContainer = (LinearLayout) view.findViewById(R.id.list_view_container);
  309. listViewContainer.setVisibility(View.VISIBLE);
  310. listView.setAdapter(adapter);
  311. if (listType != null) {
  312. // Only set listener for 1st-party adapter, leave custom adapter implementation to user with getListView()
  313. listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  314. @Override
  315. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  316. if (listType == ListType.MULTI) {
  317. // Keep our selected items up to date
  318. boolean isChecked = !((CheckBox) view.findViewById(R.id.control)).isChecked(); // Inverted because the view's click listener is called before the check is toggled
  319. boolean previouslySelected = selectedIndicesList.contains(position);
  320. if (isChecked) {
  321. if (!previouslySelected) {
  322. selectedIndicesList.add(position);
  323. }
  324. } else if (previouslySelected) {
  325. selectedIndicesList.remove(Integer.valueOf(position));
  326. }
  327. } else if (listType == ListType.SINGLE) {
  328. // Keep our selected item up to date
  329. if (selectedIndex != position) {
  330. selectedIndex = position;
  331. ((MaterialDialogAdapter) adapter).notifyDataSetChanged();
  332. }
  333. }
  334. onClick(view);
  335. }
  336. });
  337. }
  338. final int dialogFramePadding = (int) mContext.getResources().getDimension(R.dimen.md_dialog_frame_margin);
  339. final int mainFramePadding = (int) mContext.getResources().getDimension(R.dimen.md_main_frame_margin);
  340. if (titleFrame.getVisibility() == View.VISIBLE || icon.getVisibility() == View.VISIBLE) {
  341. final int customFramePadding = (int) getContext().getResources().getDimension(R.dimen.md_title_margin_plainlist);
  342. title.setPadding(customFramePadding, dialogFramePadding, customFramePadding, title.getPaddingBottom());
  343. ViewGroup titleFrame = (ViewGroup) title.getParent();
  344. titleFrame.removeView(titleFrame);
  345. listViewContainer.addView(titleFrame, 0);
  346. } else {
  347. listView.setPadding(listView.getPaddingLeft(), mainFramePadding,
  348. listView.getPaddingRight(), listView.getPaddingBottom());
  349. }
  350. }
  351. private int calculateMaxButtonWidth() {
  352. /**
  353. * Max button width = (DialogWidth - 16dp - 16dp - 8dp) / 2
  354. * From: http://www.google.com/design/spec/components/dialogs.html#dialogs-specs
  355. */
  356. final int dialogWidth = getWindow().getDecorView().getMeasuredWidth();
  357. final int eightDp = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_horizontal_external);
  358. final int sixteenDp = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_frame_side);
  359. return (dialogWidth - sixteenDp - sixteenDp - eightDp) / 2;
  360. }
  361. /**
  362. * Detects whether or not the custom view or list content can be scrolled.
  363. */
  364. private boolean canCustomViewScroll() {
  365. if (listView != null) {
  366. if (listView.getLastVisiblePosition() != -1) {
  367. return listView.getLastVisiblePosition() < (listView.getCount() - 1);
  368. }
  369. return false;
  370. }
  371. final ScrollView scrollView = (ScrollView) view.findViewById(R.id.customViewScroll);
  372. final int childHeight = view.findViewById(R.id.customViewFrame).getMeasuredHeight();
  373. return scrollView.getMeasuredHeight() < childHeight;
  374. }
  375. /**
  376. * Detects whether or not the content TextView can be scrolled.
  377. */
  378. private boolean canContentScroll() {
  379. final ScrollView scrollView = (ScrollView) view.findViewById(R.id.contentScrollView);
  380. final int childHeight = view.findViewById(R.id.content).getMeasuredHeight();
  381. return scrollView.getMeasuredHeight() < childHeight;
  382. }
  383. /**
  384. * Measures the action button's and their text to decide whether or not the button should be stacked.
  385. */
  386. private void checkIfStackingNeeded() {
  387. if (numberOfActionButtons() <= 1) return;
  388. final int maxWidth = calculateMaxButtonWidth();
  389. final Paint paint = positiveButton.getPaint();
  390. final int eightDp = (int) getContext().getResources().getDimension(R.dimen.md_button_padding_horizontal_external);
  391. final int positiveWidth = (int) paint.measureText(positiveButton.getText().toString()) + (eightDp * 2);
  392. isStacked = positiveWidth > maxWidth;
  393. if (!isStacked && this.neutralText != null) {
  394. final int neutralWidth = (int) paint.measureText(neutralButton.getText().toString()) + (eightDp * 2);
  395. isStacked = neutralWidth > maxWidth;
  396. }
  397. if (!isStacked && this.negativeText != null) {
  398. final int negativeWidth = (int) paint.measureText(negativeButton.getText().toString()) + (eightDp * 2);
  399. isStacked = negativeWidth > maxWidth;
  400. }
  401. invalidateActions();
  402. }
  403. /**
  404. * Invalidates the positive/neutral/negative action buttons. Decides whether they should be visible
  405. * and sets their properties (such as height, text color, etc.).
  406. */
  407. private boolean invalidateActions() {
  408. if (!hasActionButtons()) {
  409. // If the dialog is a plain list dialog, no buttons are shown.
  410. view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
  411. view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
  412. invalidateList();
  413. return false;
  414. }
  415. if (isStacked) {
  416. view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
  417. view.findViewById(R.id.buttonStackedFrame).setVisibility(View.VISIBLE);
  418. } else {
  419. view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.VISIBLE);
  420. view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
  421. }
  422. positiveButton = (TextView) view.findViewById(
  423. isStacked ? R.id.buttonStackedPositive : R.id.buttonDefaultPositive);
  424. if (this.positiveText != null) {
  425. setTypeface(positiveButton, mediumFont);
  426. positiveButton.setText(this.positiveText);
  427. positiveButton.setTextColor(getActionTextStateList(this.positiveColor));
  428. setBackgroundCompat(positiveButton, DialogUtils.resolveDrawable(getContext(), R.attr.md_selector));
  429. positiveButton.setTag(POSITIVE);
  430. positiveButton.setOnClickListener(this);
  431. } else {
  432. positiveButton.setVisibility(View.GONE);
  433. }
  434. neutralButton = (TextView) view.findViewById(
  435. isStacked ? R.id.buttonStackedNeutral : R.id.buttonDefaultNeutral);
  436. if (this.neutralText != null) {
  437. setTypeface(neutralButton, mediumFont);
  438. neutralButton.setVisibility(View.VISIBLE);
  439. neutralButton.setTextColor(getActionTextStateList(this.neutralColor));
  440. setBackgroundCompat(neutralButton, DialogUtils.resolveDrawable(getContext(), R.attr.md_selector));
  441. neutralButton.setText(this.neutralText);
  442. neutralButton.setTag(NEUTRAL);
  443. neutralButton.setOnClickListener(this);
  444. } else {
  445. neutralButton.setVisibility(View.GONE);
  446. }
  447. negativeButton = (TextView) view.findViewById(
  448. isStacked ? R.id.buttonStackedNegative : R.id.buttonDefaultNegative);
  449. if (this.negativeText != null) {
  450. setTypeface(negativeButton, mediumFont);
  451. negativeButton.setVisibility(View.VISIBLE);
  452. negativeButton.setTextColor(getActionTextStateList(this.negativeColor));
  453. setBackgroundCompat(negativeButton, DialogUtils.resolveDrawable(getContext(), R.attr.md_selector));
  454. negativeButton.setText(this.negativeText);
  455. negativeButton.setTag(NEGATIVE);
  456. negativeButton.setOnClickListener(this);
  457. if (!isStacked) {
  458. RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
  459. RelativeLayout.LayoutParams.WRAP_CONTENT,
  460. (int) getContext().getResources().getDimension(R.dimen.md_button_height));
  461. if (this.positiveText != null) {
  462. params.addRule(RelativeLayout.LEFT_OF, R.id.buttonDefaultPositive);
  463. } else {
  464. params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
  465. }
  466. negativeButton.setLayoutParams(params);
  467. }
  468. } else {
  469. negativeButton.setVisibility(View.GONE);
  470. }
  471. invalidateList();
  472. return true;
  473. }
  474. private void sendSingleChoiceCallback(View v) {
  475. listCallbackSingle.onSelection(this, v, selectedIndex, items[selectedIndex]);
  476. }
  477. private void sendMultichoiceCallback() {
  478. List<CharSequence> selectedTitles = new ArrayList<CharSequence>();
  479. for (Integer i : selectedIndicesList) {
  480. selectedTitles.add(items[i]);
  481. }
  482. listCallbackMulti.onSelection(this,
  483. selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]),
  484. selectedTitles.toArray(new CharSequence[selectedTitles.size()]));
  485. }
  486. @Override
  487. public final void onClick(View v) {
  488. String tag = (String) v.getTag();
  489. if (tag.equals(POSITIVE)) {
  490. if (listCallbackSingle != null) {
  491. if (autoDismiss) dismiss();
  492. sendSingleChoiceCallback(v);
  493. } else if (listCallbackMulti != null) {
  494. if (autoDismiss) dismiss();
  495. sendMultichoiceCallback();
  496. } else if (callback != null) {
  497. if (autoDismiss) dismiss();
  498. callback.onPositive(this);
  499. } else if (autoDismiss) dismiss();
  500. } else if (tag.equals(NEGATIVE)) {
  501. if (callback != null && callback instanceof Callback) {
  502. if (autoDismiss) dismiss();
  503. ((Callback) callback).onNegative(this);
  504. } else if (autoDismiss) dismiss();
  505. } else if (tag.equals(NEUTRAL)) {
  506. if (callback != null && callback instanceof FullCallback) {
  507. if (autoDismiss) dismiss();
  508. ((FullCallback) callback).onNeutral(this);
  509. } else if (autoDismiss) dismiss();
  510. } else {
  511. String[] split = tag.split(":");
  512. int index = Integer.parseInt(split[0]);
  513. if (listCallback != null) {
  514. if (autoDismiss) dismiss();
  515. listCallback.onSelection(this, v, index, split[1]);
  516. } else if (listCallbackSingle != null) {
  517. RadioButton cb = (RadioButton) ((LinearLayout) v).getChildAt(0);
  518. if (!cb.isChecked())
  519. cb.setChecked(true);
  520. invalidateSingleChoice(index);
  521. if (positiveText == null) {
  522. // Immediately send the selection callback if no positive button is shown
  523. if (autoDismiss) dismiss();
  524. sendSingleChoiceCallback(v);
  525. }
  526. } else if (listCallbackMulti != null) {
  527. CheckBox cb = (CheckBox) ((LinearLayout) v).getChildAt(0);
  528. cb.setChecked(!cb.isChecked());
  529. if (positiveText == null) {
  530. // Immediately send the selection callback if no positive button is shown
  531. if (autoDismiss) dismiss();
  532. sendMultichoiceCallback();
  533. }
  534. } else if (autoDismiss) dismiss();
  535. }
  536. }
  537. @Override
  538. public void onMeasureScroll(ScrollView view) {
  539. if (view.getMeasuredWidth() > 0) {
  540. mMeasuredScrollView = true;
  541. invalidateCustomViewAssociations();
  542. }
  543. }
  544. @Override
  545. public void onMeasureScroll(ListView view) {
  546. invalidateCustomViewAssociations();
  547. }
  548. /**
  549. * The class used to construct a MaterialDialog.
  550. */
  551. public static class Builder {
  552. protected Context context;
  553. protected CharSequence title;
  554. protected Alignment titleAlignment = Alignment.LEFT;
  555. protected Alignment contentAlignment = Alignment.LEFT;
  556. protected int titleColor = -1;
  557. protected int contentColor = -1;
  558. protected CharSequence content;
  559. protected CharSequence[] items;
  560. protected CharSequence positiveText;
  561. protected CharSequence neutralText;
  562. protected CharSequence negativeText;
  563. protected View customView;
  564. protected int positiveColor;
  565. protected int negativeColor;
  566. protected int neutralColor;
  567. protected SimpleCallback callback;
  568. protected ListCallback listCallback;
  569. protected ListCallback listCallbackSingle;
  570. protected ListCallbackMulti listCallbackMulti;
  571. protected Theme theme = Theme.LIGHT;
  572. protected boolean cancelable = true;
  573. protected float contentLineSpacingMultiplier = 1.3f;
  574. protected int selectedIndex = -1;
  575. protected Integer[] selectedIndicies = null;
  576. protected boolean autoDismiss = true;
  577. protected Typeface regularFont;
  578. protected Typeface mediumFont;
  579. protected Drawable icon;
  580. protected ListAdapter adapter;
  581. public Builder(@NonNull Context context) {
  582. this.context = context;
  583. final int materialBlue = context.getResources().getColor(R.color.md_material_blue_500);
  584. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  585. TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
  586. try {
  587. this.positiveColor = a.getColor(0, materialBlue);
  588. this.negativeColor = a.getColor(0, materialBlue);
  589. this.neutralColor = a.getColor(0, materialBlue);
  590. } catch (Exception e) {
  591. this.positiveColor = materialBlue;
  592. this.negativeColor = materialBlue;
  593. this.neutralColor = materialBlue;
  594. } finally {
  595. a.recycle();
  596. }
  597. } else {
  598. TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{R.attr.colorAccent});
  599. try {
  600. this.positiveColor = a.getColor(0, materialBlue);
  601. this.negativeColor = a.getColor(0, materialBlue);
  602. this.neutralColor = a.getColor(0, materialBlue);
  603. } catch (Exception e) {
  604. this.positiveColor = materialBlue;
  605. this.negativeColor = materialBlue;
  606. this.neutralColor = materialBlue;
  607. } finally {
  608. a.recycle();
  609. }
  610. }
  611. }
  612. public Builder title(@StringRes int titleRes) {
  613. title(this.context.getString(titleRes));
  614. return this;
  615. }
  616. public Builder title(CharSequence title) {
  617. this.title = title;
  618. return this;
  619. }
  620. public Builder titleAlignment(Alignment align) {
  621. this.titleAlignment = align;
  622. return this;
  623. }
  624. public Builder titleColorRes(@ColorRes int colorRes) {
  625. titleColor(this.context.getResources().getColor(colorRes));
  626. return this;
  627. }
  628. /**
  629. * Sets the fonts used in the dialog.
  630. *
  631. * @param medium The font used on titles and action buttons. Null uses the default.
  632. * @param regular The font used everywhere else, like on the content and list items. Null uses the default.
  633. * @return The Builder instance so you can chain calls to it.
  634. */
  635. public Builder typeface(Typeface medium, Typeface regular) {
  636. this.mediumFont = medium;
  637. this.regularFont = regular;
  638. return this;
  639. }
  640. public Builder titleColor(int color) {
  641. this.titleColor = color;
  642. return this;
  643. }
  644. public Builder icon(Drawable icon) {
  645. this.icon = icon;
  646. return this;
  647. }
  648. public Builder icon(@DrawableRes int icon) {
  649. this.icon = context.getResources().getDrawable(icon);
  650. return this;
  651. }
  652. public Builder iconAttr(int iconAttr) {
  653. this.icon = DialogUtils.resolveDrawable(context, iconAttr);
  654. return this;
  655. }
  656. public Builder contentColor(int color) {
  657. this.contentColor = color;
  658. return this;
  659. }
  660. public Builder contentColorRes(@ColorRes int colorRes) {
  661. contentColor(this.context.getResources().getColor(colorRes));
  662. return this;
  663. }
  664. public Builder content(@StringRes int contentRes) {
  665. content(this.context.getString(contentRes));
  666. return this;
  667. }
  668. public Builder content(CharSequence content) {
  669. this.content = content;
  670. return this;
  671. }
  672. public Builder content(@StringRes int contentRes, Object... formatArgs) {
  673. content(this.context.getString(contentRes, formatArgs));
  674. return this;
  675. }
  676. public Builder contentAlignment(Alignment align) {
  677. this.contentAlignment = align;
  678. return this;
  679. }
  680. public Builder contentLineSpacing(float multiplier) {
  681. this.contentLineSpacingMultiplier = multiplier;
  682. return this;
  683. }
  684. public Builder items(@ArrayRes int itemsRes) {
  685. items(this.context.getResources().getStringArray(itemsRes));
  686. return this;
  687. }
  688. public Builder items(CharSequence[] items) {
  689. this.items = items;
  690. return this;
  691. }
  692. public Builder itemsCallback(ListCallback callback) {
  693. this.listCallback = callback;
  694. this.listCallbackSingle = null;
  695. this.listCallbackMulti = null;
  696. return this;
  697. }
  698. /**
  699. * Pass anything below 0 (such as -1) for the selected index to leave all options unselected initially.
  700. * Otherwise pass the index of an item that will be selected initially.
  701. *
  702. * @param selectedIndex The checkbox index that will be selected initially.
  703. * @param callback The callback that will be called when the presses the positive button.
  704. * @return The Builder instance so you can chain calls to it.
  705. */
  706. public Builder itemsCallbackSingleChoice(int selectedIndex, ListCallback callback) {
  707. this.selectedIndex = selectedIndex;
  708. this.listCallback = null;
  709. this.listCallbackSingle = callback;
  710. this.listCallbackMulti = null;
  711. return this;
  712. }
  713. /**
  714. * Pass null for the selected indices to leave all options unselected initially. Otherwise pass
  715. * an array of indices that will be selected initially.
  716. *
  717. * @param selectedIndices The radio button indices that will be selected initially.
  718. * @param callback The callback that will be called when the presses the positive button.
  719. * @return The Builder instance so you can chain calls to it.
  720. */
  721. public Builder itemsCallbackMultiChoice(Integer[] selectedIndices, ListCallbackMulti callback) {
  722. this.selectedIndicies = selectedIndices;
  723. this.listCallback = null;
  724. this.listCallbackSingle = null;
  725. this.listCallbackMulti = callback;
  726. return this;
  727. }
  728. public Builder positiveText(@StringRes int postiveRes) {
  729. positiveText(this.context.getString(postiveRes));
  730. return this;
  731. }
  732. public Builder positiveText(CharSequence message) {
  733. this.positiveText = message;
  734. return this;
  735. }
  736. public Builder neutralText(@StringRes int neutralRes) {
  737. neutralText(this.context.getString(neutralRes));
  738. return this;
  739. }
  740. public Builder neutralText(CharSequence message) {
  741. this.neutralText = message;
  742. return this;
  743. }
  744. public Builder negativeText(@StringRes int negativeRes) {
  745. negativeText(this.context.getString(negativeRes));
  746. return this;
  747. }
  748. public Builder negativeText(CharSequence message) {
  749. this.negativeText = message;
  750. return this;
  751. }
  752. public Builder customView(@LayoutRes int layoutRes) {
  753. LayoutInflater li = LayoutInflater.from(this.context);
  754. customView(li.inflate(layoutRes, null));
  755. return this;
  756. }
  757. public Builder customView(View view) {
  758. this.customView = view;
  759. return this;
  760. }
  761. public Builder positiveColorRes(@ColorRes int colorRes) {
  762. positiveColor(this.context.getResources().getColor(colorRes));
  763. return this;
  764. }
  765. public Builder positiveColor(int color) {
  766. this.positiveColor = color;
  767. return this;
  768. }
  769. public Builder negativeColorRes(@ColorRes int colorRes) {
  770. negativeColor(this.context.getResources().getColor(colorRes));
  771. return this;
  772. }
  773. public Builder negativeColor(int color) {
  774. this.negativeColor = color;
  775. return this;
  776. }
  777. public Builder neutralColorRes(@ColorRes int colorRes) {
  778. neutralColor(this.context.getResources().getColor(colorRes));
  779. return this;
  780. }
  781. public Builder neutralColor(int color) {
  782. this.neutralColor = color;
  783. return this;
  784. }
  785. public Builder callback(SimpleCallback callback) {
  786. this.callback = callback;
  787. return this;
  788. }
  789. public Builder theme(Theme theme) {
  790. this.theme = theme;
  791. return this;
  792. }
  793. public Builder cancelable(boolean cancelable) {
  794. this.cancelable = cancelable;
  795. return this;
  796. }
  797. /**
  798. * This defaults to true. If set to false, the dialog will not automatically be dismissed
  799. * when an action button is pressed, and not automatically dismissed when the user selects
  800. * a list item.
  801. *
  802. * @param dismiss Whether or not to dismiss the dialog automatically.
  803. * @return The Builder instance so you can chain calls to it.
  804. */
  805. public Builder autoDismiss(boolean dismiss) {
  806. this.autoDismiss = dismiss;
  807. return this;
  808. }
  809. /**
  810. * Sets a custom {@link android.widget.ListAdapter} for the dialog's list
  811. *
  812. * @return This Builder object to allow for chaining of calls to set methods
  813. */
  814. public Builder adapter(ListAdapter adapter) {
  815. this.adapter = adapter;
  816. return this;
  817. }
  818. public MaterialDialog build() {
  819. return new MaterialDialog(this);
  820. }
  821. public MaterialDialog show() {
  822. MaterialDialog dialog = new MaterialDialog(this);
  823. dialog.show();
  824. return dialog;
  825. }
  826. }
  827. private ColorStateList getActionTextStateList(int newPrimaryColor) {
  828. final int fallBackButtonColor = DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary);
  829. if (newPrimaryColor == 0) newPrimaryColor = fallBackButtonColor;
  830. int[][] states = new int[][]{
  831. new int[]{-android.R.attr.state_enabled}, // disabled
  832. new int[]{} // enabled
  833. };
  834. int[] colors = new int[]{
  835. DialogUtils.adjustAlpha(newPrimaryColor, 0.4f),
  836. newPrimaryColor
  837. };
  838. return new ColorStateList(states, colors);
  839. }
  840. /**
  841. * Retrieves the view of an action button, allowing you to modify properties such as whether or not it's enabled.
  842. *
  843. * @param which The action button of which to get the view for.
  844. * @return The view from the dialog's layout representing this action button.
  845. */
  846. public final View getActionButton(DialogAction which) {
  847. if (view == null) return null;
  848. if (isStacked) {
  849. switch (which) {
  850. default:
  851. return view.findViewById(R.id.buttonStackedPositive);
  852. case NEUTRAL:
  853. return view.findViewById(R.id.buttonStackedNeutral);
  854. case NEGATIVE:
  855. return view.findViewById(R.id.buttonStackedNegative);
  856. }
  857. } else {
  858. switch (which) {
  859. default:
  860. return view.findViewById(R.id.buttonDefaultPositive);
  861. case NEUTRAL:
  862. return view.findViewById(R.id.buttonDefaultNeutral);
  863. case NEGATIVE:
  864. return view.findViewById(R.id.buttonDefaultNegative);
  865. }
  866. }
  867. }
  868. /**
  869. * Retrieves the frame view containing the title and icon. You can manually change visibility and retrieve children.
  870. */
  871. public final View getTitleFrame() {
  872. return titleFrame;
  873. }
  874. /**
  875. * Retrieves the custom view that was inflated or set to the MaterialDialog during building.
  876. *
  877. * @return The custom view that was passed into the Builder.
  878. */
  879. public final View getCustomView() {
  880. return customView;
  881. }
  882. /**
  883. * Updates an action button's title, causing invalidation to check if the action buttons should be stacked.
  884. *
  885. * @param which The action button to update.
  886. * @param title The new title of the action button.
  887. */
  888. public final void setActionButton(DialogAction which, CharSequence title) {
  889. switch (which) {
  890. default:
  891. this.positiveText = title;
  892. break;
  893. case NEUTRAL:
  894. this.neutralText = title;
  895. break;
  896. case NEGATIVE:
  897. this.negativeText = title;
  898. break;
  899. }
  900. invalidateActions();
  901. }
  902. /**
  903. * Updates an action button's title, causing invalidation to check if the action buttons should be stacked.
  904. *
  905. * @param which The action button to update.
  906. * @param titleRes The string resource of the new title of the action button.
  907. */
  908. public final void setActionButton(DialogAction which, @StringRes int titleRes) {
  909. setActionButton(which, getContext().getString(titleRes));
  910. }
  911. /**
  912. * Gets whether or not the positive, neutral, or negative action button is visible.
  913. *
  914. * @return Whether or not 1 or more action buttons is visible.
  915. */
  916. public final boolean hasActionButtons() {
  917. return numberOfActionButtons() > 0;
  918. }
  919. /**
  920. * Gets the number of visible action buttons.
  921. *
  922. * @return 0 through 3, depending on how many should be or are visible.
  923. */
  924. public final int numberOfActionButtons() {
  925. int number = 0;
  926. if (positiveText != null) number++;
  927. if (neutralText != null) number++;
  928. if (negativeText != null) number++;
  929. return number;
  930. }
  931. /**
  932. * Updates the dialog's title.
  933. */
  934. public final void setTitle(CharSequence title) {
  935. this.title.setText(title);
  936. }
  937. @Override
  938. public void setIcon(int resId) {
  939. icon.setImageResource(resId);
  940. icon.setVisibility(resId != 0 ? View.VISIBLE : View.GONE);
  941. }
  942. @Override
  943. public void setIcon(Drawable d) {
  944. icon.setImageDrawable(d);
  945. icon.setVisibility(d != null ? View.VISIBLE : View.GONE);
  946. }
  947. @Override
  948. public void setIconAttribute(int attrId) {
  949. Drawable d = DialogUtils.resolveDrawable(getContext(), attrId);
  950. icon.setImageDrawable(d);
  951. icon.setVisibility(d != null ? View.VISIBLE : View.GONE);
  952. }
  953. public final void setContent(CharSequence content) {
  954. ((TextView) view.findViewById(R.id.content)).setText(content);
  955. }
  956. public final void setItems(CharSequence[] items) {
  957. this.items = items;
  958. invalidateList();
  959. }
  960. /**
  961. * Use this to customize any list-specific logic for this dialog (OnItemClickListener, OnLongItemClickListener, etc.)
  962. *
  963. * @return The ListView instance used by this dialog, or null if not using a list.
  964. */
  965. @Nullable
  966. public ListView getListView() {
  967. return listView;
  968. }
  969. private class MaterialDialogAdapter extends ArrayAdapter<CharSequence> {
  970. final int itemColor = DialogUtils.resolveColor(getContext(), android.R.attr.textColorSecondary);
  971. public MaterialDialogAdapter(Context context, int resource, int textViewResourceId, CharSequence[] objects) {
  972. super(context, resource, textViewResourceId, objects);
  973. }
  974. @Override
  975. public boolean hasStableIds() {
  976. return true;
  977. }
  978. @Override
  979. public long getItemId(int position) {
  980. return position;
  981. }
  982. @SuppressLint("WrongViewCast")
  983. @Override
  984. public View getView(final int index, View convertView, ViewGroup parent) {
  985. final View view = super.getView(index, convertView, parent);
  986. TextView tv = (TextView) view.findViewById(R.id.title);
  987. switch (listType) {
  988. case SINGLE:
  989. RadioButton radio = (RadioButton) view.findViewById(R.id.control);
  990. radio.setChecked(selectedIndex == index);
  991. break;
  992. case MULTI:
  993. if (selectedIndices != null) {
  994. CheckBox checkbox = (CheckBox) view.findViewById(R.id.control);
  995. checkbox.setChecked(selectedIndicesList.contains(index));
  996. }
  997. break;
  998. }
  999. tv.setText(items[index]);
  1000. tv.setTextColor(itemColor);
  1001. setTypeface(tv, regularFont);
  1002. view.setTag(index + ":" + items[index]);
  1003. setBackgroundCompat(view, DialogUtils.resolveDrawable(getContext(), R.attr.md_selector));
  1004. return view;
  1005. }
  1006. }
  1007. private static enum ListType {
  1008. REGULAR, SINGLE, MULTI;
  1009. public static int getLayoutForType(ListType type) {
  1010. switch (type) {
  1011. case REGULAR:
  1012. return R.layout.md_listitem;
  1013. case SINGLE:
  1014. return R.layout.md_listitem_singlechoice;
  1015. case MULTI:
  1016. return R.layout.md_listitem_multichoice;
  1017. default:
  1018. // Shouldn't be possible
  1019. throw new IllegalArgumentException("Not a valid list type");
  1020. }
  1021. }
  1022. }
  1023. public static interface ListCallback {
  1024. void onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text);
  1025. }
  1026. public static interface ListCallbackMulti {
  1027. void onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text);
  1028. }
  1029. public static interface SimpleCallback {
  1030. void onPositive(MaterialDialog dialog);
  1031. }
  1032. public static interface Callback extends SimpleCallback {
  1033. void onNegative(MaterialDialog dialog);
  1034. }
  1035. public static interface FullCallback extends Callback {
  1036. void onNeutral(MaterialDialog dialog);
  1037. }
  1038. }