本文会不按期更新,推荐watch下项目。css
若是喜欢请star,若是以为有纰漏请提交issue,若是你有更好的点子能够提交pull request。html
本文的示例代码主要是基于EasyDialog这个库编写的,若你有其余的技巧和方法能够参与进来一块儿完善这篇文章。java
本文固定链接:github.com/tianzhijiex…react
有一个统一的dialog样式对于app是极其重要的,这种统一规范越早作越好。业务开发者应该去调用统一封装好的java api,不要随意定义本身顺手的类。固然对于特殊的业务,咱们能够在可控的状况下扩展基础的dialog api,而扩展性也是基础api所需具有的能力之一。android
实际中的不少项目都会封装原生dialog以知足自定义的需求,但其实原生dialog的设计已经将style和java逻辑彻底分离了,是一个标准的html+css的作法,并且还提供了详细的配置方案,能够说是十分完整了。本章将从源码入手,帮助你们从新认识dialog,但愿能够帮助读者找到最简单、最便捷的自定义dialog写法。ios
题外话:git
当业务开发者开始抛弃项目的基础api而定义自定义类的时候,通常是由于基础api的易用度、稳定性和扩展性出了问题。程序员
不管是大型项目仍是小型项目,设计给出的对话框样式必然是变幻无穷的,甚至一个项目里有三种以上的样式都不足为奇。经过长期的工做发现,下列问题广泛存在于各个项目中:github
既然写代码的原则是能少些就小写,能用稳定的android代码则用,那么咱们天然但愿能够利用原生的api来实现高扩展性的自定义的dialog,这也是咱们须要了解源码的重要缘由。后端
知道如何造轮子才能更好的用轮子,因此咱们先来看看android中古老的dialog类。不管是support包中的alertDialog仍是android sdk自带的datePickerDialog,他们都是继承自Dialog这个类:
Dialog的显示就是下面三行代码:
Dialog dialog=new Dialog(MainActivity.this); // 建立
dialog.setContentView(R.layout.dialog); // 设置view
dialog.show(); // 展现
复制代码
由于dialog是动态建立的,因此咱们能够猜测是view被动态挂载到了window上,下面来简单分析下初始化的过程。
Dialog(Context context, int themeResId, boolean createContextThemeWrapper) {
// 1. 设置context,必须是activity
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
// 去当前activity的theme中检索dialogTheme,用来渲染view的样式
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
// 2. 设置window,本质上是一个phoneWindow对象
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
// 创建事件的统一处理器,方便处理全部的事件
mListenersHandler = new ListenersHandler(this);
}
复制代码
第一步是context的初始化,从上面的代码可知dialog展现的时候须要主题资源,也就是contextThemeWrapper,也就是说这个context对象也只能是activity了。Dialog会根据当前activity的theme来获得dialog本身的theme,这里的样式id就是dialogTheme
。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="dialogTheme">@style/Theme.Dialog</item>
</style>
复制代码
为了帮助你们理解context在不一样场景下的具体对象,故给出下表:
关于NO上标的解释:
注:contentProvider、broadcastReceiver之因此在上述表格中,是由于在其内部有一个context对象。
图片来源:Android Context 上下文 你必须知道的一切
第二步是对window的操做,这里再次贴一下关键代码:
// context是activity,因此这里须要activity的windowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext); // 创建phoneWindow
w.setOnWindowDismissedCallback(this); // 设置监听
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel(); // 设置监听
}
});
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this); // 设置监听
复制代码
这里的回调监有关于window的,也有关于dialog的listenersHandler,这个handler会被用来处理dialog的三个重要事件:显示、取消和关闭。
private static final class ListenersHandler extends Handler {
// 弱引用
private final WeakReference<DialogInterface> mDialog;
public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}
复制代码
接着咱们来看设置view的代码,既然在初始化的时候获得了当前phoneWindow这个window对象,那么挂载view的重任天然就交给它了:
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
public void setContentView(@NonNull View view) {
mWindow.setContentView(view);
}
public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
mWindow.setContentView(view, params);
}
public void setTitle(@Nullable CharSequence title) {
mWindow.setTitle(title);
mWindow.getAttributes().setTitle(title);
}
public @Nullable View getCurrentFocus() {
return mWindow != null ? mWindow.getCurrentFocus() : null;
}
复制代码
能够这么说,正如intent是bundle的封装同样,dialog是window的封装。Dialog的不少public方法都是对内部的window的操做,好比dialog.setTitle()、dialog.getCurrentFocus()等。
显示一个dialog过程其实就是将上一步设置的view挂载到window的过程,在方法执行完毕后dialog会发送一个已经show的信号,用来标记当前dialog的状态和触发监听事件。
public void show() {
dispatchOnCreate(null); // 执行onCreate()
onStart(); // 调用onStart方法
//获取DecorView对象实例
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
mWindowManager.addView(mDecor, l); // 将decorView添加到window上(关键方法)
mShowing = true;
sendShowMessage(); // 发送“显示”的信号,调用相关的listener
}
复制代码
相对的还有dismiss()方法,关闭后会发送一个dismiss的信号:
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
void dismissDialog() {
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop(); // 执行onStop()
mShowing = false;
sendDismissMessage(); // 发送dismiss信号
}
}
复制代码
这里顺便提一下,android中只要涉及到view的展现,那必然会有view数据保存的问题。在dialog中也提供了相似于activity的数据保存方法,下面这两个方法会自动保存和恢复dialog中view的各类状态。
public Bundle onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putBoolean(DIALOG_SHOWING_TAG, mShowing);
if (mCreated) {
bundle.putBundle(DIALOG_HIERARCHY_TAG, mWindow.saveHierarchyState());
}
return bundle;
}
复制代码
恢复状态:
public void onRestoreInstanceState(Bundle savedInstanceState) {
Bundle dialogHierarchyState = savedInstanceState.getBundle(DIALOG_HIERARCHY_TAG);
if (dialogHierarchyState == null) {
// dialog has never been shown, or onCreated, nothing to restore.
return;
}
dispatchOnCreate(savedInstanceState);
mWindow.restoreHierarchyState(dialogHierarchyState);
if (savedInstanceState.getBoolean(DIALOG_SHOWING_TAG)) {
show();
}
}
复制代码
这两个方法最终会被外部进行调用,而调用的地方一般是activity或dialogFragment,从以前的知识可知fragment的数据保存是经过activity来作的,因此dialog保存的数据从本质上是存在activity的bundle中的。
Activity中的performSaveInstanceState():
final void performSaveInstanceState(Bundle outState) {
onSaveInstanceState(outState); // 保存activity自身的数据
saveManagedDialogs(outState); // 保存dialog
mActivityTransitionState.saveState(outState);
}
复制代码
通常状况下咱们不用过多的考虑数据保存的问题,由于系统提供的view都已经帮咱们处理好了。但若是你的dialog中有自定义view,若自定义view中你并无处理view的onSaveInstanceState(),那么旋转后的dialog中的数据颇有可能不会如你想象的同样保留下来。关于如何处理自定义view的状态,能够参考《Android中正确保存view的状态》。
(图示代表dialog类仅仅提供的是一块白板)
由于google官方建议不要直接使用progressDialog和dialog类,因此咱们一般用的是alertDialog。能够说dialog提供了一个基础的空白画板,而alertDialog则会用一些title、message等对其进行填充,实现了一个基本的样式。
AlertDialog是dialog的子类,经过其构造方法可知全部的重要逻辑都是交给alertController进行代理的:
protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, resolveDialogTheme(context, themeResId));
mAlert = new AlertController(getContext(), this, getWindow());
}
复制代码
AlertDialog和appCompatActivity同样使用了代理模式,下面咱们就来看看这个alertController到底作了些什么事情。
从android官网可知alertDialog提供了以下样式:
关于ui方面,相信你们都十分熟悉了,这里就不贴图说明了。咱们在alertController的构造中能够看到上述样式的属性id,也就是说这些布局最终会被填充到dialog提供的白板中,而布局文件中的view则是最终数据展现的载体。
public AlertController(Context context, AppCompatDialog di, Window window) {
final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
R.attr.alertDialogStyle, 0);
mAlertDialogLayout = a.getResourceId(R.styleable.AlertDialog_android_layout, 0);
mButtonPanelSideLayout = a.getResourceId(
R.styleable.AlertDialog_buttonPanelSideLayout, 0);
mListLayout = a.getResourceId(R.styleable.AlertDialog_listLayout, 0);
mMultiChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_multiChoiceItemLayout, 0);
mSingleChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_singleChoiceItemLayout, 0);
mListItemLayout = a.getResourceId(R.styleable.AlertDialog_listItemLayout, 0);
a.recycle();
/* We use a custom title so never request a window title */
di.supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
}
复制代码
上述代码中初始化了以下viewGroup,具体的布局和样式随着activity的theme的不一样而不一样。
布局对象 | style | 提供的view |
---|---|---|
mAlertDialogLayout | AlertDialog_android_layout | title、message、button和容器 |
mButtonPanelSideLayout | AlertDialog_buttonPanelSideLayout | 三个按钮在右侧的容器 |
mListLayout | AlertDialog_listLayout | AlertController.RecycleListView |
mMultiChoiceItemLayout | AlertDialog_multiChoiceItemLayout | CheckedTextView |
mSingleChoiceItemLayout | AlertDialog_singleChoiceItemLayout | CheckedTextView |
mListItemLayout | AlertDialog_listItemLayout | TextView |
详细的布局能够参考源码中theme的定义:
<style name="Base.AlertDialog.AppCompat" parent="android:Widget">
<item name="android:layout">@layout/abc_alert_dialog_material</item>
<item name="listLayout">@layout/abc_select_dialog_material</item>
<item name="listItemLayout">@layout/select_dialog_item_material</item>
<item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
<item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
<item name="buttonIconDimen">@dimen/abc_alert_dialog_button_dimen</item>
</style>
<style name="AlertDialog.Leanback" parent="AlertDialog.Material">
<item name="buttonPanelSideLayout">@android:layout/alert_dialog_leanback_button_panel_side</item>
</style>
复制代码
从代码来看,下图中框起来的是一个叫作customPanel的frameLayout,是自定义布局的容器:
主布局文件,即android:layout定义的布局文件,里面提供了icon、title、message、button来给dialog这个空白的画板增长了最基本的元素。不管是自定义布局仍是android提供的单选或多选列表,他们都是将本身的布局文件add到了主布局文件的customPanel中。
下图为单选对话框的布局结构,能够看见listView被add到了id为customPanel的frameLayout中:
builder = new AlertDialog.Builder(this);
builder.setIcon(R.mipmap.ic_launcher);
builder.setTitle(R.string.simple_list_dialog);
String[] Items = {"one","two","three"};
builder.setItems(Items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// ...
}
});
builder.setCancelable(true);
AlertDialog dialog = builder.create();
dialog.show();
复制代码
咱们如今知道了全部的布局都是经过alertController来设置的,那么给这些布局中的view填充的数据则是alertDialog.Builder的工做了。
由于dialog中的每一个数据都是能够独立存在的,对于构建这种有大量可选数据的对象,咱们在java中通常会经过builder模式去创建它,而android中的dialog则是一个教科书般的例子。
public static class Builder {
private final AlertController.AlertParams P; // 重要的参数
public Builder(Context context, int themeResId) {
// new出P对象
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
}
public Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
// ...
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
// 将P中的数据塞入alertDialog
P.apply(dialog.mAlert); // dialog.mAlert为alertController对象
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
return dialog;
}
}
复制代码
经过上述代码咱们能够知道builder中全部的数据最终都会存放在P中,而这个P(alertParams)就是alertDialog的全部数据参数的聚合。在alertDialog.Builder.create()中,P.apply()执行了最终的装配工做,将数据分别设置到了dialog的各个view中,让其有了title、icon等信息。
public AlertParams(Context context) {
this.mContext = context;
this.mCancelable = true;
this.mInflater = (LayoutInflater)context.getSystemService("layout_inflater");
}
public void apply(AlertController dialog) {
// 由于alertController管理了各类布局文件
// 因此经过alertController来将数据设置给各个view
if (this.mTitle != null) {
dialog.setTitle(this.mTitle);
}
if (this.mIcon != null) {
dialog.setIcon(this.mIcon);
}
// ...
}
复制代码
总结:
AlertController承担了管理dialog中全部view的工做,alertController中的alertParams承担了数据的聚合工做。AlertParams经过apply()让alertController将数据和视图进行绑定,展现出一个完整的alertDialog。
题外话:
AlertDialog自身的theme是经过
alertDialogTheme
进行设置的,咱们能够在style中经过设置以下的属性来定义它的样式。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" >
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
复制代码
new AlertDialog.Builder(this)
.setTitle("title")
.setIcon(R.drawable.ic_launcher)
.setPositiveButton("好", new positiveListener())
.setNeutralButton("中", new NeutralListener())
.setNegativeButton("差", new NegativeListener())
.creat()
.show();
复制代码
通常的alertDialog用法如上,但若是咱们想要对传入的参数作校验和判空呢,若是想要作一些通用的背景设置呢?
若是咱们更进一步,作一个如上图所示的自定义dialog。那么咱们须要绑定自定义view,甚至可能会进行网络的请求。若是用alertDialog作,上述的代码都须要在activity中完成,这会让activity的代码变得混乱不堪,让dialog失去内聚性。
一个在activity中写逻辑的糟糕例子:
public class MainActivity extends AppCompatActivity {
EditText inputTextEt; // dialog中的editText
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("title")
.setView(R.layout.dialog_input_layout)
.setCancelable(false)
.show();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
inputTextEt = ((AlertDialog)dialog).findViewById(R.id.input_et);
inputTextEt.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// 执行逻辑代码
return false;
}
});
}
});
}
}
复制代码
若是更加极端一些,屏幕方向发生变化后,activity会重建,以前显示的对话框就不见了,查看log能够发现以下异常:
04-1917:30:06.999: E/WindowManager(14495): Activitycom.example.androidtest.MainActivity has leaked windowcom.android.internal.policy.impl.PhoneWindow$DecorView{42ca3c18 V.E.....R....... 0,0-1026,414} that was originally added here
综上所述,alertDialog已经不能知足现今的复杂需求了,咱们能够考虑创建一个帮助类来解决一些问题:
public class DialogHelper {
private String title, msg;
/**
* 各类自定义参数,如:title
*/
public void setTitle(String title) {
this.title = title;
}
/**
* 各类自定义参数,如:message
*/
public void setMsg(String msg) {
this.msg = msg;
}
public void show(Context context) {
// 经过配置的参数来创建一个dialog
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(msg)
.create();
// ...
// 通用的设置
Window window = dialog.getWindow();
window.setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
dialog.show();
}
}
复制代码
帮助类的出现解决了重复代码过多和内聚性差的问题,可是仍旧没有解决dialog数据保存和生命周期管理等问题。Google在android 3.0的时候引入了一个新的类dialogFragment,如今咱们彻底可使用dialogFragment做一个controller来管理alertDialog,省去了创建帮助类的麻烦。
能够这么说,alertDialog被dialogFragment管理,dialogFragment被fragmentManager管理,fragmentManager被fragmentActivity调用。
各自的工做以下:
目前官方推荐使用dialogFragment来管理对话框,因此它可确保能正确的处理生命周期事件。DialogFragment就是一个fragment,当用户旋转屏幕时仍旧是fragment的执行流程。
旋转屏幕时的log:
04-1917:45:41.289: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:41.309: D/==========(16156): MyDialogFragment : onStart
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onStop
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onDestroyView
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onDetach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onStart
复制代码
从log可知,只要旋转屏幕就会销毁当前fragment,并创建一个新的fragment挂载到activity中,这天然能够保证dialogFragment在旋转后仍旧保留dialog,不会出现转屏后dialog自动消失的问题。
既然谈到了转屏,那么就要记得保存和恢复数据。DialogFragment的onActivityCreated()中会触发mDialog.onRestoreInstanceState()
,这个就是dialog恢复数据的方法(保存方法是onSaveInstanceState())。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final Activity activity = getActivity();
mDialog.setOwnerActivity(activity);
if (savedInstanceState != null) {
// 恢复数据
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mDialog != null) {
Bundle dialogState = mDialog.onSaveInstanceState();
if (dialogState != null) {
// 保存数据
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
}
}
}
复制代码
DialogFragment的设计虽然精巧,但要知道dialogFragment和fragment是有差别的。Google官方强烈不推荐在fragment的onCreateView()中直接inflate一个布局,推荐的作法是在onCreateDialog()中创建dialog对象。
强烈禁止的写法:
public class MyDialogFragment extends DialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// 不要复写dialogFragment的onCreateView(),若是非要复写,请直接返回null
return inflater.inflate(R.layout.dialog, null);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 应该创建dialog的方法
Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("用户申明")
.setMessage(getResources().getString(R.string.hello_world))
.setPositiveButton("我赞成", this)
.setNegativeButton("不一样意", this)
.setCancelable(false);
//.show(); // 注意不要调用show()
return builder.create();
}
}
复制代码
DialogFragment用了fragment的机制,简单完成了数据的保存和恢复工做,同时又经过onCreateDialog()来创建alertDialog对象,将fragment和alertDialog结合的至关巧妙。
Dialog重要的方法是show()和dismiss(),在dialogFragment中,这两个方法都是会被fragment间接调用的,下面咱们就来看下这两个过程。
show()
DialogFragment的show()实际上是创建了一个fragment对象,而后执行了无容器的add()操做。Fragment启动后会调用内部的onCreateDialog()创建真正的dialog对象,最后在onStart()中触发dialog.show()。
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
public int show(FragmentTransaction transaction, String tag) {
mDismissed = false;
mShownByMe = true;
transaction.add(this, tag);
mViewDestroyed = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
@Override
public void onStart() {
super.onStart();
if (mDialog != null) {
mViewDestroyed = false;
mDialog.show(); // 在fragment的onStart()中调用了dialog的show()
}
}
复制代码
必须注意的是,咱们必须在onStart()以后再去执行dialog.findView()的操做,不然会出现NPE。
dismiss()
DialogFragment提供了两个关闭的方法,分别是dismiss()和dismissAllowingStateLoss(),前者对应的是fragmentTransaction.commit(),后者对应的是fragmentTransaction.commitAllowingStateLoss()。用dismissAllowingStateLoss()的好处是可让咱们忽略异步关闭dialog时的状态问题,让咱们不用考虑当前activity的状态,这会减小不少线上的崩溃。
public void dismiss() {
dismissInternal(false);
}
public void dismissAllowingStateLoss() {
dismissInternal(true);
}
void dismissInternal(boolean allowStateLoss) {
mDismissed = true;
mShownByMe = false;
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
}
mViewDestroyed = true;
// 处理多个dialogFragment的问题
if (mBackStackId >= 0) {
getFragmentManager().popBackStack(mBackStackId,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStackId = -1;
} else {
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(this); // 移除当前的fragment
if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
}
}
}
复制代码
上述代码也说明了dialogFragment是支持回退栈的,若是栈中有就pop出来,若是没有就直接remove和commit。
dismiss()和cancel()的区别:
由于fragment自己就是一个复杂的管理器,不少开发者对于dialogFragment中的各类回调方法会产生理解上的误差,经过下面的图示能够帮助你们更好的理解这点:
public class DemoDialog extends android.support.v4.app.DialogFragment {
private static final String TAG = "DemoDialog";
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获得各类外部参数
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
// 这里返回null,让fragment做为一个controller
return null;
}
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 根据参数创建dialog
return new AlertDialog.Builder(getActivity())
.setTitle("title")
.setMessage("message")
.create();
}
public void setupDialog(Dialog dialog, int style) {
super.setupDialog(dialog, style);
// 上面创建好dialog后,这里能够进行进一步的配置操做(不推荐)
}
public void onStart() {
super.onStart();
// 这里的view来自于onCreateView,因此是null,不要使用
View view = getView();
// 能够在这里进行dialog的findViewById操做
Window window = getDialog().getWindow();
view = window.getDecorView();
}
}
复制代码
图片来源:mmazzarolo/react-native-dialog
如图所示,当咱们自定义的dialog中有一个editText时,咱们天然但愿呼出dialog后能自动弹出输入法,只惋惜原生并不支持这种操做。一个简单的解决方案是在dialogFragment中的onStart()后调用以下代码,强制弹出输入法:
public void showInputMethod(final EditText editText) {
editText.post(new Runnable() {
@Override
public void run() {
editText.setFocusable(true);
editText.setFocusableInTouchMode(true);
editText.requestFocus();
InputMethodManager imm = (InputMethodManager)
getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
}
});
}
复制代码
有的需求是须要从dialogA来弹出dialogB,对于这样的需求咱们须要利用fragment的回退栈来完成。
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(DialogFragmentA.this); // 移除A,防止屏幕上显示两个dialog
ft.addToBackStack(null); // 让A入栈
// ...
// 利用fragmentTransaction来展现B
dialogFragmentB.show(ft,"my_tag");
复制代码
按照如上代码配置后,显示B以前会隐藏A,点击返回键后B会消失,A会再次显示出来。这里须要注意的必须用fragmentTransaction来显示dialog,并且两个dialogFragment的tag不要用同一个值。
在《一个内存泄漏引起的血案》一文中,做者提到局部变量的生命周期在Dalvik VM跟ART/JVM中是有区别的。在DVM中,假如线程死循环或者阻塞,那么线程栈帧中的局部变量若没有被置null,那么就不会被回收。这个设计会致使在lollipop以前使用alertDialog的时候,引发内存泄漏。
当你看到本文的时候android5.0已经成为了主流,能够不用考虑这个问题,但咱们仍旧须要注意下非静态内部类持有外部类的问题。
new AlertDialog.Builder(this)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
}
}).show();
复制代码
在activity中咱们一般会这么写一个dialog,这里的onDismissListener就是一个非静态的匿名内部类。在设置后,alertDialog会将其保存在alertParams中,最终把它设置到dialog对象上。
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
复制代码
创建dialog对象:
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
dialog.setOnCancelListener(P.mOnCancelListener);
// 设置监听器
dialog.setOnDismissListener(P.mOnDismissListener);
return dialog;
}
复制代码
咱们着重关注一下dialog的setOnDismissListener(),这个方法中会将listener做为obj设置给handler来创建一个message,而这个mssage对象会被dialog持有。
public void setOnDismissListener(@Nullable OnDismissListener listener) {
if (listener != null) {
// 设置给handler
mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
} else {
mDismissMessage = null;
}
}
复制代码
下面是两种可能会出现内存泄漏的状况:
若是你的项目在线上遇到了这种问题,能够在dialogFragment的onDestroyView()中置空监听器,或者在dialog被移出window的时候作置空操做。
方法一:
由于dialogFragment自己就fragment,因此这里能够利用fragment的生命周期来作置空操做:
@Override
public void onDestroyView() {
super.onDestroyView();
positiveListener = null;
negativeListener = null;
neutralListener = null;
clickListener = null;
multiChoiceClickListener = null;
}
复制代码
方法二:
为了避免破坏原有的监听器,下面用《Dialog引起的内存泄漏 - 鲍阳的博客》中提到的包装类来解决这个问题。
public final class DetachableClickListener implements DialogInterface.OnClickListener {
public static DetachableClickListener wrap(DialogInterface.OnClickListener delegate) {
return new DetachableClickListener(delegate);
}
private DialogInterface.OnClickListener delegateOrNull;
private DetachableClickListener(DialogInterface.OnClickListener delegate) {
this.delegateOrNull = delegate;
}
public void onClick(DialogInterface dialog, int which) {
if (delegateOrNull != null) {
delegateOrNull.onClick(dialog, which);
}
}
public void clearOnDetach(Dialog dialog) {
dialog.getWindow()
.getDecorView()
.getViewTreeObserver()
.addOnWindowAttachListener(new OnWindowAttachListener() {
public void onWindowAttached() { }
public void onWindowDetached() {
delegateOrNull = null;
}
});
}
}
复制代码
将包装类作真正的监听器对象:
DetachableClickListener clickListener = wrap(new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
MyActivity.this.makeCroissants();
}
});
AlertDialog dialog = new AlertDialog.Builder(this)
.setPositiveButton("Baguette", clickListener)
.create();
clickListener.clearOnDetach(dialog);//监听窗口解除事件,手动释放引用
dialog.show();
复制代码
方法三:
既然咱们发现不少状况是持有message的问题,那么咱们为什么不在handlerThread空闲的时候给队列中发送一个null的message呢,这样就可让其永远不持有dialog中的任何监听了:
static void flushStackLocalLeaks(Looper looper) {
final Handler handler = new Handler(looper);
handler.post(new Runnable() {
@Override
public void run() {
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
handler.sendMessageDelayed(handler.obtainMessage(), 1000);
return true;
}
});
}
});
}
复制代码
默认的dialog是有一个固定的宽的,为了和ui稿保持一致,咱们须要进行一些。咱们能够直接在onStart()中修改window的属性,最终完成自定义的效果。
private void setStyle() {
Window window = getDialog().getWindow();
// 无标题
getDialog().requestWindowFeature(STYLE_NO_TITLE);
// 透明背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
// 设置宽高
window.getDecorView().setPadding(0, 0, 0, 0);
WindowManager.LayoutParams wlp = window.getAttributes();
wlp.width = mWidth;
wlp.height = mHeight;
// 设置dialog出现的位置
wlp.gravity = Gravity.CENTER;
// 设置x、y轴的偏移距离
wlp.x = DensityUtil.dip2px(getDialog().getContext(), mOffsetX);
wlp.y = DensityUtil.dip2px(getDialog().getContext(), mOffsetY);
// 设置显示和关闭时的动画
window.setWindowAnimations(mAnimation);
window.setAttributes(wlp);
}
复制代码
关于背景也是同理,都是对于window的操做:
private void setBackground() {
// 去除dialog的背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable());
// 白色背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0xffffffff));
// 设置主体背景
getDialog().getWindow().setBackgroundDrawableResource(R.drawable.dialog_bg_custom);
}
复制代码
咱们既能够用window.setWindowAnimations(mAnimation)
直接给window设置动画,也能够用style文件来设置动画。
dialog_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromYDelta="100%p"
android:toYDelta="0%p"
android:duration="200"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
复制代码
dialog_out.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromYDelta="0%p"
android:toYDelta="100%p"
android:duration="200"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
复制代码
定义好上述动画文件后,只须要创建一个动画样式,设置给windowAnimationStyle
:
<style name="AlertDialogAnimation">
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<item name="android:windowExitAnimation">@anim/dialog_out</item>
</style>
<!-- 动画 -->
<item name="android:windowAnimationStyle">@style/AlertDialogAnimation</item>
复制代码
题外话:
若是是作自定义的dialog,咱们一般都会将主体背景设置为透明,这样方便作增长圆角和阴影等操做。
Dialog的默认逻辑是点击任何按钮后都会自动关闭,这个默认逻辑对于要作输入校验的场景就不太友好了。咱们能够模拟一个场景:
用户输入文字后点击“ok”,若是输入的文字不符合预期,则弹出toast要求从新输入,不然关闭
在这个场景中,要求“ok”这个button被点击后,不会触发默认的dismiss事件。
要修改默认的逻辑,就要先看看源码中是怎么处理的。AlertController中将底部的三个button都设置了一个叫作mButtonHandler
的clickListener,而mButtonHandler在响应任何事件后都会触发dismiss操做。
private void setupButtons(ViewGroup buttonPanel) {
mButtonPositive = (Button) buttonPanel.findViewById(android.R.id.button1);
mButtonPositive.setOnClickListener(mButtonHandler);
mButtonNegative = buttonPanel.findViewById(android.R.id.button2);
mButtonNegative.setOnClickListener(mButtonHandler);
mButtonNeutral = (Button) buttonPanel.findViewById(android.R.id.button3);
mButtonNeutral.setOnClickListener(mButtonHandler);
}
复制代码
监听器的内部逻辑:
View.OnClickListener mButtonHandler = new View.OnClickListener() {
@Override
public void onClick(View v) {
final Message m;
if (v == mButtonPositive && mButtonPositiveMessage != null) {
// 发送positive事件
m = Message.obtain(mButtonPositiveMessage);
} else if (v == mButtonNegative && mButtonNegativeMessage != null) {
// 发送negative事件
m = Message.obtain(mButtonNegativeMessage);
} else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
// 发送neutral事件
m = Message.obtain(mButtonNeutralMessage);
}
// 任何事件最终都会触发dismiss
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialog)
.sendToTarget();
}
};
复制代码
知晓了原理后,如今的思路就是替换这个listener,将其换成本身的。好比咱们要在positiveButton点击后作一些事情,那么就在dialogFragment的onStart()后拿到当前的dialog对象,经过dialog对象获得这个positiveButton,为其设置本身的监听器。
DialogInterface有三个常量,这三个常量对应三个button对象,而咱们的“ok”就是BUTTON_POSITIVE。
public interface DialogInterface {
/** The identifier for the positive button. */
int BUTTON_POSITIVE = -1;
/** The identifier for the negative button. */
int BUTTON_NEGATIVE = -2;
/** The identifier for the neutral button. */
int BUTTON_NEUTRAL = -3;
}
复制代码
在onStart()中经过getButton(AlertDialog.BUTTON_POSITIVE)就能够获得“ok按钮”:
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
复制代码
最终,从新设置“肯定”按钮的监听器,作自定义的一些逻辑:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(mInputTextEt.getText())) {
Toast.makeText(getActivity(), "请输入内容,不然不能关闭!", Toast.LENGTH_SHORT).show();
} else {
getPositiveListener().onClick(null, AlertDialog.BUTTON_POSITIVE);
dismiss();
}
}
});
复制代码
在有这样需求的场景中,咱们通常都会自定义一个对话框,在这里完成逻辑的封装,最终调用dialogFragment的dismiss()来手动关闭对话框,将控制权紧紧把握在本身的手中。
在线上的崩溃统计中常常会看到dialogFragment的日志:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
众所周知,dialog是动态出现和消失的,而fragment的commit()对于activity状态有着严格的校验,一旦activity在dialog出现时已经走向了finish,那么则必然会崩溃。具体的缘由咱们已经在fragment一章中详细讲解了,也给出了防护的策略,这里就再也不赘述了。
一个很简单的作法是在show()和dismiss()前进行状态判断,也能够顺便包一个try-cache:
public void show(FragmentManager manager, String tag) {
if (manager == null || manager.isDestroyed() || manager.isStateSaved()) {
// 防护:state异常
return;
}
try {
super.show(manager, tag);
} catch (IllegalStateException e) {
// 防护:Can not perform this action after onSaveInstanceState
e.printStackTrace();
}
}
复制代码
换个思路来看,既然这是状态不匹配的问题,那么咱们为什么不直接忽略状态呢,直接自定义一个dialogFragment.commitAllowingStateLoss(),虽然这不是最优的策略。
定义一个showAllowingStateLoss()方法:
public void showAllowingStateLoss(FragmentManager manager) {
manager.beginTransaction().add(this, "kale-dialog").commitAllowingStateLoss();
}
复制代码
其实在android的源码中,google的程序员已经准备好了一个容许忽略状态的show方法了,只不过目前还未暴露,处于hide的阶段。
android.app.DialogFragment:
DialogFragment虽然好处多多,可是其缺失了builder模式,使用起来不是很方便。咱们封装dialogFragment的核心目的就是为其增长一个builder,让使用者能够像用alertDialog那样进行多参数的灵活配置。
封装类应该具有的功能:
既然咱们要仿照alertDialog写一个builder,那么为什么不直接用它的builder呢,毕竟alertDIalog.Builder支持的传参已经彻底够用了。
AlertDialog.Builder的部分设置功能:
alertDialogBuilder.setTitle(); // 设置标题
alertDialogBuilder.setIcon(); // 设置icon
// 设置底部三个操做按钮
alertDialogBuilder.setPositiveButton();
alertDialogBuilder.setNegativeButton();
alertDialogBuilder.setNeutralButton();
setMessage(); // 设置显示的文案
setItems(); // 设置对话框内容为简单列表项
setSingleChoiceItems(); // 设置对话框内容为单选列表项
setMultiChoiceItems(); // 设置对话框内容为多选列表项
setAdapter(); // 设置对话框内容为自定义列表项
setView(); // 设置对话框内容为自定义View
// 设置对话框是否可取消
setCancelable(boolean cancelable);
setCancelListener(onCancelListener);
复制代码
前文说到这个builder会将全部的参数放在一个叫作alertParams
的对象中,那么咱们直接从alertParams中取参数后塞给dialogFragment就好。惋惜的是alertParams自己是public的,但它的外部类alertController倒是私有的,咱们没法访问。
为了解决这个问题,咱们不得不用反射的方式来获得这个public的alertParams(十分少见的反射场景)。为了让反射的代码写起来更加简单,咱们须要作一个public的alertController,让AlertController.AlertParams
写起来不会由于找不到AlertController
这个外部类而报错。
首先,咱们创建一个叫作provided的module,在里面模仿support包写一个本身的public类:
public class AlertController { // 用public修饰
public static class AlertParams {
public Context mContext;
public LayoutInflater mInflater;
public int mIconId = 0;
public Drawable mIcon;
// ...
}
}
复制代码
而后,让工程compileOnly依赖这个provided的module,避免类冲突:
dependencies {
compileOnly project(':provided')
}
复制代码
最后,经过反射来获得系统中的alertParams,也就是P对象:
AlertParams getParams() {
AlertParams P = null;
Field field = AlertDialog.Builder.class.getDeclaredField("P");
field.setAccessible(true);
P = (AlertParams) field.get(this);
return P;
}
复制代码
这样咱们在编写下面代码的时候就不会有任何报错了:
// 由于咱们骗IDE说AlertController是public的,因此它不会出现错误提示
AlertController.AlertParams p = getParams();
// 获得P中具体的值
int iconId = p.mIconId;
Sting title = p.mTitle;
String message = p.mMessage;
复制代码
完成了主体框架后,如今来补充一些传参的细节。咱们但愿dialogFragment能够获得builder的全部参数,但直接传递alertParams对象会有访问权限的问题,因此须要定义一个基础模型类,假设就叫作dialogParams
:
public class DialogParams implements Serializable {
public int mIconId = 0;
public int themeResId;
public CharSequence title;
public CharSequence message;
public CharSequence positiveText;
public CharSequence neutralText;
public CharSequence negativeText;
public CharSequence[] items;
public boolean[] checkedItems;
public boolean isMultiChoice;
public boolean isSingleChoice;
public int checkedItem;
}
复制代码
这个类其实就是alertParams对象的复制,关键是要让其支持序列化,这样才能放入bundle中。其缘由是由于dialogFragment是一个fragment,因此传递的参数必须是可序列化的对象。
若是咱们更进一步,想要更好的支持自定义dialog,让自定义的dialog也能用父类builder中的参数,咱们确定要自定义一个继承自alertDialog.Builder的builder对象,而最好方案就是写一个“支持泛型的builder”。
自定义的builder基类:
public abstract static class Builder<T extends Builder> extends AlertDialog.Builder {
public Builder(@NonNull Context context) {
this(context);
}
@Override
public T setTitle(CharSequence title) {
return (T) super.setTitle(title);
}
@Override
public T setTitle(@StringRes int titleId) {
return (T) super.setTitle(titleId);
}
// ...
@NonNull
protected abstract EasyDialog createDialog(); // 创建子类的具体dialog
}
复制代码
经过泛型,咱们能够简单的实现任意一个dialog的builder,其既可让其拥有本身的新方法,又能够拥有父类的基础方法,想要最大限度利用了继承的能力。惋惜的是alertDialog.Builder自己不支持继承,crate()方法中已经写死了具体的类。
/**
* Calling this method does not display the dialog. If no additional
* processing is needed, {@link #show()} may be called instead to both
* create and display the dialog.
*/
public AlertDialog create() {
// 注意不要用三参数的构造方法来构造aslertDialog
// 这里new出了具体的alertDialog对象,不容许使用者替换实现类
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
P.apply(dialog.mAlert);
dialog.setCancelable(P.mCancelable);
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
复制代码
这里咱们姑且抛弃不可修改的create()方法,新增一个createDialog()
方法,在这个方法中返回子类dialog的对象,真正能实现一个可继承的builder类。
一个自定义的builder的代码,增长了本身的setInputText()方法:
/**
* 自定义builder来增长一些参数,记得要继承自父类(BaseDialog)的Builder
*/
public static class Builder extends BaseDialog.Builder<Builder> {
private Bundle bundle = new Bundle();
public Builder setInputText(CharSequence text, CharSequence hint) {
bundle.putCharSequence(KEY_INPUT_TEXT, text);
bundle.putCharSequence(KEY_INPUT_HINT, hint);
return this;
}
@Override
protected InputDialog createDialog() {
// 关键方法!!!
InputDialog dialog = new InputDialog();
dialog.setArguments(bundle);
return dialog;
}
}
复制代码
如今咱们拥有了以下对象:
因而能够完成总体的流程图:
重要的代码逻辑:
BaseDialog dialog = createDialog(); // 1. 创建一个dialogFragment
AlertParams p = getParams(); // 2. 获得alertParams
DialogParams params = createDialogParamsByAlertParams(p); // 3. 获得dialogParams
Bundle bundle = new Bundle();
bundle.putSerializable(KEY_DIALOG_PARAMS, params);
dialog.setArguments(bundle); // 4. 将dialogParams传入dialogFragment
复制代码
如今咱们的dialogFragment中已经能够获得构建dialog的全部参数了,剩下的就是解析参数和真正创建alertDialog的步骤了:
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getArguments();
if (bundle != null) {
// 1. 获得dialogParams
dialogParams = (DialogParams) bundle.getSerializable(KEY_DIALOG_PARAMS);
}
}
private Dialog onCreateDialog(@NonNull Activity activity) {
DialogParams p = dialogParams;
// 2. 将参数设置到alertDialog的builder中
AlertDialog.Builder builder = new AlertDialog.Builder(activity, p.themeResId)
.setTitle(p.title)
.setIcon(p.mIconId)
.setMessage(p.message)
.setPositiveButton(p.positiveText, positiveListener)
.setNeutralButton(p.neutralText, neutralListener)
.setNegativeButton(p.negativeText, negativeListener);
if (p.items != null) {
if (p.isMultiChoice) {
builder.setMultiChoiceItems(p.items,p.checkedItems,onMultiChoiceClickListener);
} else if (p.isSingleChoice) {
builder.setSingleChoiceItems(p.items, p.checkedItem, onClickListener);
} else {
builder.setItems(p.items, onClickListener);
}
}
return builder.create(); // 3. 创建最终的alertDialog
}
/**
* 4. 这时dialog已经建立完毕,能够调用{@link Dialog#findViewById(int)}了
*/
public void onStart() {
super.onStart();
Window window = getDialog().getWindow();
bindAndSetViews(window != null ? window.getDecorView() : null);
}
复制代码
至此,咱们的封装工做已经基本完毕,这也是tianzhijiexian/EasyDialog的核心思路,下面咱们就来着重看下这个叫作easyDialog库。
简单来讲,tianzhijiexian/EasyDialog仅仅是dialogFragment的简单封装库,它提供了极其原生的api,几乎没有学习成本,而且将自定义dialog的步骤模板化了。在上文中咱们已经了解了它的核心思想,下面就来看看该怎么使用它,而且顺便讲一下你们都会忽略的dialog样式的问题。
EasyDialog充分利用了原生alertDialog.Builder的api,因此使用方式和alertDialog无异,它提供了以下四种基本的dialog。
默认对话框
EasyDialog.Builder builder = EasyDialog.builder(this); // 创建builder对象
builder.setTitle("Title")
.setIcon(R.drawable.saber)
.setMessage(R.string.hello_world)
.setOnCancelListener(dialog -> Log.d(TAG, "onCancel"))
.setOnDismissListener(dialog -> Log.d(TAG, "onDismiss"))
// 设置下方的三个按钮
.setPositiveButton("ok", (dialog, which) -> {})
.setNegativeButton("cancel", (dialog, which) -> dialog.dismiss())
.setNeutralButton("ignore", null)
.setCancelable(true); // 点击空白处能够关闭
DialogFragment easyDialog = builder.build();
// 用showAllowingStateLoss()弹出
easyDialog.showAllowingStateLoss(getSupportFragmentManager());
复制代码
简单列表框
<string-array name="country">
<item>阿尔及利亚</item>
<item>安哥拉</item>
<item>贝宁</item>
<item>缅甸</item>
</string-array>
复制代码
Java代码:
EasyDialog.builder(this)
// R.array.country为xml中定义的string数组
.setItems(R.array.country, (dialog, which) -> showToast("click " + which))
.setPositiveButton("yes", null)
.setNegativeButton("no", null)
.build()
.show(getSupportFragmentManager());
复制代码
单选列表框
EasyDialog dialog = EasyDialog.builder(this)
.setTitle("Single Choice Dialog")
// 这里传入的“1”表示默认选择第二个选项
.setSingleChoiceItems(new String[]{"Android", "ios", "wp"}, 1,
(d, position) -> {d.dismiss();})
.setPositiveButton("ok", null)
.build();
dialog.show(getSupportFragmentManager(), TAG);
复制代码
多选列表框
EasyDialog.builder(this)
// 设置数据和默认选中的选项
.setMultiChoiceItems(
new String[]{"Android", "ios", "wp"}, new boolean[]{true, false, true},
(dialog, which, isChecked) -> showToast("onClick pos = " + which))
.build()
.show(getSupportFragmentManager());
复制代码
在不少场景中咱们都须要自定义本身的dialog,方便使用自定义的布局文件。在easyDialog中,它提供了baseCustomDialog类来让咱们继承,继承后能够看到一个明晰的代码模板:
public class DemoDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return 0; // 返回自定义的layout文件
}
@Override
protected void bindViews(View root) {
// 进行findViewById操做
}
@Override
protected void setViews() {
// 对view或者dialog的window作各类设置
}
}
复制代码
顺便一提,在这个类中咱们能够复写保存、恢复状态的方法,作特殊状况下的数据管理:
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
复制代码
在了解了自定义dialog的基本写法后,下面咱们来看下两种实现方案。
假设ui设计了一个如上图所示的dialog,那么咱们天然要创建以下布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="@drawable/kale"
/>
<TextView
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Yosemite National Park"
/>
</LinearLayout>
复制代码
分析可知,这里须要的是一个“图片资源”和“按钮文案”,对应到alertDialog中就是icon和positiveText。从上文可知,咱们的dialogFragment中能够拿到dialogParams,那么直接从这里取出须要的数据就好,无需自定义一个builder对象。
public class ImageDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return R.layout.custom_dialog_image_layout; // 引入自定义布局
}
@Override
protected void modifyAlertDialogBuilder(AlertDialog.Builder builder) {
super.modifyAlertDialogBuilder(builder);
builder.setPositiveButton(null, null); // 去掉了alerdialog的按钮
}
@Override
protected void bindViews(View root) {
button = root.findViewById(R.id.button);
}
@Override
protected void setViews() {
// 经过getDialogParams()获得外部传入的数据,拿到按钮的文案
button.setText(getDialogParams().positiveText);
button.setOnClickListener(v -> {
// 手动调用外层回调
getPositiveListener().onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
// 关闭对话框
dismiss();
});
}
}
复制代码
展现的方式和上文的dialog并无什么区别,不用修改外部的api:
EasyDialog.builder(this, ImageDialog.class)
.setPositiveButton("弹出动态设置样式的Dialog", (dialog, which) -> {})
.build()
.show(getSupportFragmentManager());
复制代码
在这个case中咱们须要着重看下modifyAlertDialogBuilder()
这个方法,modifyAlertDialogBuilder()容许easyDialog的子类修改创建alerDialog的builder对象,咱们能够复写它来作任何的事情。
@Override
void modifyAlertDialogBuilder(android.support.v7.app.AlertDialog.Builder builder) {
// 它会传入android.support.v7.app.AlertDialog.Builder
}
复制代码
Dialog作的事情必须是简单的展现逻辑,尽可能不要在里面作网络请求等异步操做。当alertDialog.Builder不支持某些数据的时候,咱们就要用到自定义builder了。
首先,定义一个自定义的dialog,好比叫作MyBuilerDialog:
public class MyBuilderDialog extends BaseCustomDialog {
public static final String KEY_AGE = "KEY_AGE", KEY_NAME="KEY_NAME";
@Override
protected int getLayoutResId() {
return 0;
}
@Override
protected void bindViews(View root) {
}
@Override
protected void setViews() {
// 拿到参数,进行展现
int age = getArguments().getInt(KEY_AGE);
Toast.makeText(getContext(), "age: " + age, Toast.LENGTH_SHORT).show();
}
}
复制代码
而后,实现自定义的Builder,创建新的set方法来把数据放入bundle中:
/**
* 继承自{@link EasyDialog.Builder}以扩展builder
*/
public static class Builder extends BaseEasyDialog.Builder<Builder> {
private Bundle bundle = new Bundle();
public Builder(@NonNull Context context) {
super(context);
}
public Builder setAge(int age) {
bundle.putInt(KEY_AGE, age); // 设置年龄
return this;
}
public Builder setName(String name) {
bundle.putString(KEY_NAME, name); // 设置姓名
return this;
}
@Override
protected EasyDialog createDialog() {
// 这里务必记得要new出本身自定义的dialog对象
MyBuilderDialog dialog = new MyBuilderDialog();
// 记得设置本身的bundle数据
dialog.setArguments(bundle);
return dialog;
}
}
复制代码
最后,用传入的数据来作自定义的操做,好比这里替换了alertDialog的message:
@Override
protected void modifyAlertDialogBuilder(AlertDialog.Builder builder) {
super.modifyAlertDialogBuilder(builder);
Bundle arguments = getArguments();
String name = arguments.getString(KEY_NAME); // name
int age = arguments.getInt(KEY_AGE); // age
String str = "name: " + name + ", age: " + age;
// 修改builder对象
builder.setMessage("修改后的message是:\n\n" + str);
}
复制代码
调用方式:
new MyBuilderDialog.Builder(this)
.setTitle("Custom Builder Dialog")
.setMessage("message")
.setName("kale")
.setAge(31)
.build()
.show(getSupportFragmentManager());
复制代码
Design Support Library新加了一个bottomSheets控件,bottomSheets顾名思义就是底部操做控件,用于在屏幕底部建立一个可滑动关闭的视图。BottomSheets必需要配合coordinatorLayout控件使用,也就是说底部对话框布局的父容器必须是coordinatorLayout。
首先,定义一个布局文件,用来承载主体的内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/ll_sheet_root"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"
>
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="内容区域"
android:textSize="30dp"
/>
</LinearLayout>
复制代码
这里有三个属性:
app:behavior_peekHeight="40dp"
app:behavior_hideable="true"
app:layout_behavior="@string/bottom_sheet_behavior"
复制代码
具体的java代码咱们就不展开讲述了,由于easyDialog已经帮咱们处理好了,这里只须要知道要在用design包中的底部对话框的时候必需要有一个coordinatorLayout作容器。
EasyDialog支持了底部对话框的样式,而实现的方式和自定义dialog并没有区别,仍旧是自定义diaog三部曲:
public class BottomDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return R.layout.custom_dialog_layout;
}
@Override
protected void bindViews(View root) {
// ...
}
@Override
protected void setViews() {
TextView textView = findView(R.id.message_tv);
textView.setOnClickListener(view -> {
dismiss();
});
// 获得外部传入的message信息
textView.setText(getDialogParams().message);
}
}
复制代码
为了代表其要从底部弹出,咱们须要在构建的时候增长一个标志位,即setIsBottomDialog(true):
BottomDialog.Builder builder = EasyDialog.builder(this, BottomDialog.class);
builder.setMessage("click me");
builder.setIsBottomDialog(true); // 设置后则会变成从底部弹出,不然为正常模式
builder.build().show(getSupportFragmentManager(), "dialog");
复制代码
这里的setIsBottomDialog()
是关键,baseCustomDialog中的onCreateDialog()会根据标志位进行判断,返回不一样的dialog对象:
public abstract class BaseCustomDialog extends EasyDialog {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (!isBottomDialog()) {
return super.onCreateDialog(savedInstanceState);
} else {
return new BottomSheetDialog(getContext(), getTheme());
}
}
}
复制代码
若是你不喜欢用这种方式,你能够经过设置window的展现位置来作一个底部对话框:
@Override
protected void setViews() {
// 获得屏幕宽度
final DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
// 创建layoutParams
final WindowManager.LayoutParams layoutParams = getDialog().getWindow().getAttributes();
int padding = 10;
layoutParams.width = dm.widthPixels - (padding * 2);
layoutParams.gravity = Gravity.BOTTOM; // 位置在底部
getDialog().getWindow().setAttributes(layoutParams);
}
复制代码
说完了用法,下面咱们来研究下原理。在阅读源码后发现,easyDialog用了support包中提供的bottomSheetDialog
,它就是底部对话框的载体。BottomSheetDialog中已经配置好了bottomSheetBehavior,它还自定义了一个frameLayout容器。
咱们的layoutId会继续被传入wrapInBottomSheet()
,将咱们的布局文件包裹一层父控件,即coordinatorLayout。
@Override
public void setContentView(@LayoutRes int layoutResId) {
super.setContentView(wrapInBottomSheet(layoutResId, null, null));
}
复制代码
wrapInBottomSheet()方法会将咱们的布局文件放入一个frameLayout中,而这个frameLayout则是由coordinatorLayout组成的,也就天然完成了bottomSheetDialog展现的前提。
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
FrameLayout container = (FrameLayout) inflate(getContext(),
R.layout.design_bottom_sheet_dialog, null);
CoordinatorLayout coordinator = container.findViewById(R.id.coordinator);
FrameLayout bottomSheet = coordinator.findViewById(R.id.design_bottom_sheet);
mBehavior = BottomSheetBehavior.from(bottomSheet); // behavior
}
复制代码
design_bottom_sheet_dialog:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/touch_outside"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"/>
<-- 自定义的布局最终会被add到这个frameLayout中 -->
<FrameLayout
android:id="@+id/design_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
app:layout_behavior="@string/bottom_sheet_behavior"
style="?attr/bottomSheetStyle"/>
</android.support.design.widget.CoordinatorLayout>
复制代码
须要注意的是,最下方的frameLayout中已经写死了layout_behavior
属性,google的开发者巧妙的将style
放在了最后一个,即style="?attr/bottomSheetStyle"
,因此咱们能够经过定义style来完成相关的设置,好比:
app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"
复制代码
图片来源:javiersantos/MaterialStyledDialogs
任何一个app都应该在早期定义一套对话框规范,这是极其必要的。不管ui设计的样式多么变幻无穷,咱们都应该用ui和data分离的思路来看问题,上图所示的materialStyledDialogs就是一个很好的效果。
数据方面若是不知足须要,能够经过自定义builder的方式来扩展,ui方面咱们能够创建一个全局的样式,这样全部的对话框文件都会自动套用此样式,不用修改任何逻辑代码,并且还能够利用style的继承作扩展。
假设咱们定义了一个叫作Theme.Dialog
的样式,若是你的项目像materialStyledDialogs那样很符合官方的alerDialog,那么修改以下属性便可:
<style name="Theme.Dialog" parent="Theme.AppCompat.Light.Dialog"> <item name="windowActionBar">false</item> <!-- 有无标题栏 --> <item name="windowNoTitle">true</item> <!-- 对话框的边框,通常不进行设置 --> <item name="android:windowFrame">@null</item> <!-- 是否浮如今activity之上 --> <item name="android:windowIsFloating">true</item> <!-- 是否半透明 --> <item name="android:windowIsTranslucent">true</item> <!-- 决定背景透明度 --> <item name="android:backgroundDimAmount">0.3</item> <!-- 除去title --> <item name="android:windowNoTitle">true</item> <!-- 对话框是否有遮盖 --> <item name="android:windowContentOverlay">@null</item> <!-- 对话框出现时背景是否变暗 --> <item name="android:backgroundDimEnabled">true</item> <!-- 背景颜色,由于windowBackground中的背景已经写死了,因此这里的设置无效 --> <item name="android:colorBackground">#ffffff</item> <!-- 着色缓存(通常不用)--> <item name="android:colorBackgroundCacheHint">@null</item> <!-- 标题的字体样式 --> <item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat </item> <item name="android:windowTitleBackgroundStyle"> @style/Base.DialogWindowTitleBackground.AppCompat </item> <!--对话框背景(重要),默认是@drawable/abc_dialog_material_background --> <item name="android:windowBackground">@drawable/abc_dialog_material_background</item> <!-- 动画 --> <item name="android:windowAnimationStyle">@style/Animation.AppCompat.Dialog</item> <!-- 输入法弹出时自适应 --> <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item> <item name="windowActionModeOverlay">true</item> <!-- 列表部分的内边距,做用于单选、多选列表 --> <item name="listPreferredItemPaddingLeft">20dip</item> <item name="listPreferredItemPaddingRight">24dip</item> <item name="android:listDivider">@null</item> <!-- 单选、多选对话框列表区域文字的颜色 默认是@color/abc_primary_text_material_light --> <item name="textColorAlertDialogListItem">#00ff00</item> <!-- 单选、多选对话框的分割线 --> <!-- dialog中listView的divider 默认是@null --> <item name="listDividerAlertDialog">@drawable/divider</item> <!-- 单选对话框的按钮图标 --> <item name="android:listChoiceIndicatorSingle">@android:drawable/btn_radio</item> <!-- 对话框总体的内边距,不做用于列表部分 默认:@dimen/abc_dialog_padding_material --> <item name="dialogPreferredPadding">20dp</item> <item name="alertDialogCenterButtons">true</item> <!-- 对话框内各个布局的布局文件,默认是@style/Base.AlertDialog.AppCompat --> <item name="alertDialogStyle">@style/Base.AlertDialog.AppCompat</item> </style>
<!-- parent="@style/Theme.AppCompat.Light.Dialog.Alert" -->
<style name="Theme.Dialog.Alert"> <item name="windowMinWidthMajor">@dimen/abc_dialog_min_width_major</item> <item name="windowMinWidthMinor">@dimen/abc_dialog_min_width_minor</item> </style>
复制代码
最后记得在activity的theme中替换本来的alertDialogTheme
属性:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
复制代码
不幸的是,大多数ui人员设计的对话框都和系统的不符,因此咱们仍是须要创建自定义的样式布局,即替换alertDialogStyle
属性中的布局文件:
<style name="Theme.Dialog" parent="Theme.AppCompat.Light.Dialog">
// ...
<!-- 对话框内各个布局的布局文件,默认是@style/Base.AlertDialog.AppCompat -->
<item name="alertDialogStyle">@style/AlertDialogStyle</item>
</style>
<!-- 这里是自定义布局 -->
<style name="AlertDialogStyle" parent="Base.AlertDialog.AppCompat">
<!-- dialog的主体布局文件,里面包含了title,message等控件 -->
<item name="android:layout">@layout/abc_alert_dialog_material</item>
<!-- dialog中的列表布局文件,其实就是listView -->
<item name="listLayout">@layout/abc_select_dialog_material</item>
<!-- dialog中列表的item的布局 -->
<item name="listItemLayout">@layout/select_dialog_item_material</item>
<!-- 多选的item的布局 -->
<item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
<!-- 单选的item的布局 -->
<item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
</style>
复制代码
题外话:
由于android文档对于样式的说明十分有限,并且还存在加“android:”前缀和不加的区别,因此这里必需要贴出整段的配置文件,防止你们用错属性。
修改默认的布局前要了解默认布局是怎么写的,这里建议直接copy系统的布局到项目中,在上面再进行二次修改,这样最不易出错。
关于如何管理自定义布局和自定义样式,有一个小技巧。咱们能够把自定义的全局样式放在一个xml文件中编写,并以common_dialog_或dialog_为前缀,也能够新建一个资源目录来专门存放,只不过千万不要在app的styles.xml文件中直接编写,会让styles.xml文件十分的混乱。
简单列表的Item
select_dialog_item_material.xml:
<TextView
android:id="@android:id/text1"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:textAppearance="?attr/textAppearanceListItemSmall"
android:textColor="?attr/textColorAlertDialogListItem"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
/>
复制代码
简单列表的item就是一个textView,复制后记得要保留id,即android:id="@android:id/text1"
,其他的则能够任意修改。
单选、多选列表的Item
单选和多选列表的item就是一个checkedTextView,一样是须要保留android:id="@android:id/text1"
。
select_dialog_multichoice_material.xml: select_dialog_singlechoice_material.xml:
<CheckedTextView
android:id="@android:id/text1"
android:textColor="?attr/textColorAlertDialogListItem"
android:paddingRight="?attr/dialogPreferredPadding"
android:drawableLeft="?android:attr/listChoiceIndicatorSingle"
/>
复制代码
做为列表框架的ListView
不管是基础的数组列表,仍是单选、多选列表,其容器都是一个viewGroup,系统默认是经过RecycleListView来实现的。这个类其实就是一个listView,它支持经过属性设置padding,没有任何复杂的逻辑。
public static class RecycleListView extends ListView {
private int mPaddingTopNoTitle, mPaddingBottomNoButtons;
public RecycleListView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecycleListView);
mPaddingBottomNoButtons = ta.getDimensionPixelOffset(
R.styleable.RecycleListView_paddingBottomNoButtons, -1);
mPaddingTopNoTitle = ta.getDimensionPixelOffset(
R.styleable.RecycleListView_paddingTopNoTitle, -1);
}
public void setHasDecor(boolean hasTitle, boolean hasButtons) {
if (!hasButtons || !hasTitle) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = hasTitle ? getPaddingTop() : mPaddingTopNoTitle;
final int paddingRight = getPaddingRight();
final int paddingBottom = hasButtons ? getPaddingBottom()
: mPaddingBottomNoButtons;
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
}
}
复制代码
系统默认的布局文件是abc_select_dialog_material.xml,咱们复制后须要保留的是select_dialog_listview
这个id,其他的属性可根据须要来修改。
abc_select_dialog_material.xml:
<!--
This layout file is used by the AlertDialog when displaying a list of items.
This layout file is inflated and used as the ListView to display the items.
Assign an ID so its state will be saved/restored.
-->
<view android:id="@+id/select_dialog_listview"
style="@style/Widget.AppCompat.ListView"
class="android.support.v7.app.AlertController$RecycleListView"
android:divider="?attr/listDividerAlertDialog"
app:paddingBottomNoButtons="@dimen/abc_dialog_list_padding_bottom_no_buttons"
app:paddingTopNoTitle="@dimen/abc_dialog_list_padding_top_no_title"/>
复制代码
Dialog总体的布局框架
abc_alert_dialog_material.xml:
AlertDialog中最重要的就是容器布局了,它决定了dialog的总体外壳,而内部的自定义布局仅仅是它其中的frameLayout区域,该文件涉及到以下三个xml:
由于这里涉及的代码量大,实际又没有难点,你们能够去搜索上述的xml文件来阅读,这里仅仅放出一个自定义后的效果:
能够看到咱们并无修改任何java代码,仅仅经过自定义layout就能够完成符合业务需求的dialog样式,这也是本章的核心思想。
Dialog的背景图片是须要编写的,绝对不是一张简单的png,这个背景图片决定了dialog的外边距,也就是说dialog的外边距不该该经过设置window的宽度来作,而是应该在背景图中静态的定义好。
InsetDrawable是android中一个颇有用的类,它一般会做为dialog的背景图。它能够指定内容的内边距,在dialog中控制的是dialog内容和屏幕四边的距离。
设置边距的属性:
设置上下左右边距的例子:
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="16dp"
android:insetLeft="26dp"
android:insetRight="26dp"
android:insetTop="16dp"
>
<bitmap android:src="@drawable/bg"/>
</inset>
复制代码
下图是用“蒙拉丽莎”作背景的展现效果,左右黑边的宽度就是咱们所设置的26dp:
有不少项目喜欢模仿ios作圆角的dialog,经过insetDrawable和shape的结合,咱们能够很简单的在android上实现ios的效果。
dialog_bg_custom:
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="16dp"
android:insetLeft="26dp"
android:insetRight="26dp"
android:insetTop="16dp"
>
<shape android:shape="rectangle">
<corners android:radius="15dp" />
<solid android:color="@color/dialog_bg_color" />
</shape>
</inset>
复制代码
上方的代码定义了上下边距为16dp,左右边距为26dp的一个圆角背景(用shape画了圆角)。定义好这个背景后,直接将其做用于windowBackground上即可获得ios风格的圆角背景:
<style name="Theme.Dialog.Alert.IOS">
<item name="android:windowBackground">@drawable/dialog_bg_custom</item>
</style>
复制代码
若是你但愿更加灵活的实现圆角布局,用cardView做为容器也是一个很好的思路。又由于cardView自己就是frameLayout,又支持层级的显示,因此它是一个完美的圆角父容器。
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="100dp"
app:cardBackgroundColor="@color/colorPrimary"
app:cardCornerRadius="15dp" // 定义圆角角度
app:cardElevation="0dp" // 去掉阴影
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/bg_mlls"
/>
</android.support.v7.widget.CardView>
复制代码
题外话:
若是你用了一个圆角的shape做为背景,那么dialog中的内容也会被切割成圆角的样式,等于说这个背景就是一个mask,这也是为何源码中用insetDrawable作背景的缘由之一。
若是你的dialog是像上图同样是上部透明,下部规整的样式,你能够考虑用“layer-list + inset”来实现:
上述的代码给背景中增长了一个透明的item,关键是要标记非透明部分的top、bottom等属性,这样才能出效果。
上文讲述的是如何静态的定义全局dialog样式,可是实际项目里面常常会有多种dialog的样式。推荐的作法是将全部的样式都定义出来,在activity的theme中配置主要的样式,将不经常使用的样式用动态的方案设置给dialog。
假设Theme.Dialog.Alert是咱们定义的系统全局样式,Theme.Dialog.Alert.Kale是部分dialog才会用到的样式,咱们把他们都定义好:
<style name="Theme.Dialog.Alert">
<item name="windowMinWidthMajor">@dimen/abc_dialog_min_width_major</item>
<item name="windowMinWidthMinor">@dimen/abc_dialog_min_width_minor</item>
</style>
<style name="Theme.Dialog.Alert.Kale">
<item name="android:windowBackground">@drawable/dialog_bg_custom</item>
</style>
复制代码
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
复制代码
系统默认的样式天然须要定义在AppTheme中了,而不经常使用的样式则是经过java代码来引入的。EasyDialog的builder()中容许咱们传递一个样式的id,也就是说这时构建出的dialog会直接用咱们传入的样式,而非使用静态定义的。
private void showDialog(){
EasyDialog.Builder builder = EasyDialog.builder(getActivity(),
R.style.Theme_Dialog_Alert_Kale); // 自定义样式
builder.setTitle("Dynamic Style Dialog")
.setIcon(R.drawable.kale)
.setMessage("上半部分是透明背景的样式")
.build()
.show(getFragmentManager());
}
复制代码
为了简单起见,咱们通常会用匿名内部类作dialog的监听事件:
// ...
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
}
})
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
复制代码
这样作好处是实现简单,百分之九十以上的状况不会出问题,而坏处是转屏后dialog的各类listener都会变成null。若是你想要保证转屏后dialog的事件不丢,那么必须采用activity来作监听器对象,而且要给easyDialog的builder设置setRetainInstance(true)
。
EasyDialog.builder(this).setRetainInstance(true);
复制代码
设置了这个标志位后,在旋转屏幕时fragment不会被执行onDestroy(),仅仅会执行到onDestroyView(),即不会销毁当前的对象。EasyDialog利用这一特性,在回调函数中作了监听器的保存和恢复操做,保证恢复的时候能让listener和新的activity产生绑定,避免丢失事件。
/**
* 保存参数
*/
@CallSuper
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 若是其中有监听器是activity的对象,那么则保存它
EasyDialogListeners.saveListenersIfActivity(this);
}
/**
* 恢复参数
*/
@CallSuper
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
// 若是发现某个监听器以前是activity对象,那么则用当前新的activity为其赋值
EasyDialogListeners.restoreListenersIfActivity(this, getActivity());
}
/**
* 清理参数
*/
@Override
public void onDestroyView() {
super.onDestroyView();
// 清理监听器的引用,防止持有activity对象,避免引发内存泄漏
EasyDialogListeners.destroyListeners(this);
}
复制代码
这里须要特别注意的是:
onDestroy()
,恢复后的dialogFragment和以前的并不相同若是咱们的app支持帐户踢出的功能,那么在接到后端push的“须要踢出当前用户”的消息后就须要弹出一个dialog。一种方式就是作一个系统层面的dialog,就像ANR时出现的系统dialog同样,让其永远保持在屏幕的上方:
Dialog dialog = new Dialog(this);
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
dialog.show();
复制代码
但这种写法有两个问题,一个是TYPE_SYSTEM_ALERT
已被废弃,其二是须要申请弹窗的权限。
/** @deprecated */
@Deprecated
public static final int TYPE_SYSTEM_ALERT = 2003;
复制代码
申请权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
复制代码
咱们能够换个思路来考虑这个需求,要知道dialog的构建和activity是强相关的,那么直接在application中保存当前的activity对象就好,这样就能够随时使用activity了,也就能够在任什么时候候进行弹窗了。但要记得在锁屏和app退出到后台时,清空保存的activity对象。
首先,让application中持有当前activity的引用:
public class App extends Application {
private AppCompatActivity curActivity;
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
curActivity = (AppCompatActivity) activity;
}
@Override
public void onActivityPaused(Activity activity) {
curActivity = null;
}
});
}
}
复制代码
而后,定义弹出dialog的方法,好比叫做showDialog():
public class App extends Application {
private AppCompatActivity curActivity;
public void showDialog(String title, String message) {
if (curActivity == null) {
return; // 不要忘了判空操做
}
EasyDialog.builder(curActivity)
.setTitle(title)
.setMessage(message)
.setPositiveButton("ok", null)
.build()
.show(curActivity.getSupportFragmentManager());
}
}
复制代码
最后,在须要的时候调用application.showDialog()来完成弹窗:
((App) getApplication()).showDialog("全局弹窗", "可在任意时机弹出一个dialog")
复制代码
这里的代码在onResume()
和onPause()
中作了activity对象的获取和清理,能够保证获取的是当前最上层的activity。此外记得要在弹出时作个activity的判空或isDestroyed()之类的判断,避免使用了即将销毁的activity对象。
题外话:
当你的应用支持了分屏功能,也就是多窗口后,那么则须要在onStart()中获得activity,在onStop()中清空activity,,更多详细的内容请参考《多窗口支持 | Android Developers》。
@Override
public void onActivityStarted(Activity activity) {
curActivity = (AppCompatActivity) activity;
}
@Override
public void onActivityStopped(Activity activity) {
curActivity = null;
}
复制代码
在读完本章后,相信你们能利用原生或者现成的方案来知足ui的需求,不用再杂乱无章的定义各类对话框了。Dialog是一个咱们很经常使用的控件,但它的知识点其实并很多。若是咱们从头思考它,你会发现它涉及fragment、activity的生命周期、windowManager挂载、fragment与activity通讯等等知识点,因此咱们须要更加深刻的了解它,学会它。
其实dialog和dialogFragment的设计算是android源码中的典范了,正由于有如此优秀的设计,咱们才能不断的扩展dialog,产生一个能随着需求复杂度增长而进化的模型,这就是“遇强则强、遇弱则惹”的设计思路。