diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index eeac034e..94139841 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -25,6 +25,7 @@ "使用异步加载方式一定程度提升文件管理器列表滑动流畅性" ], "dependency": [ + "本地化 Material Dialogs 版本 0.9.6.0", "本地化 Material Date Time Picker 版本 4.2.3", "附加 Jsoup 版本 1.19.1", "附加 Material Progressbar 版本 1.4.2", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32f794d1..38d041a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -287,16 +287,18 @@ dependencies /* Material Dialogs */ { // TODO by SuperMonster003 on Feb 5, 2022. // ! Upgrade to 3.3.0 (more difficult than expected). // ! zh-CN: 升级至 3.3.0 (实际难度超出预期较多). - val configuration: (ExternalModuleDependency).() -> Unit = { - version { - prefer("0.9.6.0") - because("Not ready to update to version 3.3.0 yet") - } - } - configuration.let { cfg -> - implementation("com.afollestad.material-dialogs:core", cfg) - implementation("com.afollestad.material-dialogs:commons", cfg) - } + // # val configuration: (ExternalModuleDependency).() -> Unit = { + // # version { + // # prefer("0.9.6.0") + // # because("Not ready to update to version 3.3.0 yet") + // # } + // # } + // # configuration.let { cfg -> + // # implementation("com.afollestad.material-dialogs:core", cfg) + // # implementation("com.afollestad.material-dialogs:commons", cfg) + // # } + implementation(project(":modules:material-dialogs")) + implementation("me.zhanghai.android.materialprogressbar:library:1.4.2") } dependencies /* Layout */ { diff --git a/modules/material-dialogs/.gitignore b/modules/material-dialogs/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/modules/material-dialogs/.gitignore @@ -0,0 +1 @@ +/build diff --git a/modules/material-dialogs/build.gradle b/modules/material-dialogs/build.gradle new file mode 100644 index 00000000..50fa737a --- /dev/null +++ b/modules/material-dialogs/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace = 'com.afollestad.materialdialogs' + + compileSdk = project.ext.compileSdk + + defaultConfig { + minSdk = project.ext.minSdk + targetSdk = project.ext.targetSdk + + versionName '0.9.6.0' + versionCode 179 + + consumerProguardFiles 'progress-proguard.txt' + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) + targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) + } + + kotlinOptions { + jvmTarget = project.ext.javaVersion + } + + lintOptions { + abortOnError false + } + +} + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:support-annotations:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'me.zhanghai.android.materialprogressbar:library:1.4.2' +} diff --git a/modules/material-dialogs/progress-proguard.txt b/modules/material-dialogs/progress-proguard.txt new file mode 100644 index 00000000..8509d753 --- /dev/null +++ b/modules/material-dialogs/progress-proguard.txt @@ -0,0 +1 @@ +-keep class me.zhanghai.android.materialprogressbar.** { *; } \ No newline at end of file diff --git a/modules/material-dialogs/src/main/AndroidManifest.xml b/modules/material-dialogs/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4c3f2e3e --- /dev/null +++ b/modules/material-dialogs/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DefaultRvAdapter.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DefaultRvAdapter.java new file mode 100644 index 00000000..aea266bb --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DefaultRvAdapter.java @@ -0,0 +1,219 @@ +package com.afollestad.materialdialogs; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.TextView; +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; +import com.afollestad.materialdialogs.internal.MDTintHelper; +import com.afollestad.materialdialogs.util.DialogUtils; + +/** @author Aidan Follestad (afollestad) */ +class DefaultRvAdapter extends RecyclerView.Adapter { + + private final MaterialDialog dialog; + @LayoutRes + private final int layout; + private final GravityEnum itemGravity; + private InternalListCallback callback; + + DefaultRvAdapter(MaterialDialog dialog, @LayoutRes int layout) { + this.dialog = dialog; + this.layout = layout; + this.itemGravity = dialog.builder.itemsGravity; + } + + void setCallback(InternalListCallback callback) { + this.callback = callback; + } + + @Override + public DefaultVH onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()).inflate(layout, parent, false); + DialogUtils.setBackgroundCompat(view, dialog.getListSelector()); + return new DefaultVH(view, this); + } + + @Override + public void onBindViewHolder(DefaultVH holder, int index) { + final View view = holder.itemView; + boolean disabled = DialogUtils.isIn(index, dialog.builder.disabledIndices); + int itemTextColor = + disabled + ? DialogUtils.adjustAlpha(dialog.builder.itemColor, 0.4f) + : dialog.builder.itemColor; + holder.itemView.setEnabled(!disabled); + + switch (dialog.listType) { + case SINGLE: { + @SuppressLint("CutPasteId") + RadioButton radio = (RadioButton) holder.control; + boolean selected = dialog.builder.selectedIndex == index; + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(radio, dialog.builder.choiceWidgetColor); + } else if (dialog.builder.widgetColorStateList != null) { + MDTintHelper.setTint(radio, dialog.builder.widgetColorStateList); + } else { + MDTintHelper.setTint(radio, dialog.builder.widgetColor); + } + radio.setChecked(selected); + radio.setEnabled(!disabled); + break; + } + case MULTI: { + @SuppressLint("CutPasteId") + CheckBox checkbox = (CheckBox) holder.control; + boolean selected = dialog.selectedIndicesList.contains(index); + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(checkbox, dialog.builder.choiceWidgetColor); + }else if (dialog.builder.widgetColorStateList != null) { + MDTintHelper.setTint(checkbox, dialog.builder.widgetColorStateList); + } else { + MDTintHelper.setTint(checkbox, dialog.builder.widgetColor); + } + checkbox.setChecked(selected); + checkbox.setEnabled(!disabled); + break; + } + } + + holder.title.setText(dialog.builder.items.get(index)); + holder.title.setTextColor(itemTextColor); + dialog.setTypeface(holder.title, dialog.builder.regularFont); + + setupGravity((ViewGroup) view); + + if (dialog.builder.itemIds != null) { + if (index < dialog.builder.itemIds.length) { + view.setId(dialog.builder.itemIds[index]); + } else { + view.setId(-1); + } + } + + ViewGroup group = (ViewGroup) view; + if (group.getChildCount() == 2) { + // Remove circular selector from check boxes and radio buttons on Lollipop + if (group.getChildAt(0) instanceof CompoundButton) { + group.getChildAt(0).setBackground(null); + } else if (group.getChildAt(1) instanceof CompoundButton) { + group.getChildAt(1).setBackground(null); + } + } + } + + @Override + public int getItemCount() { + return dialog.builder.items != null ? dialog.builder.items.size() : 0; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setupGravity(ViewGroup view) { + final LinearLayout itemRoot = (LinearLayout) view; + final int gravityInt = itemGravity.getGravityInt(); + itemRoot.setGravity(gravityInt | Gravity.CENTER_VERTICAL); + + if (view.getChildCount() == 2) { + if (itemGravity == GravityEnum.END + && !isRTL() + && view.getChildAt(0) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(0); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding( + second.getPaddingRight(), + second.getPaddingTop(), + second.getPaddingLeft(), + second.getPaddingBottom()); + + view.addView(second); + view.addView(first); + } else if (itemGravity == GravityEnum.START + && isRTL() + && view.getChildAt(1) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(1); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding( + second.getPaddingRight(), + second.getPaddingTop(), + second.getPaddingRight(), + second.getPaddingBottom()); + + view.addView(first); + view.addView(second); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private boolean isRTL() { + Configuration config = dialog.getBuilder().getContext().getResources().getConfiguration(); + return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + interface InternalListCallback { + + boolean onItemSelected( + MaterialDialog dialog, View itemView, int position, CharSequence text, boolean longPress); + } + + static class DefaultVH extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + + final CompoundButton control; + final TextView title; + final DefaultRvAdapter adapter; + + DefaultVH(View itemView, DefaultRvAdapter adapter) { + super(itemView); + control = (CompoundButton) itemView.findViewById(R.id.md_control); + title = (TextView) itemView.findViewById(R.id.md_title); + this.adapter = adapter; + itemView.setOnClickListener(this); + if (adapter.dialog.builder.listLongCallback != null) { + itemView.setOnLongClickListener(this); + } + } + + @Override + public void onClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null + && getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + adapter.callback.onItemSelected(adapter.dialog, view, getAdapterPosition(), text, false); + } + } + + @Override + public boolean onLongClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null + && getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + return adapter.callback.onItemSelected( + adapter.dialog, view, getAdapterPosition(), text, true); + } + return false; + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogAction.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogAction.java new file mode 100644 index 00000000..e717ea8b --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogAction.java @@ -0,0 +1,8 @@ +package com.afollestad.materialdialogs; + +/** @author Aidan Follestad (afollestad) */ +public enum DialogAction { + POSITIVE, + NEUTRAL, + NEGATIVE +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogBase.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogBase.java new file mode 100644 index 00000000..a991c7e0 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogBase.java @@ -0,0 +1,67 @@ +package com.afollestad.materialdialogs; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import com.afollestad.materialdialogs.internal.MDRootLayout; + +/** @author Aidan Follestad (afollestad) */ +class DialogBase extends Dialog implements DialogInterface.OnShowListener { + + protected MDRootLayout view; + private OnShowListener showListener; + + DialogBase(Context context, int theme) { + super(context, theme); + } + + @Override + public T findViewById(int id) { + return view.findViewById(id); + } + + @Override + public final void setOnShowListener(OnShowListener listener) { + showListener = listener; + } + + final void setOnShowListenerInternal() { + super.setOnShowListener(this); + } + + final void setViewInternal(View view) { + super.setContentView(view); + } + + @Override + public void onShow(DialogInterface dialog) { + if (showListener != null) { + showListener.onShow(dialog); + } + } + + @Override + @Deprecated + public void setContentView(int layoutResID) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view, ViewGroup.LayoutParams params) + throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogInit.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogInit.java new file mode 100644 index 00000000..1c2b0fa1 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/DialogInit.java @@ -0,0 +1,599 @@ +package com.afollestad.materialdialogs; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.text.InputType; +import android.text.method.LinkMovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.Display; +import android.view.Gravity; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.annotation.UiThread; +import androidx.appcompat.widget.PopupMenu; +import com.afollestad.materialdialogs.internal.MDAdapter; +import com.afollestad.materialdialogs.internal.MDButton; +import com.afollestad.materialdialogs.internal.MDRootLayout; +import com.afollestad.materialdialogs.internal.MDTintHelper; +import com.afollestad.materialdialogs.util.DialogUtils; +import me.zhanghai.android.materialprogressbar.HorizontalProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateCircularProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateHorizontalProgressDrawable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Used by MaterialDialog while initializing the dialog. Offloads some of the code to make the main + * class cleaner and easier to read/maintain. + * + * @author Aidan Follestad (afollestad) + */ +class DialogInit { + + @StyleRes + static int getTheme(@NonNull MaterialDialog.Builder builder) { + boolean darkTheme = + DialogUtils.resolveBoolean( + builder.context, R.attr.md_dark_theme, builder.theme == Theme.DARK); + builder.theme = darkTheme ? Theme.DARK : Theme.LIGHT; + return darkTheme ? R.style.MD_Dark : R.style.MD_Light; + } + + @LayoutRes + static int getInflateLayout(MaterialDialog.Builder builder) { + if (builder.customView != null) { + return R.layout.md_dialog_custom; + } else if (builder.items != null || builder.adapter != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_list_check; + } + return R.layout.md_dialog_list; + } else if (builder.progress > -2) { + return R.layout.md_dialog_progress; + } else if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + return R.layout.md_dialog_progress_indeterminate_horizontal; + } + return R.layout.md_dialog_progress_indeterminate; + } else if (builder.inputCallback != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_input_check; + } + return R.layout.md_dialog_input; + } else if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_basic_check; + } else { + return R.layout.md_dialog_basic; + } + } + + @SuppressWarnings("ConstantConditions") + @UiThread + static void init(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + + // Set cancelable flag and dialog background color + dialog.setCancelable(builder.cancelable); + dialog.setCanceledOnTouchOutside(builder.canceledOnTouchOutside); + if (builder.backgroundColor == 0) { + builder.backgroundColor = + DialogUtils.resolveColor( + builder.context, + R.attr.md_background_color, + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.colorBackgroundFloating)); + } + if (builder.backgroundColor != 0) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setCornerRadius( + builder.context.getResources().getDimension(R.dimen.md_bg_corner_radius)); + drawable.setColor(builder.backgroundColor); + dialog.getWindow().setBackgroundDrawable(drawable); + } + + // Retrieve color theme attributes + if (!builder.positiveColorSet) { + builder.positiveColor = + DialogUtils.resolveActionTextColorStateList( + builder.context, R.attr.md_positive_color, builder.positiveColor); + } + if (!builder.neutralColorSet) { + builder.neutralColor = + DialogUtils.resolveActionTextColorStateList( + builder.context, R.attr.md_neutral_color, builder.neutralColor); + } + if (!builder.negativeColorSet) { + builder.negativeColor = + DialogUtils.resolveActionTextColorStateList( + builder.context, R.attr.md_negative_color, builder.negativeColor); + } + if (!builder.widgetColorSet) { + builder.widgetColor = + DialogUtils.resolveColor(builder.context, R.attr.md_widget_color, builder.widgetColor); + } + + // Retrieve default title/content colors + if (!builder.titleColorSet) { + final int titleColorFallback = + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.textColorPrimary); + builder.titleColor = + DialogUtils.resolveColor(builder.context, R.attr.md_title_color, titleColorFallback); + } + if (!builder.contentColorSet) { + final int contentColorFallback = + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.textColorSecondary); + builder.contentColor = + DialogUtils.resolveColor(builder.context, R.attr.md_content_color, contentColorFallback); + } + if (!builder.itemColorSet) { + builder.itemColor = + DialogUtils.resolveColor(builder.context, R.attr.md_item_color, builder.contentColor); + } + + // Retrieve references to views + dialog.title = dialog.view.findViewById(R.id.md_title); + dialog.icon = dialog.view.findViewById(R.id.md_icon); + dialog.titleFrame = dialog.view.findViewById(R.id.md_titleFrame); + dialog.optionsView = dialog.view.findViewById(R.id.md_options); + dialog.content = dialog.view.findViewById(R.id.md_content); + dialog.recyclerView = dialog.view.findViewById(R.id.md_contentRecyclerView); + dialog.checkBoxPrompt = dialog.view.findViewById(R.id.md_promptCheckbox); + + // Button views initially used by checkIfStackingNeeded() + dialog.positiveButton = dialog.view.findViewById(R.id.md_buttonDefaultPositive); + dialog.neutralButton = dialog.view.findViewById(R.id.md_buttonDefaultNeutral); + dialog.negativeButton = dialog.view.findViewById(R.id.md_buttonDefaultNegative); + + // Don't allow the submit button to not be shown for input dialogs + if (builder.inputCallback != null && builder.positiveText == null) { + builder.positiveText = builder.context.getText(android.R.string.ok); + } + + // Set up the initial visibility of action buttons based on whether or not text was set + dialog.positiveButton.setVisibility(builder.positiveText != null ? View.VISIBLE : View.GONE); + dialog.neutralButton.setVisibility(builder.neutralText != null ? View.VISIBLE : View.GONE); + dialog.negativeButton.setVisibility(builder.negativeText != null ? View.VISIBLE : View.GONE); + + // Set up the focus of action buttons + dialog.positiveButton.setFocusable(true); + dialog.neutralButton.setFocusable(true); + dialog.negativeButton.setFocusable(true); + if (builder.positiveFocus) { + dialog.positiveButton.requestFocus(); + } + if (builder.neutralFocus) { + dialog.neutralButton.requestFocus(); + } + if (builder.negativeFocus) { + dialog.negativeButton.requestFocus(); + } + + // Setup icon + if (builder.icon != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(builder.icon); + } else { + Drawable d = DialogUtils.resolveDrawable(builder.context, R.attr.md_icon); + if (d != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(d); + } else { + dialog.icon.setVisibility(View.GONE); + } + } + + // Setup icon size limiting + int maxIconSize = builder.maxIconSize; + if (maxIconSize == -1) { + maxIconSize = DialogUtils.resolveDimension(builder.context, R.attr.md_icon_max_size); + } + if (builder.limitIconToDefaultSize + || DialogUtils.resolveBoolean(builder.context, R.attr.md_icon_limit_icon_to_default_size)) { + maxIconSize = builder.context.getResources().getDimensionPixelSize(R.dimen.md_icon_max_size); + } + if (maxIconSize > -1) { + dialog.icon.setAdjustViewBounds(true); + dialog.icon.setMaxHeight(maxIconSize); + dialog.icon.setMaxWidth(maxIconSize); + dialog.icon.requestLayout(); + } + + // Setup divider color in case content scrolls + if (!builder.dividerColorSet) { + final int dividerFallback = DialogUtils.resolveColor(dialog.getContext(), R.attr.md_divider); + builder.dividerColor = + DialogUtils.resolveColor(builder.context, R.attr.md_divider_color, dividerFallback); + } + dialog.view.setDividerColor(builder.dividerColor); + + // Setup title and title frame + if (dialog.title != null) { + dialog.setTypeface(dialog.title, builder.mediumFont); + dialog.title.setTextColor(builder.titleColor); + dialog.title.setGravity(builder.titleGravity.getGravityInt()); + // noinspection ResourceType + dialog.title.setTextAlignment(builder.titleGravity.getTextAlignment()); + + if (builder.title != null) { + dialog.title.setText(builder.title); + } + if (builder.options != null) { + dialog.optionsView.setVisibility(View.VISIBLE); + dialog.optionsView.setOnClickListener(v -> { + var popupMenu = new PopupMenu(builder.context, v, Gravity.END); + popupMenu.getMenuInflater().inflate(R.menu.md_more_options, popupMenu.getMenu()); + List options = builder.options; + for (int i = 0; i < options.size(); i++) { + MaterialDialog.OptionMenuItemSpec option = options.get(i); + popupMenu.getMenu().add(Menu.NONE, getDialogMenuOptionsItemActionIdByIndex(i), i, option.title()); + } + popupMenu.setOnMenuItemClickListener(menuItem -> { + List optionMenuItemSpecs = builder.options; + for (int i = 0; i < optionMenuItemSpecs.size(); i++) { + MaterialDialog.OptionMenuItemSpec option = optionMenuItemSpecs.get(i); + int menuActionId = getDialogMenuOptionsItemActionIdByIndex(i); + if (menuItem.getItemId() == menuActionId) { + option.onClickListener().onClick(dialog); + return true; + } + } + return false; + }); + popupMenu.show(); + }); + } + if (builder.title == null && builder.options == null) { + dialog.titleFrame.setVisibility(View.GONE); + } else { + dialog.titleFrame.setVisibility(View.VISIBLE); + } + } + + // Setup content + if (dialog.content != null) { + dialog.content.setMovementMethod(new LinkMovementMethod()); + dialog.setTypeface(dialog.content, builder.regularFont); + dialog.content.setLineSpacing(0f, builder.contentLineSpacingMultiplier); + if (builder.linkColor == null) { + dialog.content.setLinkTextColor( + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.textColorPrimary)); + } else { + dialog.content.setLinkTextColor(builder.linkColor); + } + dialog.content.setTextColor(builder.contentColor); + dialog.content.setGravity(builder.contentGravity.getGravityInt()); + // noinspection ResourceType + dialog.content.setTextAlignment(builder.contentGravity.getTextAlignment()); + + if (builder.content != null) { + dialog.content.setText(builder.content); + dialog.content.setVisibility(View.VISIBLE); + } else { + dialog.content.setVisibility(View.GONE); + } + } + + // Setup prompt checkbox + if (dialog.checkBoxPrompt != null) { + dialog.checkBoxPrompt.setText(builder.checkBoxPrompt); + dialog.checkBoxPrompt.setChecked(builder.checkBoxPromptInitiallyChecked); + dialog.checkBoxPrompt.setOnCheckedChangeListener(builder.checkBoxPromptListener); + dialog.setTypeface(dialog.checkBoxPrompt, builder.regularFont); + dialog.checkBoxPrompt.setTextColor(builder.contentColor); + if (builder.widgetColorStateList != null) { + MDTintHelper.setTint(dialog.checkBoxPrompt, builder.widgetColorStateList); + } else { + MDTintHelper.setTint(dialog.checkBoxPrompt, builder.widgetColor); + } + } + + // Setup action buttons + dialog.view.setButtonGravity(builder.buttonsGravity); + dialog.view.setButtonStackedGravity(builder.btnStackedGravity); + dialog.view.setStackingBehavior(builder.stackingBehavior); + boolean textAllCaps; + textAllCaps = DialogUtils.resolveBoolean(builder.context, android.R.attr.textAllCaps, true); + + MDButton positiveTextView = dialog.positiveButton; + dialog.setTypeface(positiveTextView, builder.mediumFont); + positiveTextView.setAllCapsCompat(textAllCaps); + positiveTextView.setText(builder.positiveText); + positiveTextView.setTextColor(builder.positiveColor); + dialog.positiveButton.setStackedSelector(dialog.getButtonSelector(DialogAction.POSITIVE, true)); + dialog.positiveButton.setDefaultSelector( + dialog.getButtonSelector(DialogAction.POSITIVE, false)); + dialog.positiveButton.setTag(DialogAction.POSITIVE); + dialog.positiveButton.setOnClickListener(dialog); + + MDButton negativeTextView = dialog.negativeButton; + dialog.setTypeface(negativeTextView, builder.mediumFont); + negativeTextView.setAllCapsCompat(textAllCaps); + negativeTextView.setText(builder.negativeText); + negativeTextView.setTextColor(builder.negativeColor); + dialog.negativeButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, true)); + dialog.negativeButton.setDefaultSelector( + dialog.getButtonSelector(DialogAction.NEGATIVE, false)); + dialog.negativeButton.setTag(DialogAction.NEGATIVE); + dialog.negativeButton.setOnClickListener(dialog); + + MDButton neutralTextView = dialog.neutralButton; + dialog.setTypeface(neutralTextView, builder.mediumFont); + neutralTextView.setAllCapsCompat(textAllCaps); + neutralTextView.setText(builder.neutralText); + neutralTextView.setTextColor(builder.neutralColor); + dialog.neutralButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, true)); + dialog.neutralButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, false)); + dialog.neutralButton.setTag(DialogAction.NEUTRAL); + dialog.neutralButton.setOnClickListener(dialog); + + // Setup list dialog stuff + if (builder.listCallbackMultiChoice != null) { + dialog.selectedIndicesList = new ArrayList<>(); + } + if (dialog.recyclerView != null) { + if (builder.adapter == null) { + // Determine list type + if (builder.listCallbackSingleChoice != null) { + dialog.listType = MaterialDialog.ListType.SINGLE; + } else if (builder.listCallbackMultiChoice != null) { + dialog.listType = MaterialDialog.ListType.MULTI; + if (builder.selectedIndices != null) { + dialog.selectedIndicesList = new ArrayList<>(Arrays.asList(builder.selectedIndices)); + builder.selectedIndices = null; + } + } else { + dialog.listType = MaterialDialog.ListType.REGULAR; + } + builder.adapter = + new DefaultRvAdapter(dialog, MaterialDialog.ListType.getLayoutForType(dialog.listType)); + } else if (builder.adapter instanceof MDAdapter) { + // Notify simple list adapter of the dialog it belongs to + ((MDAdapter) builder.adapter).setDialog(dialog); + } + } + + // Setup progress dialog stuff if needed + setupProgressDialog(dialog); + + // Setup input dialog stuff if needed + setupInputDialog(dialog); + + // Setup custom views + if (builder.customView != null) { + ((MDRootLayout) dialog.view.findViewById(R.id.md_root)).noTitleNoPadding(); + FrameLayout frame = dialog.view.findViewById(R.id.md_customViewFrame); + dialog.customViewFrame = frame; + View innerView = builder.customView; + if (innerView.getParent() != null) { + ((ViewGroup) innerView.getParent()).removeView(innerView); + } + if (builder.wrapCustomViewInScroll) { + /* Apply the frame padding to the content, this allows the ScrollView to draw it's + over scroll glow without clipping */ + final Resources r = dialog.getContext().getResources(); + final int framePadding = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + final ScrollView sv = new ScrollView(dialog.getContext()); + int paddingTop = r.getDimensionPixelSize(R.dimen.md_content_padding_top); + int paddingBottom = r.getDimensionPixelSize(R.dimen.md_content_padding_bottom); + sv.setClipToPadding(false); + if (innerView instanceof EditText) { + // Setting padding to an EditText causes visual errors, set it to the parent instead + sv.setPadding(framePadding, paddingTop, framePadding, paddingBottom); + } else { + // Setting padding to scroll view pushes the scroll bars out, don't do it if not necessary (like above) + sv.setPadding(0, paddingTop, 0, paddingBottom); + innerView.setPadding(framePadding, 0, framePadding, 0); + } + sv.addView( + innerView, + new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + innerView = sv; + } + frame.addView( + innerView, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + // Setup user listeners + if (builder.showListener != null) { + dialog.setOnShowListener(builder.showListener); + } + if (builder.cancelListener != null) { + dialog.setOnCancelListener(builder.cancelListener); + } + if (builder.dismissListener != null) { + dialog.setOnDismissListener(builder.dismissListener); + } + if (builder.keyListener != null) { + dialog.setOnKeyListener(builder.keyListener); + } + + // Setup internal show listener + dialog.setOnShowListenerInternal(); + + // Other internal initialization + dialog.invalidateList(); + dialog.setViewInternal(dialog.view); + dialog.checkIfListInitScroll(); + + // Min height and max width calculations + WindowManager wm = dialog.getWindow().getWindowManager(); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + final int windowWidth = size.x; + final int windowHeight = size.y; + + final int windowVerticalPadding = + builder.context.getResources().getDimensionPixelSize(R.dimen.md_dialog_vertical_margin); + final int windowHorizontalPadding = + builder.context.getResources().getDimensionPixelSize(R.dimen.md_dialog_horizontal_margin); + final int maxWidth = + builder.context.getResources().getDimensionPixelSize(R.dimen.md_dialog_max_width); + final int calculatedWidth = windowWidth - (windowHorizontalPadding * 2); + + dialog.view.setMaxHeight(windowHeight - windowVerticalPadding * 2); + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = Math.min(maxWidth, calculatedWidth); + dialog.getWindow().setAttributes(lp); + } + + private static int getDialogMenuOptionsItemActionIdByIndex(int index) { + if (index == 0) return R.id.menu_options_item_index_0; + if (index == 1) return R.id.menu_options_item_index_1; + if (index == 2) return R.id.menu_options_item_index_2; + if (index == 3) return R.id.menu_options_item_index_3; + if (index == 4) return R.id.menu_options_item_index_4; + if (index == 5) return R.id.menu_options_item_index_5; + if (index == 6) return R.id.menu_options_item_index_6; + if (index == 7) return R.id.menu_options_item_index_7; + if (index == 8) return R.id.menu_options_item_index_8; + if (index == 9) return R.id.menu_options_item_index_9; + if (index < 0) throw new IllegalArgumentException("Dialog menu options item index cannot be negative: " + index); + throw new IllegalArgumentException("Dialog menu cannot have more than 10 options"); + } + + private static void fixCanvasScalingWhenHardwareAccelerated(ProgressBar pb) { + } + + private static void setupProgressDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + if (builder.indeterminateProgress || builder.progress > -2) { + dialog.progressBar = dialog.view.findViewById(android.R.id.progress); + if (dialog.progressBar == null) { + return; + } + + if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + IndeterminateHorizontalProgressDrawable d = + new IndeterminateHorizontalProgressDrawable(builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } else { + IndeterminateCircularProgressDrawable d = + new IndeterminateCircularProgressDrawable(builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + } else { + HorizontalProgressDrawable d = new HorizontalProgressDrawable(builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + + if (!builder.indeterminateProgress || builder.indeterminateIsHorizontalProgress) { + dialog.progressBar.setIndeterminate( + builder.indeterminateProgress && builder.indeterminateIsHorizontalProgress); + dialog.progressBar.setProgress(0); + dialog.progressBar.setMax(builder.progressMax); + dialog.progressLabel = dialog.view.findViewById(R.id.md_label); + if (dialog.progressLabel != null) { + dialog.progressLabel.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressLabel, builder.mediumFont); + dialog.progressLabel.setText(builder.progressPercentFormat.format(0)); + } + dialog.progressMinMax = dialog.view.findViewById(R.id.md_minMax); + if (dialog.progressMinMax != null) { + dialog.progressMinMax.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressMinMax, builder.regularFont); + + if (builder.showMinMax) { + dialog.progressMinMax.setVisibility(View.VISIBLE); + dialog.progressMinMax.setText( + String.format(builder.progressNumberFormat, 0, builder.progressMax)); + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) dialog.progressBar.getLayoutParams(); + lp.leftMargin = 0; + lp.rightMargin = 0; + } else { + dialog.progressMinMax.setVisibility(View.GONE); + } + } else { + builder.showMinMax = false; + } + } + } + + if (dialog.progressBar != null) { + fixCanvasScalingWhenHardwareAccelerated(dialog.progressBar); + } + } + + private static void setupInputDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + dialog.input = dialog.view.findViewById(android.R.id.input); + if (dialog.input == null) { + return; + } + dialog.setTypeface(dialog.input, builder.regularFont); + if (builder.inputPrefill != null) { + dialog.input.setText(builder.inputPrefill); + } + dialog.setInternalInputCallback(); + dialog.input.setHint(builder.inputHint); + dialog.input.setSingleLine(); + dialog.input.setTextColor(builder.contentColor); + dialog.input.setHintTextColor(DialogUtils.adjustAlpha(builder.contentColor, 0.3f)); + + int widgetColor = dialog.builder.widgetColor; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Drawable textCursorDrawable = dialog.input.getTextCursorDrawable(); + textCursorDrawable.setTint(widgetColor); + dialog.input.setTextCursorDrawable(textCursorDrawable); + + Drawable textSelectHandle = dialog.input.getTextSelectHandle(); + textSelectHandle.setTint(widgetColor); + dialog.input.setTextSelectHandle(textSelectHandle); + + Drawable textSelectHandleLeft = dialog.input.getTextSelectHandleLeft(); + textSelectHandleLeft.setTint(widgetColor); + dialog.input.setTextSelectHandleLeft(textSelectHandleLeft); + + Drawable textSelectHandleRight = dialog.input.getTextSelectHandleRight(); + textSelectHandleRight.setTint(widgetColor); + dialog.input.setTextSelectHandleRight(textSelectHandleRight); + } + + MDTintHelper.setTint(dialog.input, widgetColor); + + if (builder.inputType != -1) { + dialog.input.setInputType(builder.inputType); + if (builder.inputType != InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + && (builder.inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) + == InputType.TYPE_TEXT_VARIATION_PASSWORD) { + // If the flags contain TYPE_TEXT_VARIATION_PASSWORD, apply the password transformation method automatically + dialog.input.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } + } + + dialog.inputMinMax = dialog.view.findViewById(R.id.md_minMax); + if (builder.inputMinLength > 0 || builder.inputMaxLength > -1) { + dialog.invalidateInputMinMaxIndicator( + dialog.input.getText().toString().length(), !builder.inputAllowEmpty); + } else { + dialog.inputMinMax.setVisibility(View.GONE); + dialog.inputMinMax = null; + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/GravityEnum.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/GravityEnum.java new file mode 100644 index 00000000..8db0875f --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/GravityEnum.java @@ -0,0 +1,41 @@ +package com.afollestad.materialdialogs; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Gravity; +import android.view.View; + +public enum GravityEnum { + START, + CENTER, + END; + + private static final boolean HAS_RTL = true; + + @SuppressLint("RtlHardcoded") + public int getGravityInt() { + switch (this) { + case START: + return HAS_RTL ? Gravity.START : Gravity.LEFT; + case CENTER: + return Gravity.CENTER_HORIZONTAL; + case END: + return HAS_RTL ? Gravity.END : Gravity.RIGHT; + default: + throw new IllegalStateException("Invalid gravity constant"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTextAlignment() { + switch (this) { + case CENTER: + return View.TEXT_ALIGNMENT_CENTER; + case END: + return View.TEXT_ALIGNMENT_VIEW_END; + default: + return View.TEXT_ALIGNMENT_VIEW_START; + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/MaterialDialog.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/MaterialDialog.java new file mode 100644 index 00000000..78349f73 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/MaterialDialog.java @@ -0,0 +1,2254 @@ +package com.afollestad.materialdialogs; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.text.Editable; +import android.text.Html; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.TextView; +import androidx.annotation.ArrayRes; +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntRange; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; +import androidx.core.content.res.ResourcesCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.afollestad.materialdialogs.internal.MDButton; +import com.afollestad.materialdialogs.internal.MDRootLayout; +import com.afollestad.materialdialogs.internal.MDTintHelper; +import com.afollestad.materialdialogs.internal.ThemeSingleton; +import com.afollestad.materialdialogs.util.DialogUtils; +import com.afollestad.materialdialogs.util.RippleHelper; +import com.afollestad.materialdialogs.util.TypefaceHelper; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** @author Aidan Follestad (afollestad) */ +public class MaterialDialog extends DialogBase implements View.OnClickListener, DefaultRvAdapter.InternalListCallback { + + protected final Builder builder; + private final Handler handler; + protected ImageView icon; + protected TextView title; + protected TextView content; + + EditText input; + RecyclerView recyclerView; + View titleFrame; + ImageView optionsView; + FrameLayout customViewFrame; + ProgressBar progressBar; + TextView progressLabel; + TextView progressMinMax; + TextView inputMinMax; + CheckBox checkBoxPrompt; + MDButton positiveButton; + MDButton neutralButton; + MDButton negativeButton; + ListType listType; + List selectedIndicesList; + + @SuppressLint("InflateParams") + protected MaterialDialog(Builder builder) { + super(builder.context, DialogInit.getTheme(builder)); + handler = new Handler(); + this.builder = builder; + final LayoutInflater inflater = LayoutInflater.from(builder.context); + view = (MDRootLayout) inflater.inflate(DialogInit.getInflateLayout(builder), null); + DialogInit.init(this); + } + + public final Builder getBuilder() { + return builder; + } + + public final void setTypeface(TextView target, Typeface t) { + if (t == null) { + return; + } + int flags = target.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG; + target.setPaintFlags(flags); + target.setTypeface(t); + } + + @SuppressWarnings("unused") + @Nullable + public Object getTag() { + return builder.tag; + } + + final void checkIfListInitScroll() { + if (recyclerView == null) { + return; + } + recyclerView + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @SuppressWarnings("ConstantConditions") + @Override + public void onGlobalLayout() { + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + if (listType == ListType.SINGLE || listType == ListType.MULTI) { + int selectedIndex; + if (listType == ListType.SINGLE) { + if (builder.selectedIndex < 0) { + return; + } + selectedIndex = builder.selectedIndex; + } else { + if (selectedIndicesList == null || selectedIndicesList.size() == 0) { + return; + } + Collections.sort(selectedIndicesList); + selectedIndex = selectedIndicesList.get(0); + } + + final int fSelectedIndex = selectedIndex; + recyclerView.post( + new Runnable() { + @Override + public void run() { + recyclerView.requestFocus(); + builder.layoutManager.scrollToPosition(fSelectedIndex); + } + }); + } + } + }); + } + + /** Sets the dialog RecyclerView's adapter/layout manager, and it's item click listener. */ + final void invalidateList() { + if (recyclerView == null) { + return; + } else if ((builder.items == null || builder.items.size() == 0) && builder.adapter == null) { + return; + } + if (builder.layoutManager == null) { + builder.layoutManager = new LinearLayoutManager(getContext()); + } + if (recyclerView.getLayoutManager() == null) { + recyclerView.setLayoutManager(builder.layoutManager); + } + recyclerView.setAdapter(builder.adapter); + if (listType != null) { + ((DefaultRvAdapter) builder.adapter).setCallback(this); + } + } + + @Override + public boolean onItemSelected( + MaterialDialog dialog, View view, int position, CharSequence text, boolean longPress) { + if (!view.isEnabled()) { + return false; + } + if (listType == null || listType == ListType.REGULAR) { + // Default adapter, non choice mode + if (builder.autoDismiss) { + // If auto dismiss is enabled, dismiss the dialog when a list item is selected + dismiss(); + } + if (!longPress && builder.listCallback != null) { + builder.listCallback.onSelection(this, view, position, builder.items.get(position)); + } + if (longPress && builder.listLongCallback != null) { + return builder.listLongCallback.onLongSelection( + this, view, position, builder.items.get(position)); + } + } else { + // Default adapter, choice mode + if (listType == ListType.MULTI) { + final CheckBox cb = view.findViewById(R.id.md_control); + if (!cb.isEnabled()) { + return false; + } + final boolean shouldBeChecked = !selectedIndicesList.contains(position); + if (shouldBeChecked) { + // Add the selection to the states first so the callback includes it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.add(position); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox wasn't previously selected, and the callback returns true, add it to the states and check it + if (sendMultiChoiceCallback()) { + cb.setChecked(true); + } else { + // The callback cancelled selection, remove it from the states + selectedIndicesList.remove(Integer.valueOf(position)); + } + } else { + // The callback was not used to check if selection is allowed, just select it + cb.setChecked(true); + } + } else { + // Remove the selection from the states first so the callback does not include it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.remove(Integer.valueOf(position)); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox was previously selected, and the callback returns true, remove it from the states and uncheck it + if (sendMultiChoiceCallback()) { + cb.setChecked(false); + } else { + // The callback cancelled unselection, re-add it to the states + selectedIndicesList.add(position); + } + } else { + // The callback was not used to check if the unselection is allowed, just uncheck it + cb.setChecked(false); + } + } + } else if (listType == ListType.SINGLE) { + final RadioButton radio = view.findViewById(R.id.md_control); + if (!radio.isEnabled()) { + return false; + } + boolean allowSelection = true; + final int oldSelected = builder.selectedIndex; + + if (builder.autoDismiss && builder.positiveText == null) { + // If auto dismiss is enabled, and no action button is visible to approve the selection, dismiss the dialog + dismiss(); + // Don't allow the selection to be updated since the dialog is being dismissed anyways + allowSelection = false; + // Update selected index and send callback + builder.selectedIndex = position; + sendSingleChoiceCallback(view); + } else if (builder.alwaysCallSingleChoiceCallback) { + // Temporarily set the new index so the callback uses the right one + builder.selectedIndex = position; + // Only allow the radio button to be checked if the callback returns true + allowSelection = sendSingleChoiceCallback(view); + // Restore the old selected index, so the state is updated below + builder.selectedIndex = oldSelected; + } + // Update the checked states + if (allowSelection) { + builder.selectedIndex = position; + radio.setChecked(true); + builder.adapter.notifyItemChanged(oldSelected); + builder.adapter.notifyItemChanged(position); + } + } + } + return true; + } + + final Drawable getListSelector() { + if (builder.listSelector != 0) { + return ResourcesCompat.getDrawable( + builder.context.getResources(), builder.listSelector, null); + } + final Drawable d = DialogUtils.resolveDrawable(builder.context, R.attr.md_list_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_list_selector); + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public boolean isPromptCheckBoxChecked() { + return checkBoxPrompt != null && checkBoxPrompt.isChecked(); + } + + @SuppressWarnings("unused") + public void setPromptCheckBoxChecked(boolean checked) { + if (checkBoxPrompt != null) { + checkBoxPrompt.setChecked(checked); + } + } + + /* package */ Drawable getButtonSelector(DialogAction which, boolean isStacked) { + if (isStacked) { + if (builder.btnSelectorStacked != 0) { + return ResourcesCompat.getDrawable( + builder.context.getResources(), builder.btnSelectorStacked, null); + } + final Drawable d = + DialogUtils.resolveDrawable(builder.context, R.attr.md_btn_stacked_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_stacked_selector); + } else { + switch (which) { + default: { + if (builder.btnSelectorPositive != 0) { + return ResourcesCompat.getDrawable( + builder.context.getResources(), builder.btnSelectorPositive, null); + } + Drawable d = + DialogUtils.resolveDrawable(builder.context, R.attr.md_btn_positive_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_positive_selector); + RippleHelper.applyColor(d, builder.buttonRippleColor); + return d; + } + case NEUTRAL: { + if (builder.btnSelectorNeutral != 0) { + return ResourcesCompat.getDrawable( + builder.context.getResources(), builder.btnSelectorNeutral, null); + } + Drawable d = + DialogUtils.resolveDrawable(builder.context, R.attr.md_btn_neutral_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_neutral_selector); + RippleHelper.applyColor(d, builder.buttonRippleColor); + return d; + } + case NEGATIVE: { + if (builder.btnSelectorNegative != 0) { + return ResourcesCompat.getDrawable( + builder.context.getResources(), builder.btnSelectorNegative, null); + } + Drawable d = + DialogUtils.resolveDrawable(builder.context, R.attr.md_btn_negative_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_negative_selector); + RippleHelper.applyColor(d, builder.buttonRippleColor); + return d; + } + } + } + } + + private boolean sendSingleChoiceCallback(View v) { + if (builder.listCallbackSingleChoice == null) { + return false; + } + CharSequence text = null; + if (builder.selectedIndex >= 0 && builder.selectedIndex < builder.items.size()) { + text = builder.items.get(builder.selectedIndex); + } + return builder.listCallbackSingleChoice.onSelection(this, v, builder.selectedIndex, text); + } + + private boolean sendMultiChoiceCallback() { + if (builder.listCallbackMultiChoice == null) { + return false; + } + Collections.sort(selectedIndicesList); // make sure the indices are in order + List selectedTitles = new ArrayList<>(); + for (Integer i : selectedIndicesList) { + if (i < 0 || i > builder.items.size() - 1) { + continue; + } + selectedTitles.add(builder.items.get(i)); + } + return builder.listCallbackMultiChoice.onSelection( + this, + selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]), + selectedTitles.toArray(new CharSequence[selectedTitles.size()])); + } + + @Override + public final void onClick(View v) { + DialogAction tag = (DialogAction) v.getTag(); + switch (tag) { + case POSITIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onPositive(this); + } + if (builder.onPositiveCallback != null) { + builder.onPositiveCallback.onClick(this, tag); + } + if (!builder.alwaysCallSingleChoiceCallback) { + sendSingleChoiceCallback(v); + } + if (!builder.alwaysCallMultiChoiceCallback) { + sendMultiChoiceCallback(); + } + if (builder.inputCallback != null && input != null && !builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(this, input.getText()); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + case NEGATIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNegative(this); + } + if (builder.onNegativeCallback != null) { + builder.onNegativeCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + cancel(); + } + break; + } + case NEUTRAL: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNeutral(this); + } + if (builder.onNeutralCallback != null) { + builder.onNeutralCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + } + if (builder.onAnyCallback != null) { + builder.onAnyCallback.onClick(this, tag); + } + } + + @Override + @UiThread + public void show() { + try { + super.show(); + } catch (WindowManager.BadTokenException e) { + throw new DialogException( + "Bad window token, you cannot show a dialog " + + "before an Activity is created or after it's hidden."); + } + } + + /** + * Retrieves the view of an action button, allowing you to modify properties such as whether or + * not it's enabled. Use {@link #setActionButton(DialogAction, int)} to change text, since the + * view returned here is not the view that displays text. + * + * @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 MDButton getActionButton(@NonNull DialogAction which) { + switch (which) { + default: + return positiveButton; + case NEUTRAL: + return neutralButton; + case NEGATIVE: + return negativeButton; + } + } + + /** Retrieves the view representing the dialog as a whole. Be careful with this. */ + public final View getView() { + return view; + } + + @Nullable + public final EditText getInputEditText() { + return input; + } + + /** + * Retrieves the TextView that contains the dialog title. If you want to update the title, use + * #{@link #setTitle(CharSequence)} instead. + */ + @SuppressWarnings("unused") + public final TextView getTitleView() { + return title; + } + + /** Retrieves the ImageView that contains the dialog icon. */ + @SuppressWarnings("unused") + public ImageView getIconView() { + return icon; + } + + /** + * Retrieves the TextView that contains the dialog content. If you want to update the content + * (message), use #{@link #setContent(CharSequence)} instead. + */ + @Nullable + @SuppressWarnings("unused") + public final TextView getContentView() { + return content; + } + + /** + * Retrieves the custom view that was inflated or set to the MaterialDialog during building. + * + * @return The custom view that was passed into the Builder. + */ + @Nullable + public final View getCustomView() { + return builder.customView; + } + + /** + * Updates an action button's title, causing invalidation to check if the action buttons should be + * stacked. Setting an action button's text to null is a shortcut for hiding it, too. + * + * @param which The action button to update. + * @param title The new title of the action button. + */ + @SuppressWarnings("WeakerAccess") + @UiThread + public final void setActionButton(@NonNull final DialogAction which, final CharSequence title) { + switch (which) { + case NEUTRAL: + builder.neutralText = title; + neutralButton.setText(title); + neutralButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + case NEGATIVE: + builder.negativeText = title; + negativeButton.setText(title); + negativeButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + default: + builder.positiveText = title; + positiveButton.setText(title); + positiveButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + } + } + + /** + * 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().getText(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. + */ + @SuppressWarnings("WeakerAccess") + public final int numberOfActionButtons() { + int number = 0; + if (positiveButton.getVisibility() == View.VISIBLE) { + number++; + } + if (neutralButton.getVisibility() == View.VISIBLE) { + number++; + } + if (negativeButton.getVisibility() == View.VISIBLE) { + number++; + } + return number; + } + + @UiThread + @Override + public final void setTitle(CharSequence newTitle) { + title.setText(newTitle); + } + + @UiThread + @Override + public final void setTitle(@StringRes int newTitleRes) { + setTitle(builder.context.getString(newTitleRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setTitle(@StringRes int newTitleRes, @Nullable Object... formatArgs) { + setTitle(builder.context.getString(newTitleRes, formatArgs)); + } + + @UiThread + public void setIcon(@DrawableRes final int resId) { + icon.setImageResource(resId); + icon.setVisibility(resId != 0 ? View.VISIBLE : View.GONE); + } + + @UiThread + public void setIcon(final Drawable d) { + icon.setImageDrawable(d); + icon.setVisibility(d != null ? View.VISIBLE : View.GONE); + } + + @SuppressWarnings("unused") + @UiThread + public void setIconAttribute(@AttrRes int attrId) { + Drawable d = DialogUtils.resolveDrawable(builder.context, attrId); + setIcon(d); + } + + @UiThread + public final void setContent(CharSequence newContent) { + content.setText(newContent); + content.setVisibility(TextUtils.isEmpty(newContent) ? View.GONE : View.VISIBLE); + } + + @UiThread + public final void setContent(@StringRes int newContentRes) { + setContent(builder.context.getString(newContentRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setContent(@StringRes int newContentRes, @Nullable Object... formatArgs) { + setContent(builder.context.getString(newContentRes, formatArgs)); + } + + @Nullable + public final ArrayList getItems() { + return builder.items; + } + + @UiThread + public final void setItems(CharSequence... items) { + if (builder.adapter == null) { + throw new IllegalStateException( + "This MaterialDialog instance does not " + + "yet have an adapter set to it. You cannot use setItems()."); + } + if (items != null) { + builder.items = new ArrayList<>(items.length); + Collections.addAll(builder.items, items); + } else { + builder.items = null; + } + if (!(builder.adapter instanceof DefaultRvAdapter)) { + throw new IllegalStateException( + "When using a custom adapter, setItems() " + + "cannot be used. Set items through the adapter instead."); + } + notifyItemsChanged(); + } + + @UiThread + public final void notifyItemInserted(@IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemInserted(index); + } + + @SuppressWarnings("unused") + @UiThread + public final void notifyItemChanged(@IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemChanged(index); + } + + @UiThread + public final void notifyItemsChanged() { + builder.adapter.notifyDataSetChanged(); + } + + public final int getCurrentProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getProgress(); + } + + @SuppressWarnings("unused") + public ProgressBar getProgressBar() { + return progressBar; + } + + public final void incrementProgress(final int by) { + setProgress(getCurrentProgress() + by); + } + + public final void setProgress(final int progress) { + if (builder.progress <= -2) { + Log.w( + "MaterialDialog", + "Calling setProgress(int) on an " + "indeterminate progress dialog has no effect!"); + return; + } + progressBar.setProgress(progress); + handler.post( + new Runnable() { + @Override + public void run() { + if (progressLabel != null) { + progressLabel.setText( + builder.progressPercentFormat.format( + (float) getCurrentProgress() / (float) getMaxProgress())); + } + if (progressMinMax != null) { + progressMinMax.setText( + String.format( + builder.progressNumberFormat, getCurrentProgress(), getMaxProgress())); + } + } + }); + } + + @SuppressWarnings("unused") + public final boolean isIndeterminateProgress() { + return builder.indeterminateProgress; + } + + public final int getMaxProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getMax(); + } + + @SuppressWarnings("unused") + public final void setMaxProgress(final int max) { + if (builder.progress <= -2) { + throw new IllegalStateException("Cannot use setMaxProgress() on this dialog."); + } + progressBar.setMax(max); + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + @SuppressWarnings("unused") + public final void setProgressPercentFormat(NumberFormat format) { + builder.progressPercentFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + /** + * Change the format of the small text showing current and maximum units of progress. The default + * is "%1d/%2d". + */ + @SuppressWarnings("unused") + public final void setProgressNumberFormat(String format) { + builder.progressNumberFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + public final boolean isCancelled() { + return !isShowing(); + } + + /** + * 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 + */ + @SuppressWarnings("unused") + public int getSelectedIndex() { + if (builder.listCallbackSingleChoice != null) { + return builder.selectedIndex; + } else { + return -1; + } + } + + /** + * Convenience method for setting the currently selected index of a single choice list. This only + * works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective single choice + * callback. + * + * @param index The index of the list item to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndex(int index) { + builder.selectedIndex = index; + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException( + "You can only use setSelectedIndex() " + "with the default adapter implementation."); + } + } + + /** + * 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 + @SuppressWarnings("unused") + public Integer[] getSelectedIndices() { + if (builder.listCallbackMultiChoice != null) { + return selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]); + } else { + return null; + } + } + + /** + * Convenience method for setting the currently selected indices of a multi choice list. This only + * works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective multi choice + * callback. + * + * @param indices The indices of the list items to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndices(@NonNull Integer[] indices) { + selectedIndicesList = new ArrayList<>(Arrays.asList(indices)); + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException( + "You can only use setSelectedIndices() " + "with the default adapter implementation."); + } + } + + /** Clears all selected checkboxes from multi choice list dialogs. */ + public void clearSelectedIndices() { + clearSelectedIndices(true); + } + + /** + * Clears all selected checkboxes from multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void clearSelectedIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException( + "You can only use clearSelectedIndices() " + "with multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList != null) { + selectedIndicesList.clear(); + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException( + "You can only use clearSelectedIndices() " + "with the default adapter implementation."); + } + } + + /** Selects all checkboxes in multi choice list dialogs. */ + @SuppressWarnings("unused") + public void selectAllIndices() { + selectAllIndices(true); + } + + /** + * Selects all checkboxes in multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void selectAllIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException( + "You can only use selectAllIndices() with " + "multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList == null) { + selectedIndicesList = new ArrayList<>(); + } + for (int i = 0; i < builder.adapter.getItemCount(); i++) { + if (!selectedIndicesList.contains(i)) { + selectedIndicesList.add(i); + } + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException( + "You can only use selectAllIndices() with the " + "default adapter implementation."); + } + } + + @Override + public final void onShow(DialogInterface dialog) { + if (input != null) { + DialogUtils.showKeyboard(this, builder); + if (input.getText().length() > 0) { + input.setSelection(input.getText().length()); + } + } + super.onShow(dialog); + } + + void setInternalInputCallback() { + if (input == null) { + return; + } + input.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int length = s.toString().length(); + boolean emptyDisabled = false; + if (!builder.inputAllowEmpty) { + emptyDisabled = length == 0; + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!emptyDisabled); + } + invalidateInputMinMaxIndicator(length, emptyDisabled); + if (builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(MaterialDialog.this, s); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + void invalidateInputMinMaxIndicator(int currentLength, boolean emptyDisabled) { + if (inputMinMax != null) { + if (builder.inputMaxLength > 0) { + inputMinMax.setText( + String.format(Locale.getDefault(), "%d/%d", currentLength, builder.inputMaxLength)); + inputMinMax.setVisibility(View.VISIBLE); + } else { + inputMinMax.setVisibility(View.GONE); + } + final boolean isDisabled = + (emptyDisabled && currentLength == 0) + || (builder.inputMaxLength > 0 && currentLength > builder.inputMaxLength) + || currentLength < builder.inputMinLength; + final int colorText = isDisabled ? builder.inputRangeErrorColor : builder.contentColor; + final int colorWidget = isDisabled ? builder.inputRangeErrorColor : builder.widgetColor; + if (builder.inputMaxLength > 0) { + inputMinMax.setTextColor(colorText); + } + MDTintHelper.setTint(input, colorWidget); + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!isDisabled); + } + } + + @Override + public void dismiss() { + if (input != null) { + DialogUtils.hideKeyboard(this, builder); + } + super.dismiss(); + } + + 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: + throw new IllegalArgumentException("Not a valid list type"); + } + } + } + + /** A callback used for regular list dialogs. */ + public interface ListCallback { + + void onSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** A callback used for regular list dialogs. */ + public interface ListLongCallback { + + boolean onLongSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** A callback used for multi choice (check box) list dialogs. */ + public interface ListCallbackSingleChoice { + + /** + * Return true to allow the radio button to be checked, if the alwaysCallSingleChoice() option + * is used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The index of the item that was selected. + * @param text The text of the item that was selected. + * @return True to allow the radio button to be selected. + */ + boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text); + } + + /** A callback used for multi choice (check box) list dialogs. */ + public interface ListCallbackMultiChoice { + + /** + * Return true to allow the check box to be checked, if the alwaysCallSingleChoice() option is + * used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The indices of the items that were selected. + * @param text The text of the items that were selected. + * @return True to allow the checkbox to be selected. + */ + boolean onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text); + } + + /** An alternate way to define a single callback. */ + public interface SingleButtonCallback { + + void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which); + } + + public interface InputCallback { + + void onInput(@NonNull MaterialDialog dialog, CharSequence input); + } + + private static class DialogException extends WindowManager.BadTokenException { + + DialogException(@SuppressWarnings("SameParameterValue") String message) { + super(message); + } + } + + /** The class used to construct a MaterialDialog. */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public static class Builder { + + protected final Context context; + protected CharSequence title; + protected List options; + protected GravityEnum titleGravity = GravityEnum.START; + protected GravityEnum contentGravity = GravityEnum.START; + protected GravityEnum btnStackedGravity = GravityEnum.END; + protected GravityEnum itemsGravity = GravityEnum.START; + protected GravityEnum buttonsGravity = GravityEnum.START; + protected int buttonRippleColor = 0; + protected int titleColor = -1; + protected int contentColor = -1; + protected CharSequence content; + protected ArrayList items; + protected CharSequence positiveText; + protected CharSequence neutralText; + protected CharSequence negativeText; + protected boolean positiveFocus; + protected boolean neutralFocus; + protected boolean negativeFocus; + protected View customView; + protected int widgetColor; + protected ColorStateList widgetColorStateList; + protected ColorStateList choiceWidgetColor; + protected ColorStateList positiveColor; + protected ColorStateList negativeColor; + protected ColorStateList neutralColor; + protected ColorStateList linkColor; + protected ButtonCallback callback; + protected SingleButtonCallback onPositiveCallback; + protected SingleButtonCallback onNegativeCallback; + protected SingleButtonCallback onNeutralCallback; + protected SingleButtonCallback onAnyCallback; + protected ListCallback listCallback; + protected ListLongCallback listLongCallback; + protected ListCallbackSingleChoice listCallbackSingleChoice; + protected ListCallbackMultiChoice listCallbackMultiChoice; + protected boolean alwaysCallMultiChoiceCallback = false; + protected boolean alwaysCallSingleChoiceCallback = false; + protected Theme theme = Theme.LIGHT; + protected boolean cancelable = true; + protected boolean canceledOnTouchOutside = true; + protected float contentLineSpacingMultiplier = 1.2f; + protected int selectedIndex = -1; + protected Integer[] selectedIndices = null; + protected Integer[] disabledIndices = null; + protected boolean autoDismiss = true; + protected Typeface regularFont; + protected Typeface mediumFont; + protected Drawable icon; + protected boolean limitIconToDefaultSize; + protected int maxIconSize = -1; + protected RecyclerView.Adapter adapter; + protected RecyclerView.LayoutManager layoutManager; + protected OnDismissListener dismissListener; + protected OnCancelListener cancelListener; + protected OnKeyListener keyListener; + protected OnShowListener showListener; + protected StackingBehavior stackingBehavior; + protected boolean wrapCustomViewInScroll; + protected int dividerColor; + protected int backgroundColor; + protected int itemColor; + protected boolean indeterminateProgress; + protected boolean showMinMax; + protected int progress = -2; + protected int progressMax = 0; + protected CharSequence inputPrefill; + protected CharSequence inputHint; + protected InputCallback inputCallback; + protected boolean inputAllowEmpty; + protected int inputType = -1; + protected boolean alwaysCallInputCallback; + protected int inputMinLength = -1; + protected int inputMaxLength = -1; + protected int inputRangeErrorColor = 0; + protected int[] itemIds; + protected CharSequence checkBoxPrompt; + protected boolean checkBoxPromptInitiallyChecked; + protected CheckBox.OnCheckedChangeListener checkBoxPromptListener; + + protected String progressNumberFormat; + protected NumberFormat progressPercentFormat; + protected boolean indeterminateIsHorizontalProgress; + + protected boolean titleColorSet = false; + protected boolean contentColorSet = false; + protected boolean itemColorSet = false; + protected boolean positiveColorSet = false; + protected boolean neutralColorSet = false; + protected boolean negativeColorSet = false; + protected boolean widgetColorSet = false; + protected boolean dividerColorSet = false; + + @DrawableRes + protected int listSelector; + @DrawableRes + protected int btnSelectorStacked; + @DrawableRes + protected int btnSelectorPositive; + @DrawableRes + protected int btnSelectorNeutral; + @DrawableRes + protected int btnSelectorNegative; + + protected Object tag; + + public Builder(@NonNull Context context) { + this.context = context; + final int materialBlue = DialogUtils.getColor(context, R.color.md_material_blue_600); + + // Retrieve default accent colors, which are used on the action buttons and progress bars + this.widgetColor = DialogUtils.resolveColor(context, android.R.attr.colorAccent, materialBlue); + + this.positiveColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.negativeColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.neutralColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.linkColor = + DialogUtils.getActionTextStateList( + context, DialogUtils.resolveColor(context, R.attr.md_link_color, this.widgetColor)); + + int fallback = 0; + fallback = DialogUtils.resolveColor(context, android.R.attr.colorControlHighlight); + this.buttonRippleColor = + DialogUtils.resolveColor( + context, + R.attr.md_btn_ripple_color, + DialogUtils.resolveColor(context, android.R.attr.colorControlHighlight, fallback)); + + this.progressPercentFormat = NumberFormat.getPercentInstance(); + this.progressNumberFormat = "%1d/%2d"; + + // Set the default theme based on the Activity theme's primary color darkness (more white or more black) + final int primaryTextColor = + DialogUtils.resolveColor(context, android.R.attr.textColorPrimary); + this.theme = DialogUtils.isColorDark(primaryTextColor) ? Theme.LIGHT : Theme.DARK; + + // Load theme values from the ThemeSingleton if needed + checkSingleton(); + + // Retrieve gravity settings from global theme attributes if needed + this.titleGravity = + DialogUtils.resolveGravityEnum(context, R.attr.md_title_gravity, this.titleGravity); + this.contentGravity = + DialogUtils.resolveGravityEnum(context, R.attr.md_content_gravity, this.contentGravity); + this.btnStackedGravity = + DialogUtils.resolveGravityEnum( + context, R.attr.md_btnstacked_gravity, this.btnStackedGravity); + this.itemsGravity = + DialogUtils.resolveGravityEnum(context, R.attr.md_items_gravity, this.itemsGravity); + this.buttonsGravity = + DialogUtils.resolveGravityEnum(context, R.attr.md_buttons_gravity, this.buttonsGravity); + + final String mediumFont = DialogUtils.resolveString(context, R.attr.md_medium_font); + final String regularFont = DialogUtils.resolveString(context, R.attr.md_regular_font); + try { + typeface(mediumFont, regularFont); + } catch (Throwable ignored) { + } + + if (this.mediumFont == null) { + try { + this.mediumFont = Typeface.create("sans-serif-medium", Typeface.NORMAL); + } catch (Throwable ignored) { + this.mediumFont = Typeface.DEFAULT_BOLD; + } + } + if (this.regularFont == null) { + try { + this.regularFont = Typeface.create("sans-serif", Typeface.NORMAL); + } catch (Throwable ignored) { + this.regularFont = Typeface.SANS_SERIF; + if (this.regularFont == null) { + this.regularFont = Typeface.DEFAULT; + } + } + } + } + + public final Context getContext() { + return context; + } + + public final int getItemColor() { + return itemColor; + } + + public final Typeface getRegularFont() { + return regularFont; + } + + @SuppressWarnings("ConstantConditions") + private void checkSingleton() { + if (ThemeSingleton.get(false) == null) { + return; + } + ThemeSingleton s = ThemeSingleton.get(); + if (s.darkTheme) { + this.theme = Theme.DARK; + } + if (s.titleColor != 0) { + this.titleColor = s.titleColor; + } + if (s.contentColor != 0) { + this.contentColor = s.contentColor; + } + if (s.positiveColor != null) { + this.positiveColor = s.positiveColor; + } + if (s.neutralColor != null) { + this.neutralColor = s.neutralColor; + } + if (s.negativeColor != null) { + this.negativeColor = s.negativeColor; + } + if (s.itemColor != 0) { + this.itemColor = s.itemColor; + } + if (s.icon != null) { + this.icon = s.icon; + } + if (s.backgroundColor != 0) { + this.backgroundColor = s.backgroundColor; + } + if (s.dividerColor != 0) { + this.dividerColor = s.dividerColor; + } + if (s.btnSelectorStacked != 0) { + this.btnSelectorStacked = s.btnSelectorStacked; + } + if (s.listSelector != 0) { + this.listSelector = s.listSelector; + } + if (s.btnSelectorPositive != 0) { + this.btnSelectorPositive = s.btnSelectorPositive; + } + if (s.btnSelectorNeutral != 0) { + this.btnSelectorNeutral = s.btnSelectorNeutral; + } + if (s.btnSelectorNegative != 0) { + this.btnSelectorNegative = s.btnSelectorNegative; + } + if (s.widgetColor != 0) { + this.widgetColor = s.widgetColor; + } + if (s.linkColor != null) { + this.linkColor = s.linkColor; + } + this.titleGravity = s.titleGravity; + this.contentGravity = s.contentGravity; + this.btnStackedGravity = s.btnStackedGravity; + this.itemsGravity = s.itemsGravity; + this.buttonsGravity = s.buttonsGravity; + } + + public Builder title(@StringRes int titleRes) { + title(this.context.getText(titleRes)); + return this; + } + + public Builder title(@NonNull CharSequence title) { + this.title = title; + return this; + } + + public Builder options(@NonNull List options) { + this.options = options; + return this; + } + + public Builder titleGravity(@NonNull GravityEnum gravity) { + this.titleGravity = gravity; + return this; + } + + public Builder buttonRippleColor(@ColorInt int color) { + this.buttonRippleColor = color; + return this; + } + + public Builder buttonRippleColorRes(@ColorRes int colorRes) { + return buttonRippleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder buttonRippleColorAttr(@AttrRes int colorAttr) { + return buttonRippleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder titleColor(@ColorInt int color) { + this.titleColor = color; + this.titleColorSet = true; + return this; + } + + public Builder titleColorRes(@ColorRes int colorRes) { + return titleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder titleColorAttr(@AttrRes int colorAttr) { + return titleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + /** + * Sets the fonts used in the dialog. It's recommended that you use {@link #typeface(String, + * String)} instead, to avoid duplicate Typeface allocations and high memory usage. + * + * @param medium The font used on titles and action buttons. Null uses device default. + * @param regular The font used everywhere else, like on the content and list items. Null uses + * device default. + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable Typeface medium, @Nullable Typeface regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + /** + * Sets the fonts used in the dialog, by file names. This also uses TypefaceHelper in order to + * avoid any un-needed allocations (it recycles typefaces for you). + * + * @param medium The name of font in assets/fonts used on titles and action buttons (null uses + * device default). E.g. [your-project]/app/main/assets/fonts/[medium] + * @param regular The name of font in assets/fonts used everywhere else, like content and list + * items (null uses device default). E.g. [your-project]/app/main/assets/fonts/[regular] + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable String medium, @Nullable String regular) { + if (medium != null && !medium.trim().isEmpty()) { + this.mediumFont = TypefaceHelper.get(this.context, medium); + if (this.mediumFont == null) { + throw new IllegalArgumentException("No font asset found for \"" + medium + "\""); + } + } + if (regular != null && !regular.trim().isEmpty()) { + this.regularFont = TypefaceHelper.get(this.context, regular); + if (this.regularFont == null) { + throw new IllegalArgumentException("No font asset found for \"" + regular + "\""); + } + } + return this; + } + + public Builder icon(@NonNull Drawable icon) { + this.icon = icon; + return this; + } + + public Builder iconRes(@DrawableRes int icon) { + this.icon = ResourcesCompat.getDrawable(context.getResources(), icon, null); + return this; + } + + public Builder iconAttr(@AttrRes int iconAttr) { + this.icon = DialogUtils.resolveDrawable(context, iconAttr); + return this; + } + + public Builder content(@StringRes int contentRes) { + return content(contentRes, false); + } + + public Builder content(@StringRes int contentRes, boolean html) { + CharSequence text = this.context.getText(contentRes); + if (html) { + text = Html.fromHtml(text.toString().replace("\n", "
")); + } + return content(text); + } + + public Builder content(@NonNull CharSequence content) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set content() " + "when you're using a custom view."); + } + this.content = content; + return this; + } + + public Builder content(@StringRes int contentRes, Object... formatArgs) { + String str = + String.format(this.context.getString(contentRes), formatArgs).replace("\n", "
"); + // noinspection deprecation + return content(Html.fromHtml(str)); + } + + public Builder contentColor(@ColorInt int color) { + this.contentColor = color; + this.contentColorSet = true; + return this; + } + + public Builder contentColorRes(@ColorRes int colorRes) { + contentColor(DialogUtils.getColor(this.context, colorRes)); + return this; + } + + public Builder contentColorAttr(@AttrRes int colorAttr) { + contentColor(DialogUtils.resolveColor(this.context, colorAttr)); + return this; + } + + public Builder contentGravity(@NonNull GravityEnum gravity) { + this.contentGravity = gravity; + return this; + } + + public Builder contentLineSpacing(float multiplier) { + this.contentLineSpacingMultiplier = multiplier; + return this; + } + + public Builder items(@NonNull Collection collection) { + if (collection.size() > 0) { + final CharSequence[] array = new CharSequence[collection.size()]; + int i = 0; + for (Object obj : collection) { + array[i] = obj.toString(); + i++; + } + items(array); + } else if (collection.size() == 0) { + items = new ArrayList<>(); + } + return this; + } + + public Builder items(@ArrayRes int itemsRes) { + items(this.context.getResources().getTextArray(itemsRes)); + return this; + } + + public Builder items(@NonNull CharSequence... items) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set items()" + " when you're using a custom view."); + } + this.items = new ArrayList<>(); + Collections.addAll(this.items, items); + return this; + } + + public Builder itemsCallback(@NonNull ListCallback callback) { + this.listCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsLongCallback(@NonNull ListLongCallback callback) { + this.listLongCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsColor(@ColorInt int color) { + this.itemColor = color; + this.itemColorSet = true; + return this; + } + + public Builder itemsColorRes(@ColorRes int colorRes) { + return itemsColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder itemsColorAttr(@AttrRes int colorAttr) { + return itemsColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder itemsGravity(@NonNull GravityEnum gravity) { + this.itemsGravity = gravity; + return this; + } + + public Builder itemsIds(@NonNull int[] idsArray) { + this.itemIds = idsArray; + return this; + } + + public Builder itemsIds(@ArrayRes int idsArrayRes) { + return itemsIds(context.getResources().getIntArray(idsArrayRes)); + } + + public Builder buttonsGravity(@NonNull GravityEnum gravity) { + this.buttonsGravity = gravity; + 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, @NonNull ListCallbackSingleChoice callback) { + this.selectedIndex = selectedIndex; + this.listCallback = null; + this.listCallbackSingleChoice = callback; + this.listCallbackMultiChoice = null; + return this; + } + + /** + * By default, the single choice callback is only called when the user clicks the positive + * button or if there are no buttons. Call this to force it to always call on item clicks even + * if the positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallSingleChoiceCallback() { + this.alwaysCallSingleChoiceCallback = true; + 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( + @Nullable Integer[] selectedIndices, @NonNull ListCallbackMultiChoice callback) { + this.selectedIndices = selectedIndices; + this.listCallback = null; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = callback; + return this; + } + + /** + * Sets indices of items that are not clickable. If they are checkboxes or radio buttons, they + * will not be toggleable. + * + * @param disabledIndices The item indices that will be disabled from selection. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsDisabledIndices(@Nullable Integer... disabledIndices) { + this.disabledIndices = disabledIndices; + return this; + } + + /** + * By default, the multi choice callback is only called when the user clicks the positive button + * or if there are no buttons. Call this to force it to always call on item clicks even if the + * positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallMultiChoiceCallback() { + this.alwaysCallMultiChoiceCallback = true; + return this; + } + + public Builder positiveText(@StringRes int positiveRes) { + if (positiveRes == 0) { + return this; + } + positiveText(this.context.getText(positiveRes)); + return this; + } + + public Builder positiveText(@NonNull CharSequence message) { + this.positiveText = message; + return this; + } + + public Builder positiveColor(@ColorInt int color) { + return positiveColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder positiveColorRes(@ColorRes int colorRes) { + return positiveColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder positiveColorAttr(@AttrRes int colorAttr) { + return positiveColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder positiveColor(@NonNull ColorStateList colorStateList) { + this.positiveColor = colorStateList; + this.positiveColorSet = true; + return this; + } + + public Builder positiveFocus(boolean isFocusedDefault) { + this.positiveFocus = isFocusedDefault; + return this; + } + + public Builder neutralText(@StringRes int neutralRes) { + if (neutralRes == 0) { + return this; + } + return neutralText(this.context.getText(neutralRes)); + } + + public Builder neutralText(@NonNull CharSequence message) { + this.neutralText = message; + return this; + } + + public Builder negativeColor(@ColorInt int color) { + return negativeColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder negativeColorRes(@ColorRes int colorRes) { + return negativeColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder negativeColorAttr(@AttrRes int colorAttr) { + return negativeColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder negativeColor(@NonNull ColorStateList colorStateList) { + this.negativeColor = colorStateList; + this.negativeColorSet = true; + return this; + } + + public Builder negativeText(@StringRes int negativeRes) { + if (negativeRes == 0) { + return this; + } + return negativeText(this.context.getText(negativeRes)); + } + + public Builder negativeText(@NonNull CharSequence message) { + this.negativeText = message; + return this; + } + + public Builder negativeFocus(boolean isFocusedDefault) { + this.negativeFocus = isFocusedDefault; + return this; + } + + public Builder neutralColor(@ColorInt int color) { + return neutralColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder neutralColorRes(@ColorRes int colorRes) { + return neutralColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder neutralColorAttr(@AttrRes int colorAttr) { + return neutralColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder neutralColor(@NonNull ColorStateList colorStateList) { + this.neutralColor = colorStateList; + this.neutralColorSet = true; + return this; + } + + public Builder neutralFocus(boolean isFocusedDefault) { + this.neutralFocus = isFocusedDefault; + return this; + } + + public Builder linkColor(@ColorInt int color) { + return linkColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder linkColorRes(@ColorRes int colorRes) { + return linkColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder linkColorAttr(@AttrRes int colorAttr) { + return linkColor(DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder linkColor(@NonNull ColorStateList colorStateList) { + this.linkColor = colorStateList; + return this; + } + + public Builder listSelector(@DrawableRes int selectorRes) { + this.listSelector = selectorRes; + return this; + } + + public Builder btnSelectorStacked(@DrawableRes int selectorRes) { + this.btnSelectorStacked = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes) { + this.btnSelectorPositive = selectorRes; + this.btnSelectorNeutral = selectorRes; + this.btnSelectorNegative = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes, @NonNull DialogAction which) { + switch (which) { + case NEUTRAL: + this.btnSelectorNeutral = selectorRes; + break; + case NEGATIVE: + this.btnSelectorNegative = selectorRes; + break; + default: + this.btnSelectorPositive = selectorRes; + break; + } + return this; + } + + /** + * Sets the gravity used for the text in stacked action buttons. By default, it's #{@link + * GravityEnum#END}. + * + * @param gravity The gravity to use. + * @return The Builder instance so calls can be chained. + */ + public Builder btnStackedGravity(@NonNull GravityEnum gravity) { + this.btnStackedGravity = gravity; + return this; + } + + public Builder checkBoxPrompt( + @NonNull CharSequence prompt, + boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + this.checkBoxPrompt = prompt; + this.checkBoxPromptInitiallyChecked = initiallyChecked; + this.checkBoxPromptListener = checkListener; + return this; + } + + public Builder checkBoxPromptRes( + @StringRes int prompt, + boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + return checkBoxPrompt( + context.getResources().getText(prompt), initiallyChecked, checkListener); + } + + public Builder customView(@LayoutRes int layoutRes, boolean wrapInScrollView) { + LayoutInflater li = LayoutInflater.from(this.context); + return customView(li.inflate(layoutRes, null), wrapInScrollView); + } + + public Builder customView(@NonNull View view, boolean wrapInScrollView) { + if (this.content != null) { + throw new IllegalStateException("You cannot use customView() when you have content set."); + } else if (this.items != null) { + throw new IllegalStateException("You cannot use customView() when you have items set."); + } else if (this.inputCallback != null) { + throw new IllegalStateException("You cannot use customView() with an input dialog"); + } else if (this.progress > -2 || this.indeterminateProgress) { + throw new IllegalStateException("You cannot use customView() with a progress dialog"); + } + if (view.getParent() != null && view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).removeView(view); + } + this.customView = view; + this.wrapCustomViewInScroll = wrapInScrollView; + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get to. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set progress() when you're using a custom view."); + } + if (indeterminate) { + this.indeterminateProgress = true; + this.progress = -2; + } else { + this.indeterminateIsHorizontalProgress = false; + this.indeterminateProgress = false; + this.progress = -1; + this.progressMax = max; + } + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get to. + * @param showMinMax For determinate dialogs, the min and max will be displayed to the left + * (start) of the progress bar, e.g. 50/100. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max, boolean showMinMax) { + this.showMinMax = showMinMax; + return progress(indeterminate, max); + } + + /** + * hange the format of the small text showing current and maximum units of progress. The default + * is "%1d/%2d". + */ + public Builder progressNumberFormat(@NonNull String format) { + this.progressNumberFormat = format; + return this; + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + public Builder progressPercentFormat(@NonNull NumberFormat format) { + this.progressPercentFormat = format; + return this; + } + + /** + * By default, indeterminate progress dialogs will use a circular indicator. You can change it + * to use a horizontal progress indicator. + */ + public Builder progressIndeterminateStyle(boolean horizontal) { + this.indeterminateIsHorizontalProgress = horizontal; + return this; + } + + public Builder widgetColor(@ColorInt int color) { + this.widgetColor = color; + this.widgetColorSet = true; + return this; + } + + public Builder widgetColor(ColorStateList colorStateList) { + this.widgetColor = colorStateList.getDefaultColor(); + this.widgetColorStateList = colorStateList; + this.widgetColorSet = true; + return this; + } + + public Builder widgetColorRes(@ColorRes int colorRes) { + return widgetColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder widgetColorAttr(@AttrRes int colorAttr) { + return widgetColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder choiceWidgetColor(@Nullable ColorStateList colorStateList) { + this.choiceWidgetColor = colorStateList; + return this; + } + + public Builder dividerColor(@ColorInt int color) { + this.dividerColor = color; + this.dividerColorSet = true; + return this; + } + + public Builder dividerColorRes(@ColorRes int colorRes) { + return dividerColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder dividerColorAttr(@AttrRes int colorAttr) { + return dividerColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder backgroundColor(@ColorInt int color) { + this.backgroundColor = color; + return this; + } + + public Builder backgroundColorRes(@ColorRes int colorRes) { + return backgroundColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder backgroundColorAttr(@AttrRes int colorAttr) { + return backgroundColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder callback(@NonNull ButtonCallback callback) { + this.callback = callback; + return this; + } + + public Builder onPositive(@NonNull SingleButtonCallback callback) { + this.onPositiveCallback = callback; + return this; + } + + public Builder onNegative(@NonNull SingleButtonCallback callback) { + this.onNegativeCallback = callback; + return this; + } + + public Builder onNeutral(@NonNull SingleButtonCallback callback) { + this.onNeutralCallback = callback; + return this; + } + + public Builder onAny(@NonNull SingleButtonCallback callback) { + this.onAnyCallback = callback; + return this; + } + + public Builder theme(@NonNull Theme theme) { + this.theme = theme; + return this; + } + + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + this.canceledOnTouchOutside = cancelable; + return this; + } + + public Builder canceledOnTouchOutside(boolean canceledOnTouchOutside) { + this.canceledOnTouchOutside = canceledOnTouchOutside; + 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 androidx.recyclerview.widget.RecyclerView.Adapter} for the dialog's list + * + * @param adapter The adapter to set to the list. + * @param layoutManager The layout manager to use in the RecyclerView. Pass null to use the + * default linear manager. + * @return This Builder object to allow for chaining of calls to set methods + */ + @SuppressWarnings("ConstantConditions") + public Builder adapter( + @NonNull RecyclerView.Adapter adapter, + @Nullable RecyclerView.LayoutManager layoutManager) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set adapter() when " + "you're using a custom view."); + } + if (layoutManager != null + && !(layoutManager instanceof LinearLayoutManager) + && !(layoutManager instanceof GridLayoutManager)) { + throw new IllegalStateException( + "You can currently only use LinearLayoutManager" + + " and GridLayoutManager with this library."); + } + this.adapter = adapter; + this.layoutManager = layoutManager; + return this; + } + + /** Limits the display size of a set icon to 48dp. */ + public Builder limitIconToDefaultSize() { + this.limitIconToDefaultSize = true; + return this; + } + + public Builder maxIconSize(int maxIconSize) { + this.maxIconSize = maxIconSize; + return this; + } + + public Builder maxIconSizeRes(@DimenRes int maxIconSizeRes) { + return maxIconSize((int) this.context.getResources().getDimension(maxIconSizeRes)); + } + + public Builder showListener(@NonNull OnShowListener listener) { + this.showListener = listener; + return this; + } + + public Builder dismissListener(@NonNull OnDismissListener listener) { + this.dismissListener = listener; + return this; + } + + public Builder cancelListener(@NonNull OnCancelListener listener) { + this.cancelListener = listener; + return this; + } + + public Builder keyListener(@NonNull OnKeyListener listener) { + this.keyListener = listener; + return this; + } + + /** + * Sets action button stacking behavior. + * + * @param behavior The behavior of the action button stacking logic. + * @return The Builder instance so you can chain calls to it. + */ + public Builder stackingBehavior(@NonNull StackingBehavior behavior) { + this.stackingBehavior = behavior; + return this; + } + + public Builder input( + @Nullable CharSequence hint, + @Nullable CharSequence prefill, + boolean allowEmptyInput, + @NonNull InputCallback callback) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set content() when " + "you're using a custom view."); + } + this.inputCallback = callback; + this.inputHint = hint; + this.inputPrefill = prefill; + this.inputAllowEmpty = allowEmptyInput; + return this; + } + + public Builder input( + @Nullable CharSequence hint, + @Nullable CharSequence prefill, + @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder input( + @StringRes int hint, + @StringRes int prefill, + boolean allowEmptyInput, + @NonNull InputCallback callback) { + return input( + hint == 0 ? null : context.getText(hint), + prefill == 0 ? null : context.getText(prefill), + allowEmptyInput, + callback); + } + + public Builder input( + @StringRes int hint, @StringRes int prefill, @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder inputType(int type) { + this.inputType = type; + return this; + } + + public Builder inputRange( + @IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength) { + return inputRange(minLength, maxLength, 0); + } + + /** @param errorColor Pass in 0 for the default red error color (as specified in guidelines). */ + public Builder inputRange( + @IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorInt int errorColor) { + if (minLength < 0) { + throw new IllegalArgumentException( + "Min length for input dialogs " + "cannot be less than 0."); + } + this.inputMinLength = minLength; + this.inputMaxLength = maxLength; + if (errorColor == 0) { + this.inputRangeErrorColor = DialogUtils.getColor(context, R.color.md_edittext_error); + } else { + this.inputRangeErrorColor = errorColor; + } + if (this.inputMinLength > 0) { + this.inputAllowEmpty = false; + } + return this; + } + + /** + * Same as #{@link #inputRange(int, int, int)}, but it takes a color resource ID for the error + * color. + */ + public Builder inputRangeRes( + @IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorRes int errorColor) { + return inputRange(minLength, maxLength, DialogUtils.getColor(context, errorColor)); + } + + public Builder alwaysCallInputCallback() { + this.alwaysCallInputCallback = true; + return this; + } + + public Builder tag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + @UiThread + public MaterialDialog build() { + return new MaterialDialog(this); + } + + @UiThread + public MaterialDialog show() { + MaterialDialog dialog = build(); + dialog.show(); + return dialog; + } + } + + /** + * Override these as needed, so no needing to sub empty methods from an interface + * + * @deprecated Use the individual onPositive, onNegative, onNeutral, or onAny Builder methods + * instead. + */ + @SuppressWarnings({"WeakerAccess", "UnusedParameters"}) + @Deprecated + public abstract static class ButtonCallback { + + public ButtonCallback() { + super(); + } + + @Deprecated + public void onAny(MaterialDialog dialog) { + } + + @Deprecated + public void onPositive(MaterialDialog dialog) { + } + + @Deprecated + public void onNegative(MaterialDialog dialog) { + } + + // The overidden methods below prevent Android Studio from suggesting that they are overidden by developers + + @Deprecated + public void onNeutral(MaterialDialog dialog) { + } + + @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(); + } + } + + public record OptionMenuItemSpec(String title, OnClickListener onClickListener) { + public interface OnClickListener { + void onClick(MaterialDialog dialog); + } + } + +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/StackingBehavior.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/StackingBehavior.java new file mode 100644 index 00000000..d429a780 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/StackingBehavior.java @@ -0,0 +1,11 @@ +package com.afollestad.materialdialogs; + +/** @author Aidan Follestad (afollestad) */ +public enum StackingBehavior { + /** The action buttons are always stacked vertically. */ + ALWAYS, + /** The action buttons are stacked vertically IF it is necessary for them to fit in the dialog. */ + ADAPTIVE, + /** The action buttons are never stacked, even if they should be. */ + NEVER +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/Theme.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/Theme.java new file mode 100644 index 00000000..c0c17fcc --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/Theme.java @@ -0,0 +1,7 @@ +package com.afollestad.materialdialogs; + +/** @author Aidan Follestad (afollestad) */ +public enum Theme { + LIGHT, + DARK +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/CircleView.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/CircleView.java new file mode 100644 index 00000000..1b192ef4 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/CircleView.java @@ -0,0 +1,204 @@ +package com.afollestad.materialdialogs.color; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.StateListDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.os.Build; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.FloatRange; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import com.afollestad.materialdialogs.util.DialogUtils; + +public class CircleView extends FrameLayout { + + private final int borderWidthSmall; + private final int borderWidthLarge; + + private final Paint outerPaint; + private final Paint whitePaint; + private final Paint innerPaint; + private boolean selected; + + public CircleView(Context context) { + this(context, null, 0); + } + + public CircleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + final Resources r = getResources(); + borderWidthSmall = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, r.getDisplayMetrics()); + borderWidthLarge = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, r.getDisplayMetrics()); + + whitePaint = new Paint(); + whitePaint.setAntiAlias(true); + whitePaint.setColor(Color.WHITE); + innerPaint = new Paint(); + innerPaint.setAntiAlias(true); + outerPaint = new Paint(); + outerPaint.setAntiAlias(true); + + update(Color.DKGRAY); + setWillNotDraw(false); + } + + @ColorInt + private static int translucentColor(int color) { + final float factor = 0.7f; + int alpha = Math.round(Color.alpha(color) * factor); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + return Color.argb(alpha, red, green, blue); + } + + @ColorInt + public static int shiftColor(@ColorInt int color, @FloatRange(from = 0.0f, to = 2.0f) float by) { + if (by == 1f) { + return color; + } + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= by; // value component + return Color.HSVToColor(hsv); + } + + @ColorInt + public static int shiftColorDown(@ColorInt int color) { + return shiftColor(color, 0.9f); + } + + @ColorInt + public static int shiftColorUp(@ColorInt int color) { + return shiftColor(color, 1.1f); + } + + private void update(@ColorInt int color) { + innerPaint.setColor(color); + outerPaint.setColor(shiftColorDown(color)); + + Drawable selector = createSelector(color); + int[][] states = new int[][]{new int[]{android.R.attr.state_pressed}}; + int[] colors = new int[]{shiftColorUp(color)}; + ColorStateList rippleColors = new ColorStateList(states, colors); + setForeground(new RippleDrawable(rippleColors, selector, null)); + } + + @Override + public void setBackgroundColor(@ColorInt int color) { + update(color); + requestLayout(); + invalidate(); + } + + @Override + public void setBackgroundResource(@ColorRes int color) { + setBackgroundColor(DialogUtils.getColor(getContext(), color)); + } + + /** @deprecated */ + @Deprecated + @Override + public void setBackground(Drawable background) { + throw new IllegalStateException("Cannot use setBackground() on CircleView."); + } + + /** @deprecated */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public void setBackgroundDrawable(Drawable background) { + throw new IllegalStateException("Cannot use setBackgroundDrawable() on CircleView."); + } + + /** @deprecated */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public void setActivated(boolean activated) { + throw new IllegalStateException("Cannot use setActivated() on CircleView."); + } + + public void setSelected(boolean selected) { + this.selected = selected; + requestLayout(); + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + final int outerRadius = getMeasuredWidth() / 2; + if (selected) { + final int whiteRadius = outerRadius - borderWidthLarge; + final int innerRadius = whiteRadius - borderWidthSmall; + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius, outerPaint); + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, whiteRadius, whitePaint); + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, innerRadius, innerPaint); + } else { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius, innerPaint); + } + } + + private Drawable createSelector(int color) { + ShapeDrawable darkerCircle = new ShapeDrawable(new OvalShape()); + darkerCircle.getPaint().setColor(translucentColor(shiftColorUp(color))); + StateListDrawable stateListDrawable = new StateListDrawable(); + stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, darkerCircle); + return stateListDrawable; + } + + public void showHint(int color) { + final int[] screenPos = new int[2]; + final Rect displayFrame = new Rect(); + getLocationOnScreen(screenPos); + getWindowVisibleDisplayFrame(displayFrame); + final Context context = getContext(); + final int width = getWidth(); + final int height = getHeight(); + final int midy = screenPos[1] + height / 2; + int referenceX = screenPos[0] + width / 2; + if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) { + final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; + referenceX = screenWidth - referenceX; // mirror + } + Toast cheatSheet = + Toast.makeText(context, String.format("#%06X", 0xFFFFFF & color), Toast.LENGTH_SHORT); + if (midy < displayFrame.height()) { + // Show along the top; follow action buttons + cheatSheet.setGravity( + Gravity.TOP | GravityCompat.END, referenceX, screenPos[1] + height - displayFrame.top); + } else { + // Show along the bottom center + cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); + } + cheatSheet.show(); + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorChooserDialog.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorChooserDialog.java new file mode 100644 index 00000000..3fed6482 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorChooserDialog.java @@ -0,0 +1,815 @@ +package com.afollestad.materialdialogs.color; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.GridView; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.annotation.ArrayRes; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringDef; +import androidx.annotation.StringRes; +import androidx.core.content.res.ResourcesCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.Theme; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.internal.MDTintHelper; +import com.afollestad.materialdialogs.util.DialogUtils; + +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; + +/** @author Aidan Follestad (afollestad) */ +@SuppressWarnings({"FieldCanBeLocal", "ConstantConditions"}) +public class ColorChooserDialog extends DialogFragment + implements View.OnClickListener, View.OnLongClickListener { + + public static final String TAG_PRIMARY = "[MD_COLOR_CHOOSER]"; + public static final String TAG_ACCENT = "[MD_COLOR_CHOOSER]"; + public static final String TAG_CUSTOM = "[MD_COLOR_CHOOSER]"; + private int[] colorsTop; + @Nullable + private int[][] colorsSub; + private int circleSize; + private ColorCallback callback; + private GridView grid; + private View colorChooserCustomFrame; + private EditText customColorHex; + private View customColorIndicator; + private TextWatcher customColorTextWatcher; + private SeekBar customSeekA; + private TextView customSeekAValue; + private SeekBar customSeekR; + private TextView customSeekRValue; + private SeekBar customSeekG; + private TextView customSeekGValue; + private SeekBar customSeekB; + private TextView customSeekBValue; + private SeekBar.OnSeekBarChangeListener customColorRgbListener; + private int selectedCustomColor; + + public ColorChooserDialog() { + } + + @Nullable + public static ColorChooserDialog findVisible( + @NonNull FragmentManager fragmentManager, @ColorChooserTag String tag) { + Fragment frag = fragmentManager.findFragmentByTag(tag); + if (frag != null && frag instanceof ColorChooserDialog) { + return (ColorChooserDialog) frag; + } + return null; + } + + private void generateColors() { + Builder builder = getBuilder(); + if (builder.colorsTop != null) { + colorsTop = builder.colorsTop; + colorsSub = builder.colorsSub; + return; + } + + if (builder.accentMode) { + colorsTop = ColorPalette.ACCENT_COLORS; + colorsSub = ColorPalette.ACCENT_COLORS_SUB; + } else { + colorsTop = ColorPalette.PRIMARY_COLORS; + colorsSub = ColorPalette.PRIMARY_COLORS_SUB; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("top_index", topIndex()); + outState.putBoolean("in_sub", isInSub()); + outState.putInt("sub_index", subIndex()); + outState.putBoolean( + "in_custom", + colorChooserCustomFrame != null && colorChooserCustomFrame.getVisibility() == View.VISIBLE); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getActivity() instanceof ColorCallback) { + callback = (ColorCallback) getActivity(); + } else if (getParentFragment() instanceof ColorCallback) { + callback = (ColorCallback) getParentFragment(); + } else { + throw new IllegalStateException( + "ColorChooserDialog needs to be shown from an Activity/Fragment implementing ColorCallback."); + } + } + + private boolean isInSub() { + return getArguments().getBoolean("in_sub", false); + } + + private void isInSub(boolean value) { + getArguments().putBoolean("in_sub", value); + } + + private int topIndex() { + return getArguments().getInt("top_index", -1); + } + + private void topIndex(int value) { + if (value > -1) { + findSubIndexForColor(value, colorsTop[value]); + } + getArguments().putInt("top_index", value); + } + + private int subIndex() { + if (colorsSub == null) { + return -1; + } + return getArguments().getInt("sub_index", -1); + } + + private void subIndex(int value) { + if (colorsSub == null) { + return; + } + getArguments().putInt("sub_index", value); + } + + @StringRes + public int getTitle() { + Builder builder = getBuilder(); + int title; + if (isInSub()) { + title = builder.titleSub; + } else { + title = builder.title; + } + if (title == 0) { + title = builder.title; + } + return title; + } + + public String tag() { + Builder builder = getBuilder(); + if (builder.tag != null) { + return builder.tag; + } else { + return super.getTag(); + } + } + + public boolean isAccentMode() { + return getBuilder().accentMode; + } + + @Override + public void onClick(View v) { + if (v.getTag() != null) { + final String[] tag = ((String) v.getTag()).split(":"); + final int index = Integer.parseInt(tag[0]); + final MaterialDialog dialog = (MaterialDialog) getDialog(); + final Builder builder = getBuilder(); + + if (isInSub()) { + subIndex(index); + } else { + topIndex(index); + if (colorsSub != null && index < colorsSub.length) { + dialog.setActionButton(DialogAction.NEGATIVE, builder.backBtn); + isInSub(true); + } + } + + if (builder.allowUserCustom) { + selectedCustomColor = getSelectedColor(); + } + invalidateDynamicButtonColors(); + invalidate(); + } + } + + @Override + public boolean onLongClick(View v) { + if (v.getTag() != null) { + final String[] tag = ((String) v.getTag()).split(":"); + final int color = Integer.parseInt(tag[1]); + ((CircleView) v).showHint(color); + return true; + } + return false; + } + + private void invalidateDynamicButtonColors() { + final MaterialDialog dialog = (MaterialDialog) getDialog(); + if (dialog == null) { + return; + } + final Builder builder = getBuilder(); + if (builder.dynamicButtonColor) { + int selectedColor = getSelectedColor(); + if (Color.alpha(selectedColor) < 64 + || (Color.red(selectedColor) > 247 + && Color.green(selectedColor) > 247 + && Color.blue(selectedColor) > 247)) { + // Once we get close to white or transparent, + // the action buttons and seekbars will be a very light gray. + selectedColor = Color.parseColor("#DEDEDE"); + } + + if (getBuilder().dynamicButtonColor) { + dialog.getActionButton(DialogAction.POSITIVE).setTextColor(selectedColor); + dialog.getActionButton(DialogAction.NEGATIVE).setTextColor(selectedColor); + dialog.getActionButton(DialogAction.NEUTRAL).setTextColor(selectedColor); + } + + if (customSeekR != null) { + if (customSeekA.getVisibility() == View.VISIBLE) { + MDTintHelper.setTint(customSeekA, selectedColor); + } + MDTintHelper.setTint(customSeekR, selectedColor); + MDTintHelper.setTint(customSeekG, selectedColor); + MDTintHelper.setTint(customSeekB, selectedColor); + } + } + } + + @ColorInt + private int getSelectedColor() { + if (colorChooserCustomFrame != null + && colorChooserCustomFrame.getVisibility() == View.VISIBLE) { + return selectedCustomColor; + } + + int color = 0; + if (subIndex() > -1) { + color = colorsSub[topIndex()][subIndex()]; + } else if (topIndex() > -1) { + color = colorsTop[topIndex()]; + } + if (color == 0) { + color = DialogUtils.resolveColor(getActivity(), android.R.attr.colorAccent); + } + return color; + } + + private void findSubIndexForColor(int topIndex, int color) { + if (colorsSub == null || colorsSub.length - 1 < topIndex) { + return; + } + int[] subColors = colorsSub[topIndex]; + for (int subIndex = 0; subIndex < subColors.length; subIndex++) { + if (subColors[subIndex] == color) { + subIndex(subIndex); + break; + } + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getArguments() == null || !getArguments().containsKey("builder")) { + throw new IllegalStateException( + "ColorChooserDialog should be created using its Builder interface."); + } + generateColors(); + + int preselectColor; + boolean foundPreselectColor = false; + + if (savedInstanceState != null) { + foundPreselectColor = !savedInstanceState.getBoolean("in_custom", false); + preselectColor = getSelectedColor(); + } else { + if (getBuilder().setPreselectionColor) { + preselectColor = getBuilder().preselectColor; + if (preselectColor != 0) { + for (int topIndex = 0; topIndex < colorsTop.length; topIndex++) { + if (colorsTop[topIndex] == preselectColor) { + foundPreselectColor = true; + topIndex(topIndex); + if (getBuilder().accentMode) { + subIndex(2); + } else if (colorsSub != null) { + findSubIndexForColor(topIndex, preselectColor); + } else { + subIndex(5); + } + break; + } + + if (colorsSub != null) { + for (int subIndex = 0; subIndex < colorsSub[topIndex].length; subIndex++) { + if (colorsSub[topIndex][subIndex] == preselectColor) { + foundPreselectColor = true; + topIndex(topIndex); + subIndex(subIndex); + break; + } + } + if (foundPreselectColor) { + break; + } + } + } + } + } else { + preselectColor = Color.BLACK; + foundPreselectColor = true; + } + } + + circleSize = getResources().getDimensionPixelSize(R.dimen.md_colorchooser_circlesize); + final Builder builder = getBuilder(); + + MaterialDialog.Builder bd = + new MaterialDialog.Builder(getActivity()) + .title(getTitle()) + .autoDismiss(false) + .customView(R.layout.md_dialog_colorchooser, false) + .negativeText(builder.cancelBtn) + .positiveText(builder.doneBtn) + .neutralText(builder.allowUserCustom ? builder.customBtn : 0) + .typeface(builder.mediumFont, builder.regularFont) + .onPositive((dialog, which) -> { + callback.onColorSelection(ColorChooserDialog.this, getSelectedColor()); + dismiss(); + }) + .onNegative((dialog, which) -> { + if (isInSub()) { + dialog.setActionButton(DialogAction.NEGATIVE, getBuilder().cancelBtn); + isInSub(false); + subIndex(-1); // Do this to avoid ArrayIndexOutOfBoundsException + invalidate(); + } else { + dialog.cancel(); + } + }) + .onNeutral((dialog, which) -> toggleCustom(dialog)) + .showListener(dialog -> invalidateDynamicButtonColors()); + + if (builder.theme != null) { + bd.theme(builder.theme); + } + + final MaterialDialog dialog = bd.build(); + final View v = dialog.getCustomView(); + grid = v.findViewById(R.id.md_grid); + + if (builder.allowUserCustom) { + selectedCustomColor = preselectColor; + colorChooserCustomFrame = v.findViewById(R.id.md_colorChooserCustomFrame); + customColorHex = v.findViewById(R.id.md_hexInput); + customColorIndicator = v.findViewById(R.id.md_colorIndicator); + customSeekA = v.findViewById(R.id.md_colorA); + customSeekAValue = v.findViewById(R.id.md_colorAValue); + customSeekR = v.findViewById(R.id.md_colorR); + customSeekRValue = v.findViewById(R.id.md_colorRValue); + customSeekG = v.findViewById(R.id.md_colorG); + customSeekGValue = v.findViewById(R.id.md_colorGValue); + customSeekB = v.findViewById(R.id.md_colorB); + customSeekBValue = v.findViewById(R.id.md_colorBValue); + + if (!builder.allowUserCustomAlpha) { + v.findViewById(R.id.md_colorALabel).setVisibility(View.GONE); + customSeekA.setVisibility(View.GONE); + customSeekAValue.setVisibility(View.GONE); + customColorHex.setHint("2196F3"); + customColorHex.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)}); + } else { + customColorHex.setHint("FF2196F3"); + customColorHex.setFilters(new InputFilter[]{new InputFilter.LengthFilter(8)}); + } + + if (!foundPreselectColor) { + // If color wasn't found in the preset colors, it must be custom + toggleCustom(dialog); + } + } + + invalidate(); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (callback != null) { + callback.onColorChooserDismissed(this); + } + } + + private void toggleCustom(MaterialDialog dialog) { + if (dialog == null) { + dialog = (MaterialDialog) getDialog(); + } + if (grid.getVisibility() == View.VISIBLE) { + dialog.setTitle(getBuilder().customBtn); + dialog.setActionButton(DialogAction.NEUTRAL, getBuilder().presetsBtn); + dialog.setActionButton(DialogAction.NEGATIVE, getBuilder().cancelBtn); + grid.setVisibility(View.INVISIBLE); + colorChooserCustomFrame.setVisibility(View.VISIBLE); + + customColorTextWatcher = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + try { + selectedCustomColor = Color.parseColor("#" + s.toString()); + } catch (IllegalArgumentException e) { + selectedCustomColor = Color.BLACK; + } + customColorIndicator.setBackgroundColor(selectedCustomColor); + if (customSeekA.getVisibility() == View.VISIBLE) { + int alpha = Color.alpha(selectedCustomColor); + customSeekA.setProgress(alpha); + customSeekAValue.setText(String.format(Locale.US, "%d", alpha)); + } + int red = Color.red(selectedCustomColor); + customSeekR.setProgress(red); + int green = Color.green(selectedCustomColor); + customSeekG.setProgress(green); + int blue = Color.blue(selectedCustomColor); + customSeekB.setProgress(blue); + isInSub(false); + topIndex(-1); + subIndex(-1); + invalidateDynamicButtonColors(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }; + + customColorHex.addTextChangedListener(customColorTextWatcher); + customColorRgbListener = + new SeekBar.OnSeekBarChangeListener() { + + @SuppressLint("DefaultLocale") + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + if (getBuilder().allowUserCustomAlpha) { + int color = + Color.argb( + customSeekA.getProgress(), + customSeekR.getProgress(), + customSeekG.getProgress(), + customSeekB.getProgress()); + customColorHex.setText(String.format("%08X", color)); + } else { + int color = + Color.rgb( + customSeekR.getProgress(), + customSeekG.getProgress(), + customSeekB.getProgress()); + customColorHex.setText(String.format("%06X", 0xFFFFFF & color)); + } + } + customSeekAValue.setText(String.format("%d", customSeekA.getProgress())); + customSeekRValue.setText(String.format("%d", customSeekR.getProgress())); + customSeekGValue.setText(String.format("%d", customSeekG.getProgress())); + customSeekBValue.setText(String.format("%d", customSeekB.getProgress())); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + + customSeekR.setOnSeekBarChangeListener(customColorRgbListener); + customSeekG.setOnSeekBarChangeListener(customColorRgbListener); + customSeekB.setOnSeekBarChangeListener(customColorRgbListener); + if (customSeekA.getVisibility() == View.VISIBLE) { + customSeekA.setOnSeekBarChangeListener(customColorRgbListener); + customColorHex.setText(String.format("%08X", selectedCustomColor)); + } else { + customColorHex.setText(String.format("%06X", 0xFFFFFF & selectedCustomColor)); + } + } else { + dialog.setTitle(getBuilder().title); + dialog.setActionButton(DialogAction.NEUTRAL, getBuilder().customBtn); + if (isInSub()) { + dialog.setActionButton(DialogAction.NEGATIVE, getBuilder().backBtn); + } else { + dialog.setActionButton(DialogAction.NEGATIVE, getBuilder().cancelBtn); + } + grid.setVisibility(View.VISIBLE); + colorChooserCustomFrame.setVisibility(View.GONE); + customColorHex.removeTextChangedListener(customColorTextWatcher); + customColorTextWatcher = null; + customSeekR.setOnSeekBarChangeListener(null); + customSeekG.setOnSeekBarChangeListener(null); + customSeekB.setOnSeekBarChangeListener(null); + customColorRgbListener = null; + } + } + + private void invalidate() { + if (grid.getAdapter() == null) { + grid.setAdapter(new ColorGridAdapter()); + grid.setSelector( + ResourcesCompat.getDrawable(getResources(), R.drawable.md_transparent, null)); + } else { + ((BaseAdapter) grid.getAdapter()).notifyDataSetChanged(); + } + if (getDialog() != null) { + getDialog().setTitle(getTitle()); + } + } + + private Builder getBuilder() { + if (getArguments() == null || !getArguments().containsKey("builder")) { + return null; + } + return (Builder) getArguments().getSerializable("builder"); + } + + private void dismissIfNecessary(FragmentManager fragmentManager, String tag) { + Fragment frag = fragmentManager.findFragmentByTag(tag); + if (frag != null) { + ((DialogFragment) frag).dismiss(); + fragmentManager.beginTransaction().remove(frag).commit(); + } + } + + @NonNull + public ColorChooserDialog show(FragmentManager fragmentManager) { + String tag; + Builder builder = getBuilder(); + if (builder.colorsTop != null) { + tag = TAG_CUSTOM; + } else if (builder.accentMode) { + tag = TAG_ACCENT; + } else { + tag = TAG_PRIMARY; + } + dismissIfNecessary(fragmentManager, tag); + show(fragmentManager, tag); + return this; + } + + @NonNull + public ColorChooserDialog show(FragmentActivity fragmentActivity) { + return show(fragmentActivity.getSupportFragmentManager()); + } + + @Retention(RetentionPolicy.SOURCE) + @StringDef({TAG_PRIMARY, TAG_ACCENT, TAG_CUSTOM}) + public @interface ColorChooserTag { + } + + public interface ColorCallback { + + void onColorSelection(@NonNull ColorChooserDialog dialog, @ColorInt int selectedColor); + + void onColorChooserDismissed(@NonNull ColorChooserDialog dialog); + } + + public static class Builder implements Serializable { + + @NonNull + final transient Context context; + @Nullable + String mediumFont; + @Nullable + String regularFont; + @StringRes + final int title; + @StringRes + int titleSub; + @ColorInt + int preselectColor; + @StringRes + int doneBtn = R.string.md_done_label; + @StringRes + int backBtn = R.string.md_back_label; + @StringRes + int cancelBtn = R.string.md_cancel_label; + @StringRes + int customBtn = R.string.md_custom_label; + @StringRes + int presetsBtn = R.string.md_presets_label; + @Nullable + int[] colorsTop; + @Nullable + int[][] colorsSub; + @Nullable + String tag; + @Nullable + Theme theme; + + boolean accentMode = false; + boolean dynamicButtonColor = true; + boolean allowUserCustom = true; + boolean allowUserCustomAlpha = true; + boolean setPreselectionColor = false; + + public Builder(@NonNull Context context, @StringRes int title) { + this.context = context; + this.title = title; + } + + @NonNull + public Builder typeface(@Nullable String medium, @Nullable String regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + @NonNull + public Builder titleSub(@StringRes int titleSub) { + this.titleSub = titleSub; + return this; + } + + @NonNull + public Builder tag(@Nullable String tag) { + this.tag = tag; + return this; + } + + @NonNull + public Builder theme(@NonNull Theme theme) { + this.theme = theme; + return this; + } + + @NonNull + public Builder preselect(@ColorInt int preselect) { + preselectColor = preselect; + setPreselectionColor = true; + return this; + } + + @NonNull + public Builder accentMode(boolean accentMode) { + this.accentMode = accentMode; + return this; + } + + @NonNull + public Builder doneButton(@StringRes int text) { + doneBtn = text; + return this; + } + + @NonNull + public Builder backButton(@StringRes int text) { + backBtn = text; + return this; + } + + @NonNull + public Builder cancelButton(@StringRes int text) { + cancelBtn = text; + return this; + } + + @NonNull + public Builder customButton(@StringRes int text) { + customBtn = text; + return this; + } + + @NonNull + public Builder presetsButton(@StringRes int text) { + presetsBtn = text; + return this; + } + + @NonNull + public Builder dynamicButtonColor(boolean enabled) { + dynamicButtonColor = enabled; + return this; + } + + @NonNull + public Builder customColors(@NonNull int[] topLevel, @Nullable int[][] subLevel) { + colorsTop = topLevel; + colorsSub = subLevel; + return this; + } + + @NonNull + public Builder customColors(@ArrayRes int topLevel, @Nullable int[][] subLevel) { + colorsTop = DialogUtils.getColorArray(context, topLevel); + colorsSub = subLevel; + return this; + } + + @NonNull + public Builder allowUserColorInput(boolean allow) { + allowUserCustom = allow; + return this; + } + + @NonNull + public Builder allowUserColorInputAlpha(boolean allow) { + allowUserCustomAlpha = allow; + return this; + } + + @NonNull + public ColorChooserDialog build() { + ColorChooserDialog dialog = new ColorChooserDialog(); + Bundle args = new Bundle(); + args.putSerializable("builder", this); + dialog.setArguments(args); + return dialog; + } + + @NonNull + public ColorChooserDialog show(FragmentManager fragmentManager) { + ColorChooserDialog dialog = build(); + dialog.show(fragmentManager); + return dialog; + } + + @NonNull + public ColorChooserDialog show(FragmentActivity fragmentActivity) { + return show(fragmentActivity.getSupportFragmentManager()); + } + } + + private class ColorGridAdapter extends BaseAdapter { + + ColorGridAdapter() { + } + + @Override + public int getCount() { + if (isInSub()) { + return colorsSub[topIndex()].length; + } else { + return colorsTop.length; + } + } + + @Override + public Object getItem(int position) { + if (isInSub()) { + return colorsSub[topIndex()][position]; + } else { + return colorsTop[position]; + } + } + + @Override + public long getItemId(int position) { + return position; + } + + @SuppressLint("DefaultLocale") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = new CircleView(getContext()); + convertView.setLayoutParams(new GridView.LayoutParams(circleSize, circleSize)); + } + CircleView child = (CircleView) convertView; + @ColorInt final int color = isInSub() ? colorsSub[topIndex()][position] : colorsTop[position]; + child.setBackgroundColor(color); + if (isInSub()) { + child.setSelected(subIndex() == position); + } else { + child.setSelected(topIndex() == position); + } + child.setTag(String.format("%d:%d", position, color)); + child.setOnClickListener(ColorChooserDialog.this); + child.setOnLongClickListener(ColorChooserDialog.this); + return convertView; + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorPalette.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorPalette.java new file mode 100644 index 00000000..55e6fb96 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/ColorPalette.java @@ -0,0 +1,382 @@ +package com.afollestad.materialdialogs.color; + +import android.graphics.Color; + +/** @author Aidan Follestad (afollestad) */ +class ColorPalette { + + static final int[] PRIMARY_COLORS = + new int[] { + Color.parseColor("#F44336"), + Color.parseColor("#E91E63"), + Color.parseColor("#9C27B0"), + Color.parseColor("#673AB7"), + Color.parseColor("#3F51B5"), + Color.parseColor("#2196F3"), + Color.parseColor("#03A9F4"), + Color.parseColor("#00BCD4"), + Color.parseColor("#009688"), + Color.parseColor("#4CAF50"), + Color.parseColor("#8BC34A"), + Color.parseColor("#CDDC39"), + Color.parseColor("#FFEB3B"), + Color.parseColor("#FFC107"), + Color.parseColor("#FF9800"), + Color.parseColor("#FF5722"), + Color.parseColor("#795548"), + Color.parseColor("#9E9E9E"), + Color.parseColor("#607D8B") + }; + + static final int[][] PRIMARY_COLORS_SUB = + new int[][] { + new int[] { + Color.parseColor("#FFEBEE"), + Color.parseColor("#FFCDD2"), + Color.parseColor("#EF9A9A"), + Color.parseColor("#E57373"), + Color.parseColor("#EF5350"), + Color.parseColor("#F44336"), + Color.parseColor("#E53935"), + Color.parseColor("#D32F2F"), + Color.parseColor("#C62828"), + Color.parseColor("#B71C1C") + }, + new int[] { + Color.parseColor("#FCE4EC"), + Color.parseColor("#F8BBD0"), + Color.parseColor("#F48FB1"), + Color.parseColor("#F06292"), + Color.parseColor("#EC407A"), + Color.parseColor("#E91E63"), + Color.parseColor("#D81B60"), + Color.parseColor("#C2185B"), + Color.parseColor("#AD1457"), + Color.parseColor("#880E4F") + }, + new int[] { + Color.parseColor("#F3E5F5"), + Color.parseColor("#E1BEE7"), + Color.parseColor("#CE93D8"), + Color.parseColor("#BA68C8"), + Color.parseColor("#AB47BC"), + Color.parseColor("#9C27B0"), + Color.parseColor("#8E24AA"), + Color.parseColor("#7B1FA2"), + Color.parseColor("#6A1B9A"), + Color.parseColor("#4A148C") + }, + new int[] { + Color.parseColor("#EDE7F6"), + Color.parseColor("#D1C4E9"), + Color.parseColor("#B39DDB"), + Color.parseColor("#9575CD"), + Color.parseColor("#7E57C2"), + Color.parseColor("#673AB7"), + Color.parseColor("#5E35B1"), + Color.parseColor("#512DA8"), + Color.parseColor("#4527A0"), + Color.parseColor("#311B92") + }, + new int[] { + Color.parseColor("#E8EAF6"), + Color.parseColor("#C5CAE9"), + Color.parseColor("#9FA8DA"), + Color.parseColor("#7986CB"), + Color.parseColor("#5C6BC0"), + Color.parseColor("#3F51B5"), + Color.parseColor("#3949AB"), + Color.parseColor("#303F9F"), + Color.parseColor("#283593"), + Color.parseColor("#1A237E") + }, + new int[] { + Color.parseColor("#E3F2FD"), + Color.parseColor("#BBDEFB"), + Color.parseColor("#90CAF9"), + Color.parseColor("#64B5F6"), + Color.parseColor("#42A5F5"), + Color.parseColor("#2196F3"), + Color.parseColor("#1E88E5"), + Color.parseColor("#1976D2"), + Color.parseColor("#1565C0"), + Color.parseColor("#0D47A1") + }, + new int[] { + Color.parseColor("#E1F5FE"), + Color.parseColor("#B3E5FC"), + Color.parseColor("#81D4FA"), + Color.parseColor("#4FC3F7"), + Color.parseColor("#29B6F6"), + Color.parseColor("#03A9F4"), + Color.parseColor("#039BE5"), + Color.parseColor("#0288D1"), + Color.parseColor("#0277BD"), + Color.parseColor("#01579B") + }, + new int[] { + Color.parseColor("#E0F7FA"), + Color.parseColor("#B2EBF2"), + Color.parseColor("#80DEEA"), + Color.parseColor("#4DD0E1"), + Color.parseColor("#26C6DA"), + Color.parseColor("#00BCD4"), + Color.parseColor("#00ACC1"), + Color.parseColor("#0097A7"), + Color.parseColor("#00838F"), + Color.parseColor("#006064") + }, + new int[] { + Color.parseColor("#E0F2F1"), + Color.parseColor("#B2DFDB"), + Color.parseColor("#80CBC4"), + Color.parseColor("#4DB6AC"), + Color.parseColor("#26A69A"), + Color.parseColor("#009688"), + Color.parseColor("#00897B"), + Color.parseColor("#00796B"), + Color.parseColor("#00695C"), + Color.parseColor("#004D40") + }, + new int[] { + Color.parseColor("#E8F5E9"), + Color.parseColor("#C8E6C9"), + Color.parseColor("#A5D6A7"), + Color.parseColor("#81C784"), + Color.parseColor("#66BB6A"), + Color.parseColor("#4CAF50"), + Color.parseColor("#43A047"), + Color.parseColor("#388E3C"), + Color.parseColor("#2E7D32"), + Color.parseColor("#1B5E20") + }, + new int[] { + Color.parseColor("#F1F8E9"), + Color.parseColor("#DCEDC8"), + Color.parseColor("#C5E1A5"), + Color.parseColor("#AED581"), + Color.parseColor("#9CCC65"), + Color.parseColor("#8BC34A"), + Color.parseColor("#7CB342"), + Color.parseColor("#689F38"), + Color.parseColor("#558B2F"), + Color.parseColor("#33691E") + }, + new int[] { + Color.parseColor("#F9FBE7"), + Color.parseColor("#F0F4C3"), + Color.parseColor("#E6EE9C"), + Color.parseColor("#DCE775"), + Color.parseColor("#D4E157"), + Color.parseColor("#CDDC39"), + Color.parseColor("#C0CA33"), + Color.parseColor("#AFB42B"), + Color.parseColor("#9E9D24"), + Color.parseColor("#827717") + }, + new int[] { + Color.parseColor("#FFFDE7"), + Color.parseColor("#FFF9C4"), + Color.parseColor("#FFF59D"), + Color.parseColor("#FFF176"), + Color.parseColor("#FFEE58"), + Color.parseColor("#FFEB3B"), + Color.parseColor("#FDD835"), + Color.parseColor("#FBC02D"), + Color.parseColor("#F9A825"), + Color.parseColor("#F57F17") + }, + new int[] { + Color.parseColor("#FFF8E1"), + Color.parseColor("#FFECB3"), + Color.parseColor("#FFE082"), + Color.parseColor("#FFD54F"), + Color.parseColor("#FFCA28"), + Color.parseColor("#FFC107"), + Color.parseColor("#FFB300"), + Color.parseColor("#FFA000"), + Color.parseColor("#FF8F00"), + Color.parseColor("#FF6F00") + }, + new int[] { + Color.parseColor("#FFF3E0"), + Color.parseColor("#FFE0B2"), + Color.parseColor("#FFCC80"), + Color.parseColor("#FFB74D"), + Color.parseColor("#FFA726"), + Color.parseColor("#FF9800"), + Color.parseColor("#FB8C00"), + Color.parseColor("#F57C00"), + Color.parseColor("#EF6C00"), + Color.parseColor("#E65100") + }, + new int[] { + Color.parseColor("#FBE9E7"), + Color.parseColor("#FFCCBC"), + Color.parseColor("#FFAB91"), + Color.parseColor("#FF8A65"), + Color.parseColor("#FF7043"), + Color.parseColor("#FF5722"), + Color.parseColor("#F4511E"), + Color.parseColor("#E64A19"), + Color.parseColor("#D84315"), + Color.parseColor("#BF360C") + }, + new int[] { + Color.parseColor("#EFEBE9"), + Color.parseColor("#D7CCC8"), + Color.parseColor("#BCAAA4"), + Color.parseColor("#A1887F"), + Color.parseColor("#8D6E63"), + Color.parseColor("#795548"), + Color.parseColor("#6D4C41"), + Color.parseColor("#5D4037"), + Color.parseColor("#4E342E"), + Color.parseColor("#3E2723") + }, + new int[] { + Color.parseColor("#FAFAFA"), + Color.parseColor("#F5F5F5"), + Color.parseColor("#EEEEEE"), + Color.parseColor("#E0E0E0"), + Color.parseColor("#BDBDBD"), + Color.parseColor("#9E9E9E"), + Color.parseColor("#757575"), + Color.parseColor("#616161"), + Color.parseColor("#424242"), + Color.parseColor("#212121") + }, + new int[] { + Color.parseColor("#ECEFF1"), + Color.parseColor("#CFD8DC"), + Color.parseColor("#B0BEC5"), + Color.parseColor("#90A4AE"), + Color.parseColor("#78909C"), + Color.parseColor("#607D8B"), + Color.parseColor("#546E7A"), + Color.parseColor("#455A64"), + Color.parseColor("#37474F"), + Color.parseColor("#263238") + } + }; + + static final int[] ACCENT_COLORS = + new int[] { + Color.parseColor("#FF1744"), + Color.parseColor("#F50057"), + Color.parseColor("#D500F9"), + Color.parseColor("#651FFF"), + Color.parseColor("#3D5AFE"), + Color.parseColor("#2979FF"), + Color.parseColor("#00B0FF"), + Color.parseColor("#00E5FF"), + Color.parseColor("#1DE9B6"), + Color.parseColor("#00E676"), + Color.parseColor("#76FF03"), + Color.parseColor("#C6FF00"), + Color.parseColor("#FFEA00"), + Color.parseColor("#FFC400"), + Color.parseColor("#FF9100"), + Color.parseColor("#FF3D00") + }; + + static final int[][] ACCENT_COLORS_SUB = + new int[][] { + new int[] { + Color.parseColor("#FF8A80"), + Color.parseColor("#FF5252"), + Color.parseColor("#FF1744"), + Color.parseColor("#D50000") + }, + new int[] { + Color.parseColor("#FF80AB"), + Color.parseColor("#FF4081"), + Color.parseColor("#F50057"), + Color.parseColor("#C51162") + }, + new int[] { + Color.parseColor("#EA80FC"), + Color.parseColor("#E040FB"), + Color.parseColor("#D500F9"), + Color.parseColor("#AA00FF") + }, + new int[] { + Color.parseColor("#B388FF"), + Color.parseColor("#7C4DFF"), + Color.parseColor("#651FFF"), + Color.parseColor("#6200EA") + }, + new int[] { + Color.parseColor("#8C9EFF"), + Color.parseColor("#536DFE"), + Color.parseColor("#3D5AFE"), + Color.parseColor("#304FFE") + }, + new int[] { + Color.parseColor("#82B1FF"), + Color.parseColor("#448AFF"), + Color.parseColor("#2979FF"), + Color.parseColor("#2962FF") + }, + new int[] { + Color.parseColor("#80D8FF"), + Color.parseColor("#40C4FF"), + Color.parseColor("#00B0FF"), + Color.parseColor("#0091EA") + }, + new int[] { + Color.parseColor("#84FFFF"), + Color.parseColor("#18FFFF"), + Color.parseColor("#00E5FF"), + Color.parseColor("#00B8D4") + }, + new int[] { + Color.parseColor("#A7FFEB"), + Color.parseColor("#64FFDA"), + Color.parseColor("#1DE9B6"), + Color.parseColor("#00BFA5") + }, + new int[] { + Color.parseColor("#B9F6CA"), + Color.parseColor("#69F0AE"), + Color.parseColor("#00E676"), + Color.parseColor("#00C853") + }, + new int[] { + Color.parseColor("#CCFF90"), + Color.parseColor("#B2FF59"), + Color.parseColor("#76FF03"), + Color.parseColor("#64DD17") + }, + new int[] { + Color.parseColor("#F4FF81"), + Color.parseColor("#EEFF41"), + Color.parseColor("#C6FF00"), + Color.parseColor("#AEEA00") + }, + new int[] { + Color.parseColor("#FFFF8D"), + Color.parseColor("#FFFF00"), + Color.parseColor("#FFEA00"), + Color.parseColor("#FFD600") + }, + new int[] { + Color.parseColor("#FFE57F"), + Color.parseColor("#FFD740"), + Color.parseColor("#FFC400"), + Color.parseColor("#FFAB00") + }, + new int[] { + Color.parseColor("#FFD180"), + Color.parseColor("#FFAB40"), + Color.parseColor("#FF9100"), + Color.parseColor("#FF6D00") + }, + new int[] { + Color.parseColor("#FF9E80"), + Color.parseColor("#FF6E40"), + Color.parseColor("#FF3D00"), + Color.parseColor("#DD2C00") + } + }; +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/FillGridView.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/FillGridView.java new file mode 100644 index 00000000..81007b70 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/color/FillGridView.java @@ -0,0 +1,27 @@ +package com.afollestad.materialdialogs.color; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** @author Aidan Follestad (afollestad) */ +public class FillGridView extends GridView { + + public FillGridView(Context context) { + super(context); + } + + public FillGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FillGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, expandSpec); + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FileChooserDialog.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FileChooserDialog.java new file mode 100644 index 00000000..00f4a617 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FileChooserDialog.java @@ -0,0 +1,372 @@ +package com.afollestad.materialdialogs.folderselector; + +import android.Manifest; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.R; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class FileChooserDialog extends DialogFragment implements MaterialDialog.ListCallback { + + private static final String DEFAULT_TAG = "[MD_FILE_SELECTOR]"; + + private File parentFolder; + private File[] parentContents; + private boolean canGoUp = true; + private FileCallback callback; + + public FileChooserDialog() { + } + + CharSequence[] getContentsArray() { + if (parentContents == null) { + if (canGoUp) { + return new String[]{getBuilder().goUpLabel}; + } + return new String[]{}; + } + String[] results = new String[parentContents.length + (canGoUp ? 1 : 0)]; + if (canGoUp) { + results[0] = getBuilder().goUpLabel; + } + for (int i = 0; i < parentContents.length; i++) { + results[canGoUp ? i + 1 : i] = parentContents[i].getName(); + } + return results; + } + + File[] listFiles(@Nullable String mimeType, @Nullable String[] extensions) { + File[] contents = parentFolder.listFiles(); + List results = new ArrayList<>(); + if (contents != null) { + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + for (File fi : contents) { + if (fi.isDirectory()) { + results.add(fi); + } else { + if (extensions != null) { + boolean found = false; + for (String ext : extensions) { + if (fi.getName().toLowerCase().endsWith(ext.toLowerCase())) { + found = true; + break; + } + } + if (found) { + results.add(fi); + } + } else if (mimeType != null) { + if (fileIsMimeType(fi, mimeType, mimeTypeMap)) { + results.add(fi); + } + } + } + } + Collections.sort(results, new FileSorter()); + return results.toArray(new File[results.size()]); + } + return null; + } + + boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) { + if (mimeType == null || mimeType.equals("*/*")) { + return true; + } else { + // get the file mime type + String filename = file.toURI().toString(); + int dotPos = filename.lastIndexOf('.'); + if (dotPos == -1) { + return false; + } + String fileExtension = filename.substring(dotPos + 1); + if (fileExtension.endsWith("json")) { + return mimeType.startsWith("application/json"); + } + String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension); + if (fileType == null) { + return false; + } + // check the 'type/subtype' pattern + if (fileType.equals(mimeType)) { + return true; + } + // check the 'type/*' pattern + int mimeTypeDelimiter = mimeType.lastIndexOf('/'); + if (mimeTypeDelimiter == -1) { + return false; + } + String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter); + String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1); + if (!mimeTypeSubtype.equals("*")) { + return false; + } + int fileTypeDelimiter = fileType.lastIndexOf('/'); + if (fileTypeDelimiter == -1) { + return false; + } + String fileTypeMainType = fileType.substring(0, fileTypeDelimiter); + if (fileTypeMainType.equals(mimeTypeMainType)) { + return true; + } + } + return false; + } + + @SuppressWarnings("ConstantConditions") + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + return new MaterialDialog.Builder(getActivity()) + .title(R.string.md_error_label) + .content(R.string.md_storage_perm_error) + .positiveText(android.R.string.ok) + .build(); + } + + if (getArguments() == null || !getArguments().containsKey("builder")) { + throw new IllegalStateException("You must create a FileChooserDialog using the Builder."); + } + if (!getArguments().containsKey("current_path")) { + getArguments().putString("current_path", getBuilder().initialPath); + } + parentFolder = new File(getArguments().getString("current_path")); + checkIfCanGoUp(); + parentContents = listFiles(getBuilder().mimeType, getBuilder().extensions); + return new MaterialDialog.Builder(getActivity()) + .title(parentFolder.getAbsolutePath()) + .typeface(getBuilder().mediumFont, getBuilder().regularFont) + .items(getContentsArray()) + .itemsCallback(this) + .onNegative( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.dismiss(); + } + }) + .autoDismiss(false) + .negativeText(getBuilder().cancelButton) + .build(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (callback != null) { + callback.onFileChooserDismissed(this); + } + } + + @Override + public void onSelection(MaterialDialog materialDialog, View view, int i, CharSequence s) { + if (canGoUp && i == 0) { + parentFolder = parentFolder.getParentFile(); + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = parentFolder.getParentFile(); + } + canGoUp = parentFolder.getParent() != null; + } else { + parentFolder = parentContents[canGoUp ? i - 1 : i]; + canGoUp = true; + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = Environment.getExternalStorageDirectory(); + } + } + if (parentFolder.isFile()) { + callback.onFileSelection(this, parentFolder); + dismiss(); + } else { + parentContents = listFiles(getBuilder().mimeType, getBuilder().extensions); + MaterialDialog dialog = (MaterialDialog) getDialog(); + dialog.setTitle(parentFolder.getAbsolutePath()); + getArguments().putString("current_path", parentFolder.getAbsolutePath()); + dialog.setItems(getContentsArray()); + } + } + + private void checkIfCanGoUp() { + try { + canGoUp = parentFolder.getPath().split("/").length > 1; + } catch (IndexOutOfBoundsException e) { + canGoUp = false; + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getActivity() instanceof FileCallback) { + callback = (FileCallback) getActivity(); + } else if (getParentFragment() instanceof FileCallback) { + callback = (FileCallback) getParentFragment(); + } else { + throw new IllegalStateException( + "FileChooserDialog needs to be shown from an Activity/Fragment implementing FileCallback."); + } + } + + public void show(FragmentManager fragmentManager) { + final String tag = getBuilder().tag; + Fragment frag = fragmentManager.findFragmentByTag(tag); + if (frag != null) { + ((DialogFragment) frag).dismiss(); + fragmentManager.beginTransaction().remove(frag).commit(); + } + show(fragmentManager, tag); + } + + public void show(FragmentActivity fragmentActivity) { + show(fragmentActivity.getSupportFragmentManager()); + } + + @NonNull + public String getInitialPath() { + return getBuilder().initialPath; + } + + @SuppressWarnings("ConstantConditions") + @NonNull + private Builder getBuilder() { + return (Builder) getArguments().getSerializable("builder"); + } + + public interface FileCallback { + + void onFileSelection(@NonNull FileChooserDialog dialog, @NonNull File file); + + void onFileChooserDismissed(@NonNull FileChooserDialog dialog); + } + + public static class Builder implements Serializable { + + @NonNull + final transient Context context; + @StringRes + int cancelButton; + String initialPath; + String mimeType; + String[] extensions; + String tag; + String goUpLabel; + @Nullable + String mediumFont; + @Nullable + String regularFont; + + public Builder(@NonNull Context context) { + this.context = context; + cancelButton = android.R.string.cancel; + initialPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + mimeType = null; + goUpLabel = "..."; + } + + @NonNull + public Builder typeface(@Nullable String medium, @Nullable String regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + @NonNull + public Builder cancelButton(@StringRes int text) { + cancelButton = text; + return this; + } + + @NonNull + public Builder initialPath(@Nullable String initialPath) { + if (initialPath == null) { + initialPath = File.separator; + } + this.initialPath = initialPath; + return this; + } + + @NonNull + public Builder mimeType(@Nullable String type) { + mimeType = type; + return this; + } + + @NonNull + public Builder extensionsFilter(@Nullable String... extensions) { + this.extensions = extensions; + return this; + } + + @NonNull + public Builder tag(@Nullable String tag) { + if (tag == null) { + tag = DEFAULT_TAG; + } + this.tag = tag; + return this; + } + + @NonNull + public Builder goUpLabel(String text) { + goUpLabel = text; + return this; + } + + @NonNull + public FileChooserDialog build() { + FileChooserDialog dialog = new FileChooserDialog(); + Bundle args = new Bundle(); + args.putSerializable("builder", this); + dialog.setArguments(args); + return dialog; + } + + @NonNull + public FileChooserDialog show(FragmentManager fragmentManager) { + FileChooserDialog dialog = build(); + dialog.show(fragmentManager); + return dialog; + } + + @NonNull + public FileChooserDialog show(FragmentActivity fragmentActivity) { + return show(fragmentActivity.getSupportFragmentManager()); + } + } + + private static class FileSorter implements Comparator { + + @Override + public int compare(File lhs, File rhs) { + if (lhs.isDirectory() && !rhs.isDirectory()) { + return -1; + } else if (!lhs.isDirectory() && rhs.isDirectory()) { + return 1; + } else { + return lhs.getName().compareTo(rhs.getName()); + } + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FolderChooserDialog.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FolderChooserDialog.java new file mode 100644 index 00000000..fe9f7f77 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/folderselector/FolderChooserDialog.java @@ -0,0 +1,354 @@ +package com.afollestad.materialdialogs.folderselector; + +import android.Manifest; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.R; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** @author Aidan Follestad (afollestad) */ +public class FolderChooserDialog extends DialogFragment implements MaterialDialog.ListCallback { + + private static final String DEFAULT_TAG = "[MD_FOLDER_SELECTOR]"; + + private File parentFolder; + private File[] parentContents; + private boolean canGoUp = false; + private FolderCallback callback; + + public FolderChooserDialog() { + } + + String[] getContentsArray() { + if (parentContents == null) { + if (canGoUp) { + return new String[]{getBuilder().goUpLabel}; + } + return new String[]{}; + } + String[] results = new String[parentContents.length + (canGoUp ? 1 : 0)]; + if (canGoUp) { + results[0] = getBuilder().goUpLabel; + } + for (int i = 0; i < parentContents.length; i++) { + results[canGoUp ? i + 1 : i] = parentContents[i].getName(); + } + return results; + } + + File[] listFiles() { + File[] contents = parentFolder.listFiles(); + List results = new ArrayList<>(); + if (contents != null) { + for (File fi : contents) { + if (fi.isDirectory()) { + results.add(fi); + } + } + Collections.sort(results, new FolderSorter()); + return results.toArray(new File[results.size()]); + } + return null; + } + + @SuppressWarnings("ConstantConditions") + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + return new MaterialDialog.Builder(getActivity()) + .title(R.string.md_error_label) + .content(R.string.md_storage_perm_error) + .positiveText(android.R.string.ok) + .build(); + } + if (getArguments() == null || !getArguments().containsKey("builder")) { + throw new IllegalStateException("You must create a FolderChooserDialog using the Builder."); + } + if (!getArguments().containsKey("current_path")) { + getArguments().putString("current_path", getBuilder().initialPath); + } + parentFolder = new File(getArguments().getString("current_path")); + checkIfCanGoUp(); + parentContents = listFiles(); + MaterialDialog.Builder builder = + new MaterialDialog.Builder(getActivity()) + .typeface(getBuilder().mediumFont, getBuilder().regularFont) + .title(parentFolder.getAbsolutePath()) + .items((CharSequence[]) getContentsArray()) + .itemsCallback(this) + .onPositive( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.dismiss(); + callback.onFolderSelection(FolderChooserDialog.this, parentFolder); + } + }) + .onNegative( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.dismiss(); + } + }) + .autoDismiss(false) + .positiveText(getBuilder().chooseButton) + .negativeText(getBuilder().cancelButton); + if (getBuilder().allowNewFolder) { + builder.neutralText(getBuilder().newFolderButton); + builder.onNeutral( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + createNewFolder(); + } + }); + } + if ("/".equals(getBuilder().initialPath)) { + canGoUp = false; + } + return builder.build(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (callback != null) { + callback.onFolderChooserDismissed(this); + } + } + + private void createNewFolder() { + new MaterialDialog.Builder(getActivity()) + .title(getBuilder().newFolderButton) + .input( + 0, + 0, + false, + new MaterialDialog.InputCallback() { + @Override + public void onInput(@NonNull MaterialDialog dialog, CharSequence input) { + // noinspection ResultOfMethodCallIgnored + final File newFi = new File(parentFolder, input.toString()); + if (!newFi.mkdir()) { + String msg = + "Unable to create folder " + + newFi.getAbsolutePath() + + ", make sure you have the WRITE_EXTERNAL_STORAGE permission or root permissions."; + Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); + } else { + reload(); + } + } + }) + .show(); + } + + @Override + public void onSelection(MaterialDialog materialDialog, View view, int i, CharSequence s) { + if (canGoUp && i == 0) { + parentFolder = parentFolder.getParentFile(); + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = parentFolder.getParentFile(); + } + canGoUp = parentFolder.getParent() != null; + } else { + parentFolder = parentContents[canGoUp ? i - 1 : i]; + canGoUp = true; + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = Environment.getExternalStorageDirectory(); + } + } + reload(); + } + + private void checkIfCanGoUp() { + try { + canGoUp = parentFolder.getPath().split("/").length > 1; + } catch (IndexOutOfBoundsException e) { + canGoUp = false; + } + } + + private void reload() { + parentContents = listFiles(); + MaterialDialog dialog = (MaterialDialog) getDialog(); + dialog.setTitle(parentFolder.getAbsolutePath()); + getArguments().putString("current_path", parentFolder.getAbsolutePath()); + dialog.setItems((CharSequence[]) getContentsArray()); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getActivity() instanceof FolderCallback) { + callback = (FolderCallback) getActivity(); + } else if (getParentFragment() instanceof FolderCallback) { + callback = (FolderCallback) getParentFragment(); + } else { + throw new IllegalStateException( + "FolderChooserDialog needs to be shown from an Activity/Fragment implementing FolderCallback."); + } + } + + public void show(FragmentActivity fragmentActivity) { + show(fragmentActivity.getSupportFragmentManager()); + } + + public void show(FragmentManager fragmentManager) { + final String tag = getBuilder().tag; + Fragment frag = fragmentManager.findFragmentByTag(tag); + if (frag != null) { + ((DialogFragment) frag).dismiss(); + fragmentManager.beginTransaction().remove(frag).commit(); + } + show(fragmentManager, tag); + } + + @SuppressWarnings("ConstantConditions") + @NonNull + private Builder getBuilder() { + return (Builder) getArguments().getSerializable("builder"); + } + + public interface FolderCallback { + + void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder); + + void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog); + } + + public static class Builder implements Serializable { + + @NonNull + final transient Context context; + @StringRes + int chooseButton; + @StringRes + int cancelButton; + String initialPath; + String tag; + boolean allowNewFolder; + @StringRes + int newFolderButton; + String goUpLabel; + @Nullable + String mediumFont; + @Nullable + String regularFont; + + public Builder(@NonNull Context context) { + this.context = context; + chooseButton = R.string.md_choose_label; + cancelButton = android.R.string.cancel; + goUpLabel = "..."; + initialPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + } + + @NonNull + public Builder typeface(@Nullable String medium, @Nullable String regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + @NonNull + public Builder chooseButton(@StringRes int text) { + chooseButton = text; + return this; + } + + @NonNull + public Builder cancelButton(@StringRes int text) { + cancelButton = text; + return this; + } + + @NonNull + public Builder goUpLabel(String text) { + goUpLabel = text; + return this; + } + + @NonNull + public Builder allowNewFolder(boolean allow, @StringRes int buttonLabel) { + allowNewFolder = allow; + if (buttonLabel == 0) { + buttonLabel = R.string.new_folder; + } + newFolderButton = buttonLabel; + return this; + } + + @NonNull + public Builder initialPath(@Nullable String initialPath) { + if (initialPath == null) { + initialPath = File.separator; + } + this.initialPath = initialPath; + return this; + } + + @NonNull + public Builder tag(@Nullable String tag) { + if (tag == null) { + tag = DEFAULT_TAG; + } + this.tag = tag; + return this; + } + + @NonNull + public FolderChooserDialog build() { + FolderChooserDialog dialog = new FolderChooserDialog(); + Bundle args = new Bundle(); + args.putSerializable("builder", this); + dialog.setArguments(args); + return dialog; + } + + @NonNull + public FolderChooserDialog show(FragmentManager fragmentManager) { + FolderChooserDialog dialog = build(); + dialog.show(fragmentManager); + return dialog; + } + + @NonNull + public FolderChooserDialog show(FragmentActivity fragmentActivity) { + return show(fragmentActivity.getSupportFragmentManager()); + } + } + + private static class FolderSorter implements Comparator { + + @Override + public int compare(File lhs, File rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/AllCapsTransformationMethod.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/AllCapsTransformationMethod.java new file mode 100644 index 00000000..5609bbd8 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/AllCapsTransformationMethod.java @@ -0,0 +1,29 @@ +package com.afollestad.materialdialogs.internal; + +import android.content.Context; +import android.graphics.Rect; +import android.text.method.TransformationMethod; +import android.view.View; +import java.util.Locale; + +class AllCapsTransformationMethod implements TransformationMethod { + + private Locale mLocale; + + AllCapsTransformationMethod(Context context) { + mLocale = context.getResources().getConfiguration().locale; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + return source != null ? source.toString().toUpperCase(mLocale) : null; + } + + @Override + public void onFocusChanged( + View view, + CharSequence sourceText, + boolean focused, + int direction, + Rect previouslyFocusedRect) {} +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDAdapter.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDAdapter.java new file mode 100644 index 00000000..05f0dd4c --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDAdapter.java @@ -0,0 +1,9 @@ +package com.afollestad.materialdialogs.internal; + +import com.afollestad.materialdialogs.MaterialDialog; + +/** @author Aidan Follestad (afollestad) */ +public interface MDAdapter { + + void setDialog(MaterialDialog dialog); +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDButton.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDButton.java new file mode 100644 index 00000000..97210b1d --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDButton.java @@ -0,0 +1,83 @@ +package com.afollestad.materialdialogs.internal; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.widget.TextView; +import com.afollestad.materialdialogs.GravityEnum; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.util.DialogUtils; + +/** @author Kevin Barry (teslacoil) 4/02/2015 */ +@SuppressLint("AppCompatCustomView") +public class MDButton extends TextView { + + private boolean stacked = false; + private GravityEnum stackedGravity; + + private int stackedEndPadding; + private Drawable stackedBackground; + private Drawable defaultBackground; + + public MDButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public MDButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + stackedEndPadding = + context.getResources().getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + stackedGravity = GravityEnum.END; + } + + /** + * Set if the button should be displayed in stacked mode. This should only be called from + * MDRootLayout's onMeasure, and we must be measured after calling this. + */ + /* package */ void setStacked(boolean stacked, boolean force) { + if (this.stacked != stacked || force) { + + setGravity( + stacked ? (Gravity.CENTER_VERTICAL | stackedGravity.getGravityInt()) : Gravity.CENTER); + // noinspection ResourceType + setTextAlignment(stacked ? stackedGravity.getTextAlignment() : TEXT_ALIGNMENT_CENTER); + + DialogUtils.setBackgroundCompat(this, stacked ? stackedBackground : defaultBackground); + if (stacked) { + setPadding(stackedEndPadding, getPaddingTop(), stackedEndPadding, getPaddingBottom()); + } /* Else the padding was properly reset by the drawable */ + + this.stacked = stacked; + } + } + + public void setStackedGravity(GravityEnum gravity) { + stackedGravity = gravity; + } + + public void setStackedSelector(Drawable d) { + stackedBackground = d; + if (stacked) { + setStacked(true, true); + } + } + + public void setDefaultSelector(Drawable d) { + defaultBackground = d; + if (!stacked) { + setStacked(false, true); + } + } + + public void setAllCapsCompat(boolean allCaps) { + setAllCaps(allCaps); + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDRootLayout.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDRootLayout.java new file mode 100644 index 00000000..039da5c1 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDRootLayout.java @@ -0,0 +1,640 @@ +package com.afollestad.materialdialogs.internal; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.webkit.WebView; +import android.widget.AdapterView; +import android.widget.ScrollView; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import com.afollestad.materialdialogs.GravityEnum; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.StackingBehavior; +import com.afollestad.materialdialogs.util.DialogUtils; + +/** + * @author Kevin Barry (teslacoil) 4/02/2015 This is the top level view for all MaterialDialogs It + * handles the layout of: titleFrame (md_stub_titleframe) content (text, custom view, listview, + * etc) buttonDefault... (either stacked or horizontal) + */ +public class MDRootLayout extends ViewGroup { + + private static final int INDEX_NEUTRAL = 0; + private static final int INDEX_NEGATIVE = 1; + private static final int INDEX_POSITIVE = 2; + private final MDButton[] buttons = new MDButton[3]; + private int maxHeight; + private View titleBar; + private View content; + private boolean drawTopDivider = false; + private boolean drawBottomDivider = false; + private StackingBehavior stackBehavior = StackingBehavior.ADAPTIVE; + private boolean isStacked = false; + private boolean useFullPadding = true; + private boolean reducePaddingNoTitleNoButtons; + private boolean noTitleNoPadding; + + private int noTitlePaddingFull; + private int buttonPaddingFull; + private int buttonBarHeight; + + private GravityEnum buttonGravity = GravityEnum.START; + + /* Margin from dialog frame to first button */ + private int buttonHorizontalEdgeMargin; + + private Paint dividerPaint; + + private ViewTreeObserver.OnScrollChangedListener topOnScrollChangedListener; + private ViewTreeObserver.OnScrollChangedListener bottomOnScrollChangedListener; + private int dividerWidth; + + public MDRootLayout(Context context) { + super(context); + init(context, null, 0); + } + + public MDRootLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private static boolean isVisible(View v) { + boolean visible = v != null && v.getVisibility() != View.GONE; + if (visible && v instanceof MDButton) { + visible = ((MDButton) v).getText().toString().trim().length() > 0; + } + return visible; + } + + public static boolean canRecyclerViewScroll(RecyclerView view) { + return view != null + && view.getLayoutManager() != null + && view.getLayoutManager().canScrollVertically(); + } + + private static boolean canScrollViewScroll(ScrollView sv) { + if (sv.getChildCount() == 0) { + return false; + } + final int childHeight = sv.getChildAt(0).getMeasuredHeight(); + return sv.getMeasuredHeight() - sv.getPaddingTop() - sv.getPaddingBottom() < childHeight; + } + + private static boolean canWebViewScroll(WebView view) { + // noinspection deprecation + return view.getMeasuredHeight() < view.getContentHeight() * view.getScale(); + } + + private static boolean canAdapterViewScroll(AdapterView lv) { + /* Force it to layout it's children */ + if (lv.getLastVisiblePosition() == -1) { + return false; + } + + /* We can scroll if the first or last item is not visible */ + boolean firstItemVisible = lv.getFirstVisiblePosition() == 0; + boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1; + + if (firstItemVisible && lastItemVisible && lv.getChildCount() > 0) { + /* Or the first item's top is above or own top */ + if (lv.getChildAt(0).getTop() < lv.getPaddingTop()) { + return true; + } + /* or the last item's bottom is beyond our own bottom */ + return lv.getChildAt(lv.getChildCount() - 1).getBottom() + > lv.getHeight() - lv.getPaddingBottom(); + } + + return true; + } + + /** + * Find the view touching the bottom of this ViewGroup. Non visible children are ignored, however + * getChildDrawingOrder is not taking into account for simplicity and because it behaves + * inconsistently across platform versions. + * + * @return View touching the bottom of this ViewGroup or null + */ + @Nullable + private static View getBottomView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View bottomView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE + && child.getBottom() == viewGroup.getMeasuredHeight()) { + bottomView = child; + break; + } + } + return bottomView; + } + + @Nullable + private static View getTopView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View topView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child.getTop() == 0) { + topView = child; + break; + } + } + return topView; + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + Resources r = context.getResources(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MDRootLayout, defStyleAttr, 0); + reducePaddingNoTitleNoButtons = + a.getBoolean(R.styleable.MDRootLayout_md_reduce_padding_no_title_no_buttons, true); + a.recycle(); + + noTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding); + buttonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding); + + buttonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side); + buttonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height); + + dividerPaint = new Paint(); + dividerWidth = r.getDimensionPixelSize(R.dimen.md_divider_height); + dividerPaint.setColor(DialogUtils.resolveColor(context, R.attr.md_divider_color)); + setWillNotDraw(false); + } + + public void setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + } + + public void noTitleNoPadding() { + noTitleNoPadding = true; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + if (v.getId() == R.id.md_titleFrame) { + titleBar = v; + } else if (v.getId() == R.id.md_buttonDefaultNeutral) { + buttons[INDEX_NEUTRAL] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultNegative) { + buttons[INDEX_NEGATIVE] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultPositive) { + buttons[INDEX_POSITIVE] = (MDButton) v; + } else { + content = v; + } + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (height > maxHeight) { + height = maxHeight; + } + + useFullPadding = true; + boolean hasButtons = false; + + final boolean stacked; + if (stackBehavior == StackingBehavior.ALWAYS) { + stacked = true; + } else if (stackBehavior == StackingBehavior.NEVER) { + stacked = false; + } else { + int buttonsWidth = 0; + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(false, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + buttonsWidth += button.getMeasuredWidth(); + hasButtons = true; + } + } + + int buttonBarPadding = + getContext().getResources().getDimensionPixelSize(R.dimen.md_neutral_button_margin); + final int buttonFrameWidth = width - 2 * buttonBarPadding; + stacked = buttonsWidth > buttonFrameWidth; + } + + int stackedHeight = 0; + isStacked = stacked; + if (stacked) { + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(true, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + stackedHeight += button.getMeasuredHeight(); + hasButtons = true; + } + } + } + + int availableHeight = height; + int fullPadding = 0; + int minPadding = 0; + if (hasButtons) { + if (isStacked) { + availableHeight -= stackedHeight; + fullPadding += 2 * buttonPaddingFull; + minPadding += 2 * buttonPaddingFull; + } else { + availableHeight -= buttonBarHeight; + fullPadding += 2 * buttonPaddingFull; + /* No minPadding */ + } + } else { + /* Content has 8dp, we add 16dp and get 24dp, the frame margin */ + fullPadding += 2 * buttonPaddingFull; + } + + if (isVisible(titleBar)) { + titleBar.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED); + availableHeight -= titleBar.getMeasuredHeight(); + } else if (!noTitleNoPadding) { + fullPadding += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST)); + + if (content.getMeasuredHeight() <= availableHeight - fullPadding) { + if (!reducePaddingNoTitleNoButtons || isVisible(titleBar) || hasButtons) { + useFullPadding = true; + availableHeight -= content.getMeasuredHeight() + fullPadding; + } else { + useFullPadding = false; + availableHeight -= content.getMeasuredHeight() + minPadding; + } + } else { + useFullPadding = false; + availableHeight = 0; + } + } + + setMeasuredDimension(width, height - availableHeight); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (content != null) { + if (drawTopDivider) { + int y = content.getTop(); + canvas.drawRect(0, y - dividerWidth, getMeasuredWidth(), y, dividerPaint); + } + + if (drawBottomDivider) { + int y = content.getBottom(); + canvas.drawRect(0, y, getMeasuredWidth(), y + dividerWidth, dividerPaint); + } + } + } + + @Override + protected void onLayout(boolean changed, final int l, int t, final int r, int b) { + if (isVisible(titleBar)) { + int height = titleBar.getMeasuredHeight(); + titleBar.layout(l, t, r, t + height); + t += height; + } else if (!noTitleNoPadding && useFullPadding) { + t += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.layout(l, t, r, t + content.getMeasuredHeight()); + } + + if (isStacked) { + b -= buttonPaddingFull; + for (MDButton mButton : buttons) { + if (isVisible(mButton)) { + mButton.layout(l, b - mButton.getMeasuredHeight(), r, b); + b -= mButton.getMeasuredHeight(); + } + } + } else { + int barTop; + int barBottom = b; + if (useFullPadding) { + barBottom -= buttonPaddingFull; + } + barTop = barBottom - buttonBarHeight; + /* START: + Neutral Negative Positive + + CENTER: + Negative Neutral Positive + + END: + Positive Negative Neutral + + (With no Positive, Negative takes it's place except for CENTER) + */ + int offset = buttonHorizontalEdgeMargin; + + /* Used with CENTER gravity */ + int neutralLeft = -1; + int neutralRight = -1; + + if (isVisible(buttons[INDEX_POSITIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_POSITIVE].getMeasuredWidth(); + } else { + /* START || CENTER */ + br = r - offset; + bl = br - buttons[INDEX_POSITIVE].getMeasuredWidth(); + neutralRight = bl; + } + buttons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom); + offset += buttons[INDEX_POSITIVE].getMeasuredWidth(); + } + + if (isVisible(buttons[INDEX_NEGATIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + br = r - offset; + bl = br - buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else { + /* CENTER */ + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + neutralLeft = br; + } + buttons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom); + } + + if (isVisible(buttons[INDEX_NEUTRAL])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + br = r - buttonHorizontalEdgeMargin; + bl = br - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else { + /* CENTER */ + if (neutralLeft == -1 && neutralRight != -1) { + neutralLeft = neutralRight - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1 && neutralLeft != -1) { + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1) { + neutralLeft = (r - l) / 2 - buttons[INDEX_NEUTRAL].getMeasuredWidth() / 2; + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } + bl = neutralLeft; + br = neutralRight; + } + + buttons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom); + } + } + + setUpDividersVisibility(content, true, true); + } + + public void setStackingBehavior(StackingBehavior behavior) { + stackBehavior = behavior; + invalidate(); + } + + public void setDividerColor(int color) { + dividerPaint.setColor(color); + invalidate(); + } + + public void setButtonGravity(GravityEnum gravity) { + buttonGravity = gravity; + invertGravityIfNecessary(); + } + + private void invertGravityIfNecessary() { + Configuration config = getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + switch (buttonGravity) { + case START: + buttonGravity = GravityEnum.END; + break; + case END: + buttonGravity = GravityEnum.START; + break; + } + } + } + + public void setButtonStackedGravity(GravityEnum gravity) { + for (MDButton mButton : buttons) { + if (mButton != null) { + mButton.setStackedGravity(gravity); + } + } + } + + private void setUpDividersVisibility( + final View view, final boolean setForTop, final boolean setForBottom) { + if (view == null) { + return; + } + if (view instanceof ScrollView) { + final ScrollView sv = (ScrollView) view; + if (canScrollViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof AdapterView) { + final AdapterView sv = (AdapterView) view; + if (canAdapterViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof WebView) { + view.getViewTreeObserver() + .addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (view.getMeasuredHeight() != 0) { + if (!canWebViewScroll((WebView) view)) { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } else { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + view.getViewTreeObserver().removeOnPreDrawListener(this); + } + return true; + } + }); + } else if (view instanceof RecyclerView) { + boolean canScroll = canRecyclerViewScroll((RecyclerView) view); + if (setForTop) { + drawTopDivider = canScroll; + } + if (setForBottom) { + drawBottomDivider = canScroll; + } + if (canScroll) { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + } else if (view instanceof ViewGroup) { + View topView = getTopView((ViewGroup) view); + setUpDividersVisibility(topView, setForTop, setForBottom); + View bottomView = getBottomView((ViewGroup) view); + if (bottomView != topView) { + setUpDividersVisibility(bottomView, false, true); + } + } + } + + private void addScrollListener( + final ViewGroup vg, final boolean setForTop, final boolean setForBottom) { + if ((!setForBottom && topOnScrollChangedListener == null + || (setForBottom && bottomOnScrollChangedListener == null))) { + if (vg instanceof RecyclerView) { + RecyclerView.OnScrollListener scrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + invalidate(); + } + }; + ((RecyclerView) vg).addOnScrollListener(scrollListener); + scrollListener.onScrolled((RecyclerView) vg, 0, 0); + } else { + ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = + new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + if (vg instanceof WebView) { + invalidateDividersForWebView((WebView) vg, setForTop, setForBottom, hasButtons); + } else { + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + } + invalidate(); + } + }; + if (!setForBottom) { + topOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(topOnScrollChangedListener); + } else { + bottomOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(bottomOnScrollChangedListener); + } + onScrollChangedListener.onScrollChanged(); + } + } + } + + private void invalidateDividersForScrollingView( + ViewGroup view, final boolean setForTop, boolean setForBottom, boolean hasButtons) { + if (setForTop && view.getChildCount() > 0) { + drawTopDivider = + titleBar != null + && titleBar.getVisibility() != View.GONE + && + // Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > view.getChildAt(0).getTop(); + } + if (setForBottom && view.getChildCount() > 0) { + drawBottomDivider = + hasButtons + && view.getScrollY() + view.getHeight() - view.getPaddingBottom() + < view.getChildAt(view.getChildCount() - 1).getBottom(); + } + } + + private void invalidateDividersForWebView( + WebView view, final boolean setForTop, boolean setForBottom, boolean hasButtons) { + if (setForTop) { + drawTopDivider = + titleBar != null + && titleBar.getVisibility() != View.GONE + && + // Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > 0; + } + if (setForBottom) { + // noinspection deprecation + drawBottomDivider = + hasButtons + && view.getScrollY() + view.getMeasuredHeight() - view.getPaddingBottom() + < view.getContentHeight() * view.getScale(); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDTintHelper.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDTintHelper.java new file mode 100644 index 00000000..bef430b2 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/MDTintHelper.java @@ -0,0 +1,147 @@ +package com.afollestad.materialdialogs.internal; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.util.DialogUtils; + +import java.lang.reflect.Field; + +/** Tints widgets */ +@SuppressLint("PrivateResource") +public class MDTintHelper { + + public static void setTint(@NonNull RadioButton radioButton, @NonNull ColorStateList colors) { + radioButton.setButtonTintList(colors); + } + + public static void setTint(@NonNull RadioButton radioButton, @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(radioButton.getContext()); + ColorStateList sl = + new ColorStateList( + new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, + new int[]{ + DialogUtils.resolveColor(radioButton.getContext(), android.R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(radioButton, sl); + } + + public static void setTint(@NonNull CheckBox box, @NonNull ColorStateList colors) { + box.setButtonTintList(colors); + } + + public static void setTint(@NonNull CheckBox box, @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(box.getContext()); + ColorStateList sl = + new ColorStateList( + new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, + new int[]{ + DialogUtils.resolveColor(box.getContext(), android.R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(box, sl); + } + + public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color) { + ColorStateList s1 = ColorStateList.valueOf(color); + seekBar.setThumbTintList(s1); + seekBar.setProgressTintList(s1); + } + + public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color) { + setTint(progressBar, color, false); + } + + private static void setTint( + @NonNull ProgressBar progressBar, @ColorInt int color, boolean skipIndeterminate) { + ColorStateList sl = ColorStateList.valueOf(color); + progressBar.setProgressTintList(sl); + progressBar.setSecondaryProgressTintList(sl); + if (!skipIndeterminate) { + progressBar.setIndeterminateTintList(sl); + } + } + + private static ColorStateList createEditTextColorStateList(@NonNull Context context, @ColorInt int color) { + int[][] states = new int[3][]; + int[] colors = new int[3]; + int i = 0; + states[i] = new int[]{-android.R.attr.state_enabled}; + colors[i] = DialogUtils.resolveColor(context, android.R.attr.colorControlNormal); + i++; + states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused}; + colors[i] = DialogUtils.resolveColor(context, android.R.attr.colorControlNormal); + i++; + states[i] = new int[]{}; + colors[i] = color; + return new ColorStateList(states, colors); + } + + public static void setTint(@NonNull EditText editText, @ColorInt int color) { + ColorStateList editTextColorStateList = + createEditTextColorStateList(editText.getContext(), color); + if (editText instanceof AppCompatEditText) { + // noinspection RestrictedApi + ((AppCompatEditText) editText).setSupportBackgroundTintList(editTextColorStateList); + } else { + editText.setBackgroundTintList(editTextColorStateList); + } + setCursorTint(editText, color); + } + + private static void setCursorTint(@NonNull EditText editText, @ColorInt int color) { + try { + Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); + fCursorDrawableRes.setAccessible(true); + int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); + Field fEditor = TextView.class.getDeclaredField("mEditor"); + fEditor.setAccessible(true); + Object editor = fEditor.get(editText); + Class clazz = editor.getClass(); + Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); + fCursorDrawable.setAccessible(true); + Drawable[] drawables = new Drawable[2]; + drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); + fCursorDrawable.set(editor, drawables); + } catch (NoSuchFieldException e1) { + Log.d("MDTintHelper", "Device issue with cursor tinting: " + e1.getMessage()); + e1.printStackTrace(); + } catch (Exception e2) { + e2.printStackTrace(); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/ThemeSingleton.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/ThemeSingleton.java new file mode 100644 index 00000000..9e43d81d --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/internal/ThemeSingleton.java @@ -0,0 +1,60 @@ +package com.afollestad.materialdialogs.internal; + +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import com.afollestad.materialdialogs.GravityEnum; + +/** + * Use of this is discouraged for now; for internal use only. See the Global Theming section of the + * README. + */ +public class ThemeSingleton { + + private static ThemeSingleton singleton; + public boolean darkTheme = false; + @ColorInt + public int titleColor = 0; + @ColorInt + public int contentColor = 0; + public ColorStateList positiveColor = null; + public ColorStateList neutralColor = null; + public ColorStateList negativeColor = null; + @ColorInt + public int widgetColor = 0; + @ColorInt + public int itemColor = 0; + public Drawable icon = null; + @ColorInt + public int backgroundColor = 0; + @ColorInt + public int dividerColor = 0; + public ColorStateList linkColor = null; + @DrawableRes + public int listSelector = 0; + @DrawableRes + public int btnSelectorStacked = 0; + @DrawableRes + public int btnSelectorPositive = 0; + @DrawableRes + public int btnSelectorNeutral = 0; + @DrawableRes + public int btnSelectorNegative = 0; + public GravityEnum titleGravity = GravityEnum.START; + public GravityEnum contentGravity = GravityEnum.START; + public GravityEnum btnStackedGravity = GravityEnum.END; + public GravityEnum itemsGravity = GravityEnum.START; + public GravityEnum buttonsGravity = GravityEnum.START; + + public static ThemeSingleton get(boolean createIfNull) { + if (singleton == null && createIfNull) { + singleton = new ThemeSingleton(); + } + return singleton; + } + + public static ThemeSingleton get() { + return get(true); + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialDialogPreference.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialDialogPreference.java new file mode 100644 index 00000000..82275cf0 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialDialogPreference.java @@ -0,0 +1,181 @@ +package com.afollestad.materialdialogs.prefs; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.NonNull; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +/** @author Aidan Follestad (afollestad) */ +public class MaterialDialogPreference extends DialogPreference { + + private Context context; + private MaterialDialog dialog; + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialDialogPreference(Context context) { + super(context); + init(context, null); + } + + public MaterialDialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public MaterialDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialDialogPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + this.context = context; + PrefUtil.setLayoutResource(context, this, attrs); + } + + @Override + public Dialog getDialog() { + return dialog; + } + + @Override + protected void showDialog(Bundle state) { + MaterialDialog.Builder builder = + new MaterialDialog.Builder(context) + .title(getDialogTitle()) + .icon(getDialogIcon()) + .dismissListener(this) + .onAny( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + switch (which) { + default: + MaterialDialogPreference.this.onClick( + dialog, DialogInterface.BUTTON_POSITIVE); + break; + case NEUTRAL: + MaterialDialogPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEUTRAL); + break; + case NEGATIVE: + MaterialDialogPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEGATIVE); + break; + } + } + }) + .positiveText(getPositiveButtonText()) + .negativeText(getNegativeButtonText()) + .autoDismiss(true); // immediately close the dialog after selection + + final View contentView = onCreateDialogView(); + if (contentView != null) { + onBindDialogView(contentView); + builder.customView(contentView, false); + } else { + builder.content(getDialogMessage()); + } + + PrefUtil.registerOnActivityDestroyListener(this, this); + + dialog = builder.build(); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + PrefUtil.unregisterOnActivityDestroyListener(this, this); + } + + @Override + public void onActivityDestroy() { + super.onActivityDestroy(); + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + Dialog dialog = getDialog(); + if (dialog == null || !dialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = dialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + // From DialogPreference + private static class SavedState extends BaseSavedState { + + public static final Creator CREATOR = + new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + boolean isDialogShowing; + Bundle dialogBundle; + + SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialEditTextPreference.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialEditTextPreference.java new file mode 100644 index 00000000..17c09e0b --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialEditTextPreference.java @@ -0,0 +1,263 @@ +package com.afollestad.materialdialogs.prefs; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.EditTextPreference; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatEditText; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.MaterialDialog.Builder; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.internal.MDTintHelper; +import com.afollestad.materialdialogs.util.DialogUtils; + +/** @author Aidan Follestad (afollestad) */ +public class MaterialEditTextPreference extends EditTextPreference { + + private int color = 0; + private MaterialDialog dialog; + private EditText editText; + + public MaterialEditTextPreference(Context context) { + super(context); + init(context, null); + } + + public MaterialEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialEditTextPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + PrefUtil.setLayoutResource(context, this, attrs); + int fallback; + fallback = DialogUtils.resolveColor(context, android.R.attr.colorAccent); + color = DialogUtils.resolveColor(context, R.attr.md_widget_color, fallback); + + editText = new AppCompatEditText(context, attrs); + // Give it an ID so it can be saved/restored + editText.setId(android.R.id.edit); + editText.setEnabled(true); + } + + @Override + protected void onAddEditTextToDialogView(@NonNull View dialogView, @NonNull EditText editText) { + ((ViewGroup) dialogView) + .addView( + editText, + new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + @SuppressLint("MissingSuperCall") + @Override + protected void onBindDialogView(@NonNull View view) { + EditText editText = this.editText; + editText.setText(getText()); + // Initialize cursor to end of text + if (editText.getText().length() > 0) { + editText.setSelection(editText.length()); + } + ViewParent oldParent = editText.getParent(); + if (oldParent != view) { + if (oldParent != null) { + ((ViewGroup) oldParent).removeView(editText); + } + onAddEditTextToDialogView(view, editText); + } + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + String value = editText.getText().toString(); + if (callChangeListener(value)) { + setText(value); + } + } + } + + @Override + public EditText getEditText() { + return editText; + } + + @Override + public Dialog getDialog() { + return dialog; + } + + @Override + protected void showDialog(Bundle state) { + Builder mBuilder = + new MaterialDialog.Builder(getContext()) + .title(getDialogTitle()) + .icon(getDialogIcon()) + .positiveText(getPositiveButtonText()) + .negativeText(getNegativeButtonText()) + .dismissListener(this) + .onAny( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + switch (which) { + default: + MaterialEditTextPreference.this.onClick( + dialog, DialogInterface.BUTTON_POSITIVE); + break; + case NEUTRAL: + MaterialEditTextPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEUTRAL); + break; + case NEGATIVE: + MaterialEditTextPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEGATIVE); + break; + } + } + }) + .dismissListener(this); + + @SuppressLint("InflateParams") + View layout = LayoutInflater.from(getContext()).inflate(R.layout.md_stub_inputpref, null); + onBindDialogView(layout); + + MDTintHelper.setTint(editText, color); + + TextView message = (TextView) layout.findViewById(android.R.id.message); + if (getDialogMessage() != null && getDialogMessage().toString().length() > 0) { + message.setVisibility(View.VISIBLE); + message.setText(getDialogMessage()); + } else { + message.setVisibility(View.GONE); + } + mBuilder.customView(layout, false); + + PrefUtil.registerOnActivityDestroyListener(this, this); + + dialog = mBuilder.build(); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + requestInputMethod(dialog); + + dialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + PrefUtil.unregisterOnActivityDestroyListener(this, this); + } + + /** Copied from DialogPreference.java */ + private void requestInputMethod(Dialog dialog) { + Window window = dialog.getWindow(); + if (window == null) { + return; + } + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + @Override + public void onActivityDestroy() { + super.onActivityDestroy(); + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + Dialog dialog = getDialog(); + if (dialog == null || !dialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = dialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + // From DialogPreference + private static class SavedState extends BaseSavedState { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + boolean isDialogShowing; + Bundle dialogBundle; + + SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialListPreference.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialListPreference.java new file mode 100644 index 00000000..00e5af59 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialListPreference.java @@ -0,0 +1,225 @@ +package com.afollestad.materialdialogs.prefs; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.lang.reflect.Field; + +/** @author Marc Holder Kluver (marchold), Aidan Follestad (afollestad) */ +public class MaterialListPreference extends ListPreference { + + private Context context; + private MaterialDialog dialog; + + public MaterialListPreference(Context context) { + super(context); + init(context, null); + } + + public MaterialListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialListPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + this.context = context; + PrefUtil.setLayoutResource(context, this, attrs); + } + + @Override + public void setEntries(CharSequence[] entries) { + super.setEntries(entries); + if (dialog != null) { + dialog.setItems(entries); + } + } + + @Override + public Dialog getDialog() { + return dialog; + } + + public RecyclerView getRecyclerView() { + if (getDialog() == null) { + return null; + } + return ((MaterialDialog) getDialog()).getRecyclerView(); + } + + @Override + protected void showDialog(Bundle state) { + if (getEntries() == null || getEntryValues() == null) { + throw new IllegalStateException( + "ListPreference requires an entries array and an entryValues array."); + } + + int preselect = findIndexOfValue(getValue()); + MaterialDialog.Builder builder = + new MaterialDialog.Builder(context) + .title(getDialogTitle()) + .icon(getDialogIcon()) + .dismissListener(this) + .onAny( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + switch (which) { + default: + MaterialListPreference.this.onClick( + dialog, DialogInterface.BUTTON_POSITIVE); + break; + case NEUTRAL: + MaterialListPreference.this.onClick(dialog, DialogInterface.BUTTON_NEUTRAL); + break; + case NEGATIVE: + MaterialListPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEGATIVE); + break; + } + } + }) + .negativeText(getNegativeButtonText()) + .items(getEntries()) + .autoDismiss(true) // immediately close the dialog after selection + .itemsCallbackSingleChoice( + preselect, + new MaterialDialog.ListCallbackSingleChoice() { + @Override + public boolean onSelection( + MaterialDialog dialog, View itemView, int which, CharSequence text) { + onClick(null, DialogInterface.BUTTON_POSITIVE); + if (which >= 0 && getEntryValues() != null) { + try { + Field clickedIndex = + ListPreference.class.getDeclaredField("mClickedDialogEntryIndex"); + clickedIndex.setAccessible(true); + clickedIndex.set(MaterialListPreference.this, which); + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + }); + + final View contentView = onCreateDialogView(); + if (contentView != null) { + onBindDialogView(contentView); + builder.customView(contentView, false); + } else { + builder.content(getDialogMessage()); + } + + PrefUtil.registerOnActivityDestroyListener(this, this); + + dialog = builder.build(); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + onClick(dialog, DialogInterface.BUTTON_NEGATIVE); + dialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + PrefUtil.unregisterOnActivityDestroyListener(this, this); + } + + @Override + public void onActivityDestroy() { + super.onActivityDestroy(); + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + Dialog dialog = getDialog(); + if (dialog == null || !dialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = dialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + // From DialogPreference + private static class SavedState extends BaseSavedState { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + boolean isDialogShowing; + Bundle dialogBundle; + + SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialMultiSelectListPreference.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialMultiSelectListPreference.java new file mode 100644 index 00000000..86501eb0 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/MaterialMultiSelectListPreference.java @@ -0,0 +1,224 @@ +package com.afollestad.materialdialogs.prefs; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.MultiSelectListPreference; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.NonNull; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class only works on Honeycomb (API 11) and above. + * + * @author Aidan Follestad (afollestad) + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +public class MaterialMultiSelectListPreference extends MultiSelectListPreference { + + private Context context; + private MaterialDialog mDialog; + + public MaterialMultiSelectListPreference(Context context) { + super(context); + init(context, null); + } + + public MaterialMultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MaterialMultiSelectListPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs); + } + + @Override + public void setEntries(CharSequence[] entries) { + super.setEntries(entries); + if (mDialog != null) { + mDialog.setItems(entries); + } + } + + private void init(Context context, AttributeSet attrs) { + this.context = context; + PrefUtil.setLayoutResource(context, this, attrs); + } + + @Override + public Dialog getDialog() { + return mDialog; + } + + @Override + protected void showDialog(Bundle state) { + List indices = new ArrayList<>(); + for (String s : getValues()) { + int index = findIndexOfValue(s); + if (index >= 0) { + indices.add(findIndexOfValue(s)); + } + } + MaterialDialog.Builder builder = + new MaterialDialog.Builder(context) + .title(getDialogTitle()) + .icon(getDialogIcon()) + .negativeText(getNegativeButtonText()) + .positiveText(getPositiveButtonText()) + .onAny( + new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + switch (which) { + default: + MaterialMultiSelectListPreference.this.onClick( + dialog, DialogInterface.BUTTON_POSITIVE); + break; + case NEUTRAL: + MaterialMultiSelectListPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEUTRAL); + break; + case NEGATIVE: + MaterialMultiSelectListPreference.this.onClick( + dialog, DialogInterface.BUTTON_NEGATIVE); + break; + } + } + }) + .items(getEntries()) + .itemsCallbackMultiChoice( + indices.toArray(new Integer[indices.size()]), + new MaterialDialog.ListCallbackMultiChoice() { + @Override + public boolean onSelection( + MaterialDialog dialog, Integer[] which, CharSequence[] text) { + onClick(null, DialogInterface.BUTTON_POSITIVE); + dialog.dismiss(); + final Set values = new HashSet<>(); + for (int i : which) { + values.add(getEntryValues()[i].toString()); + } + if (callChangeListener(values)) { + setValues(values); + } + return true; + } + }) + .dismissListener(this); + + final View contentView = onCreateDialogView(); + if (contentView != null) { + onBindDialogView(contentView); + builder.customView(contentView, false); + } else { + builder.content(getDialogMessage()); + } + + PrefUtil.registerOnActivityDestroyListener(this, this); + + mDialog = builder.build(); + if (state != null) { + mDialog.onRestoreInstanceState(state); + } + mDialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + PrefUtil.unregisterOnActivityDestroyListener(this, this); + } + + @Override + public void onActivityDestroy() { + super.onActivityDestroy(); + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + Dialog dialog = getDialog(); + if (dialog == null || !dialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = dialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + // From DialogPreference + private static class SavedState extends BaseSavedState { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + boolean isDialogShowing; + Bundle dialogBundle; + + SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/PrefUtil.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/PrefUtil.java new file mode 100644 index 00000000..e327aa1e --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/prefs/PrefUtil.java @@ -0,0 +1,81 @@ +package com.afollestad.materialdialogs.prefs; + +import android.content.Context; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.afollestad.materialdialogs.R; + +import java.lang.reflect.Method; + +/** @author Aidan Follestad (afollestad) */ +class PrefUtil { + + private PrefUtil() { + } + + static void setLayoutResource( + @NonNull Context context, @NonNull Preference preference, @Nullable AttributeSet attrs) { + boolean foundLayout = false; + if (attrs != null) { + for (int i = 0; i < attrs.getAttributeCount(); i++) { + final String namespace = ((XmlResourceParser) attrs).getAttributeNamespace(0); + if (namespace.equals("http://schemas.android.com/apk/res/android") + && attrs.getAttributeName(i).equals("layout")) { + foundLayout = true; + break; + } + } + } + + boolean useStockLayout = false; + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Preference, 0, 0); + try { + useStockLayout = a.getBoolean(R.styleable.Preference_useStockLayout, false); + } finally { + a.recycle(); + } + } + + if (!foundLayout && !useStockLayout) { + preference.setLayoutResource(R.layout.md_preference_custom); + } + } + + static void registerOnActivityDestroyListener( + @NonNull Preference preference, + @NonNull PreferenceManager.OnActivityDestroyListener listener) { + try { + PreferenceManager pm = preference.getPreferenceManager(); + Method method = + pm.getClass() + .getDeclaredMethod( + "registerOnActivityDestroyListener", + PreferenceManager.OnActivityDestroyListener.class); + method.setAccessible(true); + method.invoke(pm, listener); + } catch (Exception ignored) { + } + } + + static void unregisterOnActivityDestroyListener( + @NonNull Preference preference, + @NonNull PreferenceManager.OnActivityDestroyListener listener) { + try { + PreferenceManager pm = preference.getPreferenceManager(); + Method method = + pm.getClass() + .getDeclaredMethod( + "unregisterOnActivityDestroyListener", + PreferenceManager.OnActivityDestroyListener.class); + method.setAccessible(true); + method.invoke(pm, listener); + } catch (Exception ignored) { + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListAdapter.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListAdapter.java new file mode 100644 index 00000000..7976449a --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListAdapter.java @@ -0,0 +1,119 @@ +package com.afollestad.materialdialogs.simplelist; + +import android.graphics.PorterDuff; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.R; +import com.afollestad.materialdialogs.internal.MDAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * See the sample project to understand how this is used. Mimics the Simple List dialog style + * displayed on Google's guidelines site: + * https://www.google.com/design/spec/components/dialogs.html#dialogs-simple-dialogs + * + * @author Aidan Follestad (afollestad) + */ +public class MaterialSimpleListAdapter + extends RecyclerView.Adapter implements MDAdapter { + + private MaterialDialog dialog; + private List items; + private Callback callback; + + public MaterialSimpleListAdapter(Callback callback) { + items = new ArrayList<>(4); + this.callback = callback; + } + + public void add(MaterialSimpleListItem item) { + items.add(item); + notifyItemInserted(items.size() - 1); + } + + public void clear() { + items.clear(); + notifyDataSetChanged(); + } + + public MaterialSimpleListItem getItem(int index) { + return items.get(index); + } + + @Override + public void setDialog(MaterialDialog dialog) { + this.dialog = dialog; + } + + @Override + public SimpleListVH onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.md_simplelist_item, parent, false); + return new SimpleListVH(view, this); + } + + @Override + public void onBindViewHolder(SimpleListVH holder, int position) { + if (dialog != null) { + final MaterialSimpleListItem item = items.get(position); + if (item.getIcon() != null) { + holder.icon.setImageDrawable(item.getIcon()); + holder.icon.setPadding( + item.getIconPadding(), + item.getIconPadding(), + item.getIconPadding(), + item.getIconPadding()); + holder + .icon + .getBackground() + .setColorFilter(item.getBackgroundColor(), PorterDuff.Mode.SRC_ATOP); + } else { + holder.icon.setVisibility(View.GONE); + } + holder.title.setTextColor(dialog.getBuilder().getItemColor()); + holder.title.setText(item.getContent()); + dialog.setTypeface(holder.title, dialog.getBuilder().getRegularFont()); + } + } + + @Override + public int getItemCount() { + return items.size(); + } + + public interface Callback { + + void onMaterialListItemSelected(MaterialDialog dialog, int index, MaterialSimpleListItem item); + } + + static class SimpleListVH extends RecyclerView.ViewHolder implements View.OnClickListener { + + final ImageView icon; + final TextView title; + final MaterialSimpleListAdapter adapter; + + SimpleListVH(View itemView, MaterialSimpleListAdapter adapter) { + super(itemView); + icon = (ImageView) itemView.findViewById(android.R.id.icon); + title = (TextView) itemView.findViewById(android.R.id.title); + this.adapter = adapter; + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + if (adapter.callback != null) { + adapter.callback.onMaterialListItemSelected( + adapter.dialog, getAdapterPosition(), adapter.getItem(getAdapterPosition())); + } + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListItem.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListItem.java new file mode 100644 index 00000000..b947f3a9 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/simplelist/MaterialSimpleListItem.java @@ -0,0 +1,142 @@ +package com.afollestad.materialdialogs.simplelist; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntRange; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import com.afollestad.materialdialogs.util.DialogUtils; + +/** @author Aidan Follestad (afollestad) */ +public class MaterialSimpleListItem { + + private final Builder builder; + + private MaterialSimpleListItem(Builder builder) { + this.builder = builder; + } + + public Drawable getIcon() { + return builder.icon; + } + + public CharSequence getContent() { + return builder.content; + } + + public int getIconPadding() { + return builder.iconPadding; + } + + @ColorInt + public int getBackgroundColor() { + return builder.backgroundColor; + } + + public long getId() { + return builder.id; + } + + @Nullable + public Object getTag() { + return builder.tag; + } + + @Override + public String toString() { + if (getContent() != null) { + return getContent().toString(); + } else { + return "(no content)"; + } + } + + public static class Builder { + + private final Context context; + protected Drawable icon; + protected CharSequence content; + protected long id; + + int iconPadding; + int backgroundColor; + Object tag; + + public Builder(Context context) { + this.context = context; + backgroundColor = Color.parseColor("#BCBCBC"); + } + + public Builder icon(Drawable icon) { + this.icon = icon; + return this; + } + + public Builder icon(@DrawableRes int iconRes) { + return icon(ContextCompat.getDrawable(context, iconRes)); + } + + public Builder iconPadding(@IntRange(from = 0, to = Integer.MAX_VALUE) int padding) { + this.iconPadding = padding; + return this; + } + + public Builder iconPaddingDp(@IntRange(from = 0, to = Integer.MAX_VALUE) int paddingDp) { + this.iconPadding = + (int) + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + paddingDp, + context.getResources().getDisplayMetrics()); + return this; + } + + public Builder iconPaddingRes(@DimenRes int paddingRes) { + return iconPadding(context.getResources().getDimensionPixelSize(paddingRes)); + } + + public Builder content(CharSequence content) { + this.content = content; + return this; + } + + public Builder content(@StringRes int contentRes) { + return content(context.getString(contentRes)); + } + + public Builder backgroundColor(@ColorInt int color) { + this.backgroundColor = color; + return this; + } + + public Builder backgroundColorRes(@ColorRes int colorRes) { + return backgroundColor(DialogUtils.getColor(context, colorRes)); + } + + public Builder backgroundColorAttr(@AttrRes int colorAttr) { + return backgroundColor(DialogUtils.resolveColor(context, colorAttr)); + } + + public Builder id(long id) { + this.id = id; + return this; + } + + public Builder tag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + public MaterialSimpleListItem build() { + return new MaterialSimpleListItem(this); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/DialogUtils.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/DialogUtils.java new file mode 100644 index 00000000..6a1a0726 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/DialogUtils.java @@ -0,0 +1,298 @@ +package com.afollestad.materialdialogs.util; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.IBinder; +import android.util.TypedValue; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import androidx.annotation.ArrayRes; +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.afollestad.materialdialogs.GravityEnum; +import com.afollestad.materialdialogs.MaterialDialog; + +/** @author Aidan Follestad (afollestad) */ +public class DialogUtils { + + // @SuppressWarnings("ConstantConditions") + // public static float resolveFloat(Context context, int attr) { + // TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); + // try { + // return a.getFloat(0, 0); + // } finally { + // a.recycle(); + // } + // } + + @ColorInt + public static int getDisabledColor(Context context) { + final int primaryColor = resolveColor(context, android.R.attr.textColorPrimary); + final int disabledColor = isColorDark(primaryColor) ? Color.BLACK : Color.WHITE; + return adjustAlpha(disabledColor, 0.3f); + } + + @ColorInt + public static int adjustAlpha( + @ColorInt int color, @SuppressWarnings("SameParameterValue") float factor) { + int alpha = Math.round(Color.alpha(color) * factor); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + return Color.argb(alpha, red, green, blue); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr) { + return resolveColor(context, attr, 0); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getColor(0, fallback); + } finally { + a.recycle(); + } + } + + // Try to resolve the colorAttr attribute. + public static ColorStateList resolveActionTextColorStateList( + Context context, @AttrRes int colorAttr, ColorStateList fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); + try { + final TypedValue value = a.peekValue(0); + if (value == null) { + return fallback; + } + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + final ColorStateList stateList = a.getColorStateList(0); + if (stateList != null) { + return stateList; + } else { + return fallback; + } + } + } finally { + a.recycle(); + } + } + + // Get the specified color resource, creating a ColorStateList if the resource + // points to a color value. + public static ColorStateList getActionTextColorStateList(Context context, @ColorRes int colorId) { + final TypedValue value = new TypedValue(); + context.getResources().getValue(colorId, value, true); + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + return context.getColorStateList(colorId); + } + } + + /** + * Returns a color associated with a particular resource ID + * + *

Starting in {@link android.os.Build.VERSION_CODES#M}, the returned color will be styled for + * the specified Context's theme. + * + * @param colorId The desired resource identifier, as generated by the aapt tool. This integer + * encodes the package, type, and resource entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + */ + @ColorInt + public static int getColor(Context context, @ColorRes int colorId) { + return ContextCompat.getColor(context, colorId); + } + + public static String resolveString(Context context, @AttrRes int attr) { + TypedValue v = new TypedValue(); + context.getTheme().resolveAttribute(attr, v, true); + return (String) v.string; + } + + private static int gravityEnumToAttrInt(GravityEnum value) { + switch (value) { + case CENTER: + return 1; + case END: + return 2; + default: + return 0; + } + } + + public static GravityEnum resolveGravityEnum( + Context context, @AttrRes int attr, GravityEnum defaultGravity) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + switch (a.getInt(0, gravityEnumToAttrInt(defaultGravity))) { + case 1: + return GravityEnum.CENTER; + case 2: + return GravityEnum.END; + default: + return GravityEnum.START; + } + } finally { + a.recycle(); + } + } + + public static Drawable resolveDrawable(Context context, @AttrRes int attr) { + return resolveDrawable(context, attr, null); + } + + private static Drawable resolveDrawable( + Context context, + @AttrRes int attr, + @SuppressWarnings("SameParameterValue") Drawable fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + Drawable d = a.getDrawable(0); + if (d == null && fallback != null) { + d = fallback; + } + return d; + } finally { + a.recycle(); + } + } + + public static int resolveDimension(Context context, @AttrRes int attr) { + return resolveDimension(context, attr, -1); + } + + private static int resolveDimension(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getDimensionPixelSize(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr, boolean fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getBoolean(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr) { + return resolveBoolean(context, attr, false); + } + + public static boolean isColorDark(@ColorInt int color) { + double darkness = + 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) + / 255; + return darkness >= 0.5; + } + + public static void setBackgroundCompat(View view, Drawable d) { + view.setBackground(d); + } + + public static void showKeyboard( + @NonNull final DialogInterface di, @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + dialog + .getInputEditText() + .post( + new Runnable() { + @Override + public void run() { + dialog.getInputEditText().requestFocus(); + InputMethodManager imm = + (InputMethodManager) + builder.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(dialog.getInputEditText(), InputMethodManager.SHOW_IMPLICIT); + } + } + }); + } + + public static void hideKeyboard( + @NonNull final DialogInterface di, @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + InputMethodManager imm = + (InputMethodManager) builder.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + final View currentFocus = dialog.getCurrentFocus(); + IBinder windowToken = null; + if (currentFocus != null) { + windowToken = currentFocus.getWindowToken(); + } else if (dialog.getView() != null) { + windowToken = dialog.getView().getWindowToken(); + } + if (windowToken != null) { + imm.hideSoftInputFromWindow(windowToken, 0); + } + } + } + + public static ColorStateList getActionTextStateList(Context context, int newPrimaryColor) { + final int fallBackButtonColor = + DialogUtils.resolveColor(context, 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); + } + + public static int[] getColorArray(@NonNull Context context, @ArrayRes int array) { + if (array == 0) { + return null; + } + TypedArray ta = context.getResources().obtainTypedArray(array); + int[] colors = new int[ta.length()]; + for (int i = 0; i < ta.length(); i++) { + colors[i] = ta.getColor(i, 0); + } + ta.recycle(); + return colors; + } + + public static boolean isIn(@NonNull T find, @Nullable T[] ary) { + if (ary == null || ary.length == 0) { + return false; + } + for (T item : ary) { + if (item.equals(find)) { + return true; + } + } + return false; + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/RippleHelper.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/RippleHelper.java new file mode 100644 index 00000000..88a264b0 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/RippleHelper.java @@ -0,0 +1,19 @@ +package com.afollestad.materialdialogs.util; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import androidx.annotation.ColorInt; + +/** @author Aidan Follestad (afollestad) */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RippleHelper { + + public static void applyColor(Drawable d, @ColorInt int color) { + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setColor(ColorStateList.valueOf(color)); + } + } +} diff --git a/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/TypefaceHelper.java b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/TypefaceHelper.java new file mode 100644 index 00000000..27a7f379 --- /dev/null +++ b/modules/material-dialogs/src/main/java/com/afollestad/materialdialogs/util/TypefaceHelper.java @@ -0,0 +1,45 @@ +package com.afollestad.materialdialogs.util; + +import android.content.Context; +import android.graphics.Typeface; +import androidx.collection.SimpleArrayMap; + +/* + Each call to Typeface.createFromAsset will load a new instance of the typeface into memory, + and this memory is not consistently get garbage collected + http://code.google.com/p/android/issues/detail?id=9904 + (It states released but even on Lollipop you can see the typefaces accumulate even after + multiple GC passes) + + You can detect this by running: + adb shell dumpsys meminfo com.your.packagenage + + You will see output like: + + Asset Allocations + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Regular.ttf: 123K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + +*/ +public class TypefaceHelper { + + private static final SimpleArrayMap cache = new SimpleArrayMap<>(); + + public static Typeface get(Context c, String name) { + synchronized (cache) { + if (!cache.containsKey(name)) { + try { + Typeface t = Typeface.createFromAsset(c.getAssets(), String.format("fonts/%s", name)); + cache.put(name, t); + return t; + } catch (RuntimeException e) { + return null; + } + } + return cache.get(name); + } + } +} diff --git a/modules/material-dialogs/src/main/res/anim/decelerate_cubic.xml b/modules/material-dialogs/src/main/res/anim/decelerate_cubic.xml new file mode 100644 index 00000000..9f46713f --- /dev/null +++ b/modules/material-dialogs/src/main/res/anim/decelerate_cubic.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/anim/popup_enter.xml b/modules/material-dialogs/src/main/res/anim/popup_enter.xml new file mode 100644 index 00000000..51039efb --- /dev/null +++ b/modules/material-dialogs/src/main/res/anim/popup_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/anim/popup_exit.xml b/modules/material-dialogs/src/main/res/anim/popup_exit.xml new file mode 100644 index 00000000..457d02ac --- /dev/null +++ b/modules/material-dialogs/src/main/res/anim/popup_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple.xml b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple.xml new file mode 100644 index 00000000..732eea4e --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple_dark.xml b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple_dark.xml new file mode 100644 index 00000000..732eea4e --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_selector_ripple_dark.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable-v21/md_btn_shape.xml b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_shape.xml new file mode 100644 index 00000000..a0060c7b --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable-v21/md_btn_shape.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/gray_circle.xml b/modules/material-dialogs/src/main/res/drawable/gray_circle.xml new file mode 100644 index 00000000..4c36a36c --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/gray_circle.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_btn_selected.xml b/modules/material-dialogs/src/main/res/drawable/md_btn_selected.xml new file mode 100644 index 00000000..65d97c86 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_btn_selected.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_btn_selected_dark.xml b/modules/material-dialogs/src/main/res/drawable/md_btn_selected_dark.xml new file mode 100644 index 00000000..f91e7d78 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_btn_selected_dark.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_btn_selector.xml b/modules/material-dialogs/src/main/res/drawable/md_btn_selector.xml new file mode 100644 index 00000000..ead9d212 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_btn_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_btn_selector_dark.xml b/modules/material-dialogs/src/main/res/drawable/md_btn_selector_dark.xml new file mode 100644 index 00000000..564e1410 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_btn_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_ic_more.png b/modules/material-dialogs/src/main/res/drawable/md_ic_more.png new file mode 100644 index 00000000..de851fc5 Binary files /dev/null and b/modules/material-dialogs/src/main/res/drawable/md_ic_more.png differ diff --git a/modules/material-dialogs/src/main/res/drawable/md_item_selected.xml b/modules/material-dialogs/src/main/res/drawable/md_item_selected.xml new file mode 100644 index 00000000..10bf840b --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_item_selected.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_item_selected_dark.xml b/modules/material-dialogs/src/main/res/drawable/md_item_selected_dark.xml new file mode 100644 index 00000000..b9252007 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_item_selected_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_nav_back.xml b/modules/material-dialogs/src/main/res/drawable/md_nav_back.xml new file mode 100644 index 00000000..a95494b7 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_nav_back.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_selector.xml b/modules/material-dialogs/src/main/res/drawable/md_selector.xml new file mode 100644 index 00000000..df3322b3 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_selector_dark.xml b/modules/material-dialogs/src/main/res/drawable/md_selector_dark.xml new file mode 100644 index 00000000..8c599e92 --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/drawable/md_transparent.xml b/modules/material-dialogs/src/main/res/drawable/md_transparent.xml new file mode 100644 index 00000000..261a644c --- /dev/null +++ b/modules/material-dialogs/src/main/res/drawable/md_transparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_multichoice.xml b/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_multichoice.xml new file mode 100644 index 00000000..5dc1acfa --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_multichoice.xml @@ -0,0 +1,39 @@ + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml b/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml new file mode 100644 index 00000000..7a720350 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml @@ -0,0 +1,39 @@ + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress.xml b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress.xml new file mode 100644 index 00000000..3bfc4ed9 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate.xml b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate.xml new file mode 100644 index 00000000..3c4517ae --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml new file mode 100644 index 00000000..b34cfd3f --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_basic.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_basic.xml new file mode 100644 index 00000000..1983a02a --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_basic.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_basic_check.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_basic_check.xml new file mode 100644 index 00000000..57cbbaa2 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_basic_check.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_colorchooser.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_colorchooser.xml new file mode 100644 index 00000000..930c66d3 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_colorchooser.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_custom.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_custom.xml new file mode 100644 index 00000000..42bcfbc8 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_custom.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_input.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_input.xml new file mode 100644 index 00000000..58ddcea7 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_input.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_input_check.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_input_check.xml new file mode 100644 index 00000000..12d3623e --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_input_check.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_list.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_list.xml new file mode 100644 index 00000000..e455ac3b --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_list.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_list_check.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_list_check.xml new file mode 100644 index 00000000..687cc0b2 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_list_check.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_progress.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_progress.xml new file mode 100644 index 00000000..1fb39b76 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_progress.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate.xml new file mode 100644 index 00000000..9cf4670e --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate_horizontal.xml b/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate_horizontal.xml new file mode 100644 index 00000000..e2aa2585 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_dialog_progress_indeterminate_horizontal.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_listitem.xml b/modules/material-dialogs/src/main/res/layout/md_listitem.xml new file mode 100644 index 00000000..ca89f80b --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_listitem.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_listitem_multichoice.xml b/modules/material-dialogs/src/main/res/layout/md_listitem_multichoice.xml new file mode 100644 index 00000000..4b52a083 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_listitem_multichoice.xml @@ -0,0 +1,40 @@ + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_listitem_singlechoice.xml b/modules/material-dialogs/src/main/res/layout/md_listitem_singlechoice.xml new file mode 100644 index 00000000..4ed2bce3 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_listitem_singlechoice.xml @@ -0,0 +1,40 @@ + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_preference_custom.xml b/modules/material-dialogs/src/main/res/layout/md_preference_custom.xml new file mode 100644 index 00000000..842a9ab9 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_preference_custom.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_simplelist_item.xml b/modules/material-dialogs/src/main/res/layout/md_simplelist_item.xml new file mode 100644 index 00000000..8a085e0c --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_simplelist_item.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_actionbuttons.xml b/modules/material-dialogs/src/main/res/layout/md_stub_actionbuttons.xml new file mode 100644 index 00000000..ad15a6c4 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_actionbuttons.xml @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_custom.xml b/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_custom.xml new file mode 100644 index 00000000..6de905eb --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_custom.xml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_grid.xml b/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_grid.xml new file mode 100644 index 00000000..a4e7c839 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_colorchooser_grid.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_inputpref.xml b/modules/material-dialogs/src/main/res/layout/md_stub_inputpref.xml new file mode 100644 index 00000000..26d9d928 --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_inputpref.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_progress.xml b/modules/material-dialogs/src/main/res/layout/md_stub_progress.xml new file mode 100644 index 00000000..f107f11b --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_progress.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate.xml b/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate.xml new file mode 100644 index 00000000..a31fa1dc --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate_horizontal.xml b/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate_horizontal.xml new file mode 100644 index 00000000..259f73cf --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_progress_indeterminate_horizontal.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_titleframe.xml b/modules/material-dialogs/src/main/res/layout/md_stub_titleframe.xml new file mode 100644 index 00000000..00183c2f --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_titleframe.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/layout/md_stub_titleframe_lesspadding.xml b/modules/material-dialogs/src/main/res/layout/md_stub_titleframe_lesspadding.xml new file mode 100644 index 00000000..5d840ead --- /dev/null +++ b/modules/material-dialogs/src/main/res/layout/md_stub_titleframe_lesspadding.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/menu/md_more_options.xml b/modules/material-dialogs/src/main/res/menu/md_more_options.xml new file mode 100644 index 00000000..1fe7aa6d --- /dev/null +++ b/modules/material-dialogs/src/main/res/menu/md_more_options.xml @@ -0,0 +1,2 @@ + +

\ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-large/dimens.xml b/modules/material-dialogs/src/main/res/values-large/dimens.xml new file mode 100644 index 00000000..ed362496 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-large/dimens.xml @@ -0,0 +1,6 @@ + + + + 28dp + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-sw600dp/dimens.xml b/modules/material-dialogs/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 00000000..e1cf92f6 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,6 @@ + + + + 446dp + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-sw720dp-land/dimens.xml b/modules/material-dialogs/src/main/res/values-sw720dp-land/dimens.xml new file mode 100644 index 00000000..93690aa7 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-sw720dp-land/dimens.xml @@ -0,0 +1,6 @@ + + + + 564dp + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-sw720dp/dimens.xml b/modules/material-dialogs/src/main/res/values-sw720dp/dimens.xml new file mode 100644 index 00000000..acf0656f --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-sw720dp/dimens.xml @@ -0,0 +1,6 @@ + + + + 496dp + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-v11/styles.xml b/modules/material-dialogs/src/main/res/values-v11/styles.xml new file mode 100644 index 00000000..b2096deb --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-v11/styles.xml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values-v21/styles.xml b/modules/material-dialogs/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..1c7fc215 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values-v21/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/attrs.xml b/modules/material-dialogs/src/main/res/values/attrs.xml new file mode 100644 index 00000000..567daa38 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/attrs.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/colors.xml b/modules/material-dialogs/src/main/res/values/colors.xml new file mode 100644 index 00000000..6b12d9cc --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + #33969696 + #40CBCBCB + + #10000000 + #10FFFFFF + + #2196F3 + #1565C0 + + #DD2C00 + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/dimens.xml b/modules/material-dialogs/src/main/res/values/dimens.xml new file mode 100644 index 00000000..0d0e9ac3 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/dimens.xml @@ -0,0 +1,91 @@ + + + + + + + 24dp + + 12dp + 6dp + + 16dp + + 20dp + + 8dp + 8dp + + 72dp + + 6dp + 4dp + 1dp + 8dp + 4dp + 32dp + + 12dp + 12dp + + + 8dp + 8dp + 48dp + 20sp + 16sp + 14sp + 16sp + 48dp + 6dp + 16dp + 48dp + 24dp + 2dp + 1dp + + 2dp + 4dp + 12dp + 8dp + + 356dp + 52dp + 28dp + + 62dp + 40dp + 16dp + 18sp + + 16dp + 56dp + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/ids.xml b/modules/material-dialogs/src/main/res/values/ids.xml new file mode 100644 index 00000000..020f6c31 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/ids.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/public.xml b/modules/material-dialogs/src/main/res/values/public.xml new file mode 100644 index 00000000..31e19d84 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/public.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/material-dialogs/src/main/res/values/strings.xml b/modules/material-dialogs/src/main/res/values/strings.xml new file mode 100644 index 00000000..3e958b77 --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + + Back + Done + Cancel + Choose + Error + Your app has not been granted the READ_EXTERNAL_STORAGE permission. + Custom + Presets + New Folder + \ No newline at end of file diff --git a/modules/material-dialogs/src/main/res/values/styles.xml b/modules/material-dialogs/src/main/res/values/styles.xml new file mode 100644 index 00000000..5133c89e --- /dev/null +++ b/modules/material-dialogs/src/main/res/values/styles.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1fadec0c..50644f5d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ private val modules = listOf( "apk-signer", "apk-parser", "color-picker", + "material-dialogs", "material-date-time-picker", )