AlertDialog点击确定必定会dismiss问题
场景:
对于业务处理,需要在点击AlertDialog的确定按钮之后做判断,如果满足要求,弹窗消失,反之,在弹窗上做特殊的错误处理,弹窗不消失。
首先,弹窗部分使用原生的AlertDialog.Builder来构建的。因此,最开始我觉得这个功能只需要设置对应确定按钮即可–setPositiveButton(text, positiveClickListener);
positiveClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 逻辑判断
........
if (条件不满足) {
return;
}
dialog.dismiss();
}
}
最开始的理解,是AlertDialog需要在点击确定的时候手动dismiss掉。所以在调用dialog.dismiss()之前进行逻辑判断,不满足条件在dismiss之前返回即可。但是后续实际测试时发现,不论何时return,只要点击了确定按钮,dialog都会dismiss。
解决方案:
先说明下解决方案:
网络上有很多说用放射的方式,实际上没有必要,只要拿到对应的弹窗确定按钮,然后手动设置点击响应即可(注意,需要在Dialog show之后进行设置);
// dialog 未对应AlertDialog实例对象;
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
// 逻辑确认
........
if (不满足条件) {
return;
}
dialog.dismiss();
}
原理解析:
从现象上看:我们对于DialogInterface.OnClickListener这个监听的设置不生效,所以得从源码看下:
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setView(dialogLayout)
.setCancelable(cancelable)
.setPositiveButton(positiveText, onPositiveClickListener)
.setNegativeButton(negativeText, onNegativeClickListener);
通常来说,对应AlertDialog的创建如上所示,使用Builder进行构建时,会先设置参数:
public Builder(@NonNull Context context, @StyleRes int themeResId) {
this.P = new AlertParams(new ContextThemeWrapper(context, AlertDialog.resolveDialogTheme(context, themeResId)));
this.mTheme = themeResId;
}
public AlertDialog.Builder setView(View view) {
this.P.mView = view;
this.P.mViewLayoutResId = 0;
this.P.mViewSpacingSpecified = false;
return this;
}
public AlertDialog.Builder setPositiveButton(CharSequence text, OnClickListener listener) {
this.P.mPositiveButtonText = text;
this.P.mPositiveButtonListener = listener;
return this;
}
通过Builder进行构造,实际上只是将数据存储在其AlertParams中,后面实际起作用是在Builder.create()或Builder.show():
public AlertDialog show() {
AlertDialog dialog = this.create();
dialog.show();
return dialog;
}
public AlertDialog create() {
AlertDialog dialog = new AlertDialog(this.P.mContext, this.mTheme);
this.P.apply(dialog.mAlert);
dialog.setCancelable(this.P.mCancelable);
if (this.P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(this.P.mOnCancelListener);
dialog.setOnDismissListener(this.P.mOnDismissListener);
if (this.P.mOnKeyListener != null) {
dialog.setOnKeyListener(this.P.mOnKeyListener);
}
return dialog;
}
实际上对于Builder.show会在内部调用create,先创建一个AlertDialog对象,然后调用Dialog的show方法进行展示。
在create()方法中,首先创建一个AlertDialog对象,然后调用AlertParams.apply(),后面都是对Dialog监听的设置,这里没有涉及到确定/取消按钮的,所以先不深入;
在apply()中会传递一个AlertControl对象,它是在AlertDialog构造函数中被创建的:
protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, resolveDialogTheme(context, themeResId));
this.mAlert = new AlertController(this.getContext(), this, this.getWindow());
}
public void apply(AlertController dialog) {
........
if (this.mMessage != null) {
dialog.setMessage(this.mMessage);
}
if (this.mPositiveButtonText != null || this.mPositiveButtonIcon != null) {
dialog.setButton(-1, this.mPositiveButtonText, this.mPositiveButtonListener, (Message)null, this.mPositiveButtonIcon);
}
if (this.mNegativeButtonText != null || this.mNegativeButtonIcon != null) {
dialog.setButton(-2, this.mNegativeButtonText, this.mNegativeButtonListener, (Message)null, this.mNegativeButtonIcon);
}
if (this.mNeutralButtonText != null || this.mNeutralButtonIcon != null) {
dialog.setButton(-3, this.mNeutralButtonText, this.mNeutralButtonListener, (Message)null, this.mNeutralButtonIcon);
}
if (this.mItems != null || this.mCursor != null || this.mAdapter != null) {
this.createListView(dialog);
}
........
}
只看相关部分,也就是dialog.setButton(-1, this.mPositiveBu…);部分
public void setButton(int whichButton, CharSequence text, android.content.DialogInterface.OnClickListener listener, Message msg, Drawable icon) {
if (msg == null && listener != null) {
// 这里的mHandler实际上是在AlertController构造中创建的
// this.mHandler = new AlertController.ButtonHandler(di);
// 传递了一个DialogInterface对象进去,
msg = this.mHandler.obtainMessage(whichButton, listener);
}
switch(whichButton) {
case -3:
this.mButtonNeutralText = text;
this.mButtonNeutralMessage = msg;
this.mButtonNeutralIcon = icon;
break;
case -2:
this.mButtonNegativeText = text;
this.mButtonNegativeMessage = msg;
this.mButtonNegativeIcon = icon;
break;
case -1:
// 将前面构建的Message赋值给AlertController的mButtonPositiveMessage字段
// 对应的确定按钮监听放在message中。
this.mButtonPositiveText = text;
this.mButtonPositiveMessage = msg;
this.mButtonPositiveIcon = icon;
break;
default:
throw new IllegalArgumentException("Button does not exist");
}
}
到这,实际上AlertDialog的创建就完成了,我们还没明白为什么弹窗一定会消失,只能说这个确定按钮的监听在Message中,后面肯定是通过handler机制进行消息传递的。
接着看,Dialog.show()方法:
// Dialog.java中代码
public void show() {
........
if (!mCreated) {
dispatchOnCreate(null);
} else {
........
}
void dispatchOnCreate(Bundle savedInstanceState) {
if (!mCreated) {
onCreate(savedInstanceState);
mCreated = true;
}
}
protected void onCreate(Bundle savedInstanceState) {
}
在show()中,第一次创建的时候会调用dispatchOnCreate()->onCreate()但是,在Dialog中onCreate的实现为空,所以它应该在子类中有Override:
// AlertDialog extends Dialog
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// mAlert是一个AlertController对象
mAlert.installContent();
}
// AlertController.java
public void installContent() {
int contentView = this.selectContentView();
this.mDialog.setContentView(contentView);
this.setupView(); // 相关按钮代码在setupView中
}
private void setupButtons(ViewGroup buttonPanel) {
int BIT_BUTTON_POSITIVE = true;
int BIT_BUTTON_NEGATIVE = true;
int BIT_BUTTON_NEUTRAL = true;
int whichButtons = false;
this.mButtonPositive = (Button)buttonPanel.findViewById(16908313);
this.mButtonPositive.setOnClickListener(this.mButtonHandler);
........
}
setupView()->setupButtons();在设置按钮方法张,先从ViewGroup中拿到对应的确认按钮,然后设置按钮监听,这里传递进去的是一个OnClickListener对象,在AlertController的全局字段中:
Handler mHandler;
private final OnClickListener mButtonHandler = new OnClickListener() {
public void onClick(View v) {
Message m;
if (v == AlertController.this.mButtonPositive && AlertController.this.mButtonPositiveMessage != null) {
m = Message.obtain(AlertController.this.mButtonPositiveMessage);
} else if (v == AlertController.this.mButtonNegative && AlertController.this.mButtonNegativeMessage != null) {
m = Message.obtain(AlertController.this.mButtonNegativeMessage);
} else if (v == AlertController.this.mButtonNeutral && AlertController.this.mButtonNeutralMessage != null) {
m = Message.obtain(AlertController.this.mButtonNeutralMessage);
} else {
m = null;
}
if (m != null) {
m.sendToTarget();
}
AlertController.this.mHandler.obtainMessage(1, AlertController.this.mDialog).sendToTarget();
}
};
所以,当我们点击确认按钮时,会调用mButtonHandler这个监听,判断时mButtonPositive这个View时,从AlertController中拿mButtonPositiveMessage这个变量,它是在setButton中被赋值的,其中的msg.obj就是我们在AlertDialog.Builder.setPositiveButton(listener)中传递进来的一个DialogInterface.OnClickListener对象。
拿到消息后,先调用Message.sendToTarget,根据Handler-Message的消息机制,发送消息后,对应处理消息回调在Handler中,而这里的Handler就是mHandler:this.mHandler = new AlertController.ButtonHandler(di);
private static final class ButtonHandler extends Handler {
private static final int MSG_DISMISS_DIALOG = 1;
private WeakReference<DialogInterface> mDialog;
public ButtonHandler(DialogInterface dialog) {
this.mDialog = new WeakReference(dialog);
}
public void handleMessage(Message msg) {
switch(msg.what) {
case -3:
case -2:
case -1:
((android.content.DialogInterface.OnClickListener)msg.obj).onClick((DialogInterface)this.mDialog.get(), msg.what);
case 0:
default:
break;
case 1:
((DialogInterface)msg.obj).dismiss();
}
}
}
所以在处理消息逻辑中,先是从msg中拿出之前保存的DialogInterface.OnClickListener对象,然后调用其onClick,也就是执行我们自己写的确认回调;
到此位置,点击确认按钮源码流程就理解通了,但是,为什么会消失呢?
我们回到之前的按钮的监听流程中:
if (m != null) {
m.sendToTarget();
}
AlertController.this.mHandler.obtainMessage(1, AlertController.this.mDialog).sendToTarget();
在调用m.sendToTarget()执行自定义监听之后,还会发送一个消息,其中msgWhat = 1;msg.obj = AppCompatDialog;
所以就算我们之前自定义监听中,判断不满足条件,直接return之后,也会执行到ButtonHandler.handleMessage中的case 1,所以必定会走Dialog.dismiss();
而我们做的,就是在AlertDialog显示之后,通过getButton拿到确认按钮实例,然后自行设置OnClickListener,覆盖掉之前在Dialog::show()->Dialog::dispatchOnCreate()->AlertDialog::onCreate()->AlertController::installContent()->AlertController::setupView()->AlertController::setupButtons()->mButtonPositive.setOnClickListener();中设置的mButtonHandler。将这个监听覆盖掉之后,就不会走后续DialogInterface.OnClickListener监听的流程了,而是直接走的View.OnClickListener流程;
所以,上面提到的“必须在弹窗show()之后进行监听设置”,也是因为只有调用show之后,才能获取到对应的窗口中Buttom实例(此时View被从xml中解析,并实例化),并且在此之后,我们设置的监听才能覆盖掉原有show流程中设置的监听。