关于AlertDialog点击按钮弹窗自动dismiss的源码分析及解决方案

关于AlertDialog点击按钮弹窗自动dismiss的源码分析及解决方案

前言:在使用AlertDialog时,发现即使没有调用dismiss()点击取消或者确定按钮对话框都会自动关闭,看了源码才发现,这一操作在源码已经写死了。

源码分析

首先我们先看下AlertDialog的一般用法:

1
2
3
4
5
6
7
8
9
10
11
12
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle("提示")
.setMessage(featureContent)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
AlertDialog dialog = builder.create();
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(true);
dialog.show();

AlertDialog采用Builder进行构造参数的配置,最后通过show()方法返回一个已配置的AlertDialog实例。
从API调用我们可以知道应该先从AlertDialog.Builder入手查起,看看Builder的构造器都做了什么。

1
2
3
4
5
6
7
8
9
public Builder(Context context) {
this(context, resolveDialogTheme(context, 0));
}

public Builder(Context context, int theme) {
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, theme)));
mTheme = theme;
}

接着分析builder.setPositiveButton()方法:

1
2
3
4
5
public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
P.mPositiveButtonText = text;
P.mPositiveButtonListener = listener;
return this;
}

从上面的代码我们可以看出最终的操作都指向了AlertController这个类(AlertController类是Android的内部类,在com.android.internal.app包中,无法通过普通的方式访问,也无法在eclipse通过按Ctrl键跟踪进源代码),在AlertController的构造器中我们看到了一个Hanlder类:

1
2
3
4
5
6
7
  public AlertController(Context context, DialogInterface di, Window window) {
mContext = context;
mDialogInterface = di;
mWindow = window;
mHandler = new ButtonHandler(di);
...
}

再看下ButtonHandler的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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<DialogInterface> mDialog;

public ButtonHandler(DialogInterface dialog) {
mDialog = new WeakReference<DialogInterface>(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();
}
}
}

在这里我们看到了熟悉的dismiss()方法。而在这个类的开头我们就发现了OnClickListener事件的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
View.OnClickListener mButtonHandler = new View.OnClickListener() {
public void onClick(View v) {
Message m = null;
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);
}
if (m != null) {
m.sendToTarget();
}

// Post a message so we dismiss after the above handlers are executed
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
.sendToTarget();
}
};

这段代码的前几行用来触发对话框中的三个按钮,而在最后的代码就是通过mHandler来关闭对话框的。

总结
总结对话框单击事件的处理过程:在AlertController类处理对话框按钮时会为每个按钮添加一个onClick事件,而这个事件的实例就是上面的mButtonHandler,在这个实例中会通过发消息的方式调用为按钮设置的单击事件,在触发完按钮的单击事件后,会通过发消息的方式调用dismiss()来关闭对话框,这就是没有调用dismiss()点击取消或者确定按钮对话框都会自动关闭的原因。

解决方案

  1. 使用我们自己的Hanlder对象来替换掉ButtonHandler就可以阻止AlertDialog调用dismiss()关闭对话框。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private class MyButtonHandler extends Handler{

private WeakReference<DialogInterface> mDialog;

public MyButtonHandler(Dialog dialog) {
mDialog = new WeakReference<DialogInterface>(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;
default:
break;
}

}
}

上面自定义的Handler类和源代码相比只去掉了最后一个case语句。
然后在dialog调用show()方法之前为AlertController类的mHandler变量重新赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
/**
* 通过获取字段,然后设定setAccessible为true,即可访问私有变量
*/
Field field = dialog.getClass().getDeclaredField("mAlert");
field.setAccessible(true);

Object obj = field.get(dialog);
field = obj.getClass().getDeclaredField("mHandler");
field.setAccessible(true);
field.set(obj, new MyButtonHandler(dialog));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
dialog.show();

  1. 从dismiss()方法上做手脚解决。我们可以先看一下dismiss()的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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();
}
}

在dismissDialog()内部会先判断对话框是否关闭(对应mShowing标识位),所以我们可以在代码中设置这个值,使系统认为对话框已经关闭。

1
2
3
4
5
6
7
8
9
10
11
12
try {
Field field = dialog.getClass().getSuperclass().getDeclaredField("mShowing");
field.setAccessible(true);
field.set(dialog, false);
dialog.dismiss();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}