用过Dialog的朋友都知道,一般我们会在界面上添加一个或几个按钮,如确定取消等,但是不管我们如何设置回调函数,按下按钮时,dialog都会自动消失,有时这个逻辑很方便,省去我们手动调用dismiss的过程。而有些时候,例如我们在让用户输入东西时,在点击button后,我们需要先做判断,当输入非法时要让用户进行修改,这时就不能直接隐藏整个dialog,然后再弹出一个新的窗口来输入,这样的用户体验比较差,今天我们就从源码入手,来试着解决一下这个问题,本文参考的源码基于Android 8.0版本,其余版本可能有细节上差异,但基本逻辑都是大同小异。
我们首先来看一下如何设置button的回调,一般我们会用AlterDialog提供的builder来设置我们Dialog的各项属性,如下
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG,"click");
}
});
或者调用AlertDialog的setButton方法:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
AlertDialog dialog = builder.create();
dialog.setButton(AlertDialog.BUTTON_POSITIVE, "click", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG,"click");
}
});
dialog.show();
事实上,AlertDialog的几个构造方法的访问权限都不是public的,所以我们无法直接new一个对象,只能通过builder间接的创建,所以我们从第一种设置回调的方法入手进行分析,其实二者最后实质上都是一样的,感兴趣的朋友可以翻阅源码。
在使用builder设置完一系列属性之后,我们会调用show()或者create()方法,两个方法在AlertDialog类中,位置如下:
android\frameworks\base\core\java\android\app\AlertDialog.java
方法实现如下:
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
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;
}
public AlertDialog show() {
final AlertDialog dialog = create();
dialog.show();
return dialog;
}
可见show()方法也是先调用create()创建AlertDialog 实例,在进行显示。在create()中,有关键的一步P.apply(dialog.mAlert);这是将我们之前设置的各种属性应用进去,当然也包括button的回调。我们去看看apply这个方法,其中P是AlertParams的一个实例,这个类是AlertController的一个静态内部类。位置如下:
android\frameworks\base\core\java\com\android\internal\app\AlertController.java
apply方法这里就不贴出来了,内容比较简单,就是先判断各项内容是否为空,不为空就赋给AlertController的各个成员变量。其中包括我们要看的设置button回调,我们以PositiveButton为例进行分析,设置的方法如下:
if (mPositiveButtonText != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
mPositiveButtonListener, null);
}
可见源码中是通过判断button内容是否为空来判断我们是否设置了回调的,具体实现调用了AlertController的setButton方法,我们继续看这个方法:
public void setButton(int whichButton, CharSequence text,
DialogInterface.OnClickListener listener, Message msg) {
if (msg == null && listener != null) {
msg = mHandler.obtainMessage(whichButton, listener);
}
switch (whichButton) {
case DialogInterface.BUTTON_POSITIVE:
mButtonPositiveText = text;
mButtonPositiveMessage = msg;
break;
case DialogInterface.BUTTON_NEGATIVE:
mButtonNegativeText = text;
mButtonNegativeMessage = msg;
break;
case DialogInterface.BUTTON_NEUTRAL:
mButtonNeutralText = text;
mButtonNeutralMessage = msg;
break;
default:
throw new IllegalArgumentException("Button does not exist");
}
}
这个方法的第一个参数是Button的类型,第二个是button的内容,第三个就是我们设置的回调,最后是一个Message对象。关键就是第一个if语句,会通过obtainMessage获取一个Message对象。然后赋给具体的各种Message对象,如mButtonPositiveMessage。
可见直到这一步才最终把我们的回调设置完毕,只不过暂时放在一个Message中保存,既然是Message,就肯定要用到Handler。我们就去看看这个Handler,我们发现mHandler对象是在AlertController的构造中初始化的,有朋友可能要问了,AlertController的构造是在什么时候调用的,还记得之前的apply方法么,调用的时候需要传入一个AlertController对象,这个对象就在AlertDialog构造时产生的。
这个Handler类如下:
private static final class ButtonHandler extends Handler {
// Button clicks have Message.what as the BUTTON{1,2,3} constant
private static final int MSG_DISMISS_DIALOG = 1;
private WeakReference mDialog;
public ButtonHandler(DialogInterface dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DialogInterface.BUTTON_POSITIVE:
case DialogInterface.BUTTON_NEGATIVE:
case DialogInterface.BUTTON_NEUTRAL:
((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
break;
case MSG_DISMISS_DIALOG:
((DialogInterface) msg.obj).dismiss();
}
}
}
看到这个类其实就很清楚了,这就是执行回调的地方,至于是谁发送的这个信息,自然是在Button被按下的时候触发的,在源码中有这样一个方法
protected void setupButtons(ViewGroup buttonPanel) {
...
mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1);
mButtonPositive.setOnClickListener(mButtonHandler);
if (TextUtils.isEmpty(mButtonPositiveText)) {
mButtonPositive.setVisibility(View.GONE);
} else {
mButtonPositive.setText(mButtonPositiveText);
mButtonPositive.setVisibility(View.VISIBLE);
whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
}
...
这里给按钮设置了监听,接口如下:
private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
@Override
public void onClick(View v) {
final Message m;
if (v == mButtonPositive && mButtonPositiveMessage != null) {
m = Message.obtain(mButtonPositiveMessage);
} else if (v == mButtonNegative && mButtonNegativeMessage != null) {
m = Message.obtain(mButtonNegativeMessage);
} else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
m = Message.obtain(mButtonNeutralMessage);
} else {
m = null;
}
if (m != null) {
m.sendToTarget();
}
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
.sendToTarget();
}
};
就这样,具体某个button被按下时,发送对应Message,至于为什么按钮按下后Dialog会自动消失也在这里有了答案,可以看到在OnClickListener 接口中,不但发送了对应的按钮事件,每种按钮都发送了MSG_DISMISS_DIALOG信息,对应到Handler中就是调用dismiss()方法。同时也回答了为什么我们经常在配置取消按钮时,直接传入一个null即可,当回调为null时,不执行任何动作,但最后仍然也会执行dismiss()。
整个设置回调和调用回调都分析完了,我们再来解决我们一开始开始引入的问题,一般而言有两种方法:
第一种是从button的事件监听入手,既然是在OnClickListener 中发送了dismiss信号,我们虽然不能更改源码,但是能重写回调,让他不自动隐藏,在需要隐藏的时候我们手动调用dismiss()即可。实现如下:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG,"click");
}
});
AlertDialog dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG,"Override");
}
});
通过log发现只打印了“Override”,没有打印“click”,可见button的回调重写成功了,而且Dialog没有随着button的点击消失。需要注意的时,dialog.getButton必须在dialog.show()之后调用,否则会出错,至于为什么我们稍后再说。
第二种方法我们从dismiss()入手,既然我们无法阻止其自动调用dismiss(),我们看能不能阻止其dismiss()的生效,我们继续看源码,由于AlertDialog的dismiss()方法时调用父类的,我们去父类里看看,位置及实现如下:
android\frameworks\base\core\java\android\app\Dialog.java
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
dismiss()是对外的接口,具体实现在dismissDialog()方法中,在这个方法中,首先会判断dialog的窗口view是否为空,以及是否在显示,前一个参数我们不方便去改动,后一个我们可以在需要他不自动消失时,手动改为false,使这个方法失效即可。但是我们发现mShowing是私有的成员变量,也没有set方法,不过我们可以用反射实现,如下:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG,"click");
Field field = null;
try {
field = dialog.getClass().getSuperclass().getDeclaredField("mShowing" );
field.setAccessible( true );
field.set(dialog, false);
Log.d(TAG,"no dismiss");
} catch (Exception e) {
e.printStackTrace();
Log.d(TAG,e.getMessage());
}
}
});
builder.show();
这样也可以实现点击button后dialog不自动消失。
最后,我们再分析一个问题,刚才整个流程中似乎没有那个地方对button设置了监听,那么到底哪里设置的呢。这里就需要回到AlertDialog源码中,当我们设置完所有属性后,最后调用show()方法显示出dialog时,我们看看源码中到底做了什么,和dismiss()一样,show()也是走的父类方法,其中调用了dispatchOnCreate方法,这个方法里调用了一个抽象方法onCreate,这个方法在子类里实现,AlertDialog实现如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
}
发现调用了AlertController的installContent()方法,这个方法很重要,通过源码我们知道,这个方法里初始化了dialog界面的各个控件,设置了如title,button,content等信息(代码比较长,这里就不贴了),总之可以理解为在这里,初始化了界面,当然也初始化了button,如获取对象,设置隐藏或者显示,添加监听等。
这也就回答了为什么我们在重写button事件监听时必须先调用show()方法,否则getButton不能正确拿到button对象,结果导致报空指针异常。