1.问题发现
什么!!!都0202年的了,竟然还有人在用4.x的系统。
是的这两天接手了一个项目,需要设置一个起止时间,想到系统已有的轮子TimePickerDialog
,
TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute,boolean is24HourView)
一行代码搞定还蛮简单,接着运行到设备看了下效果:
竟然没有确定&取消按钮,而是直接在dialog消失时候回调到OnTimeSetListener#onTimeSet
方法,虽然可以接收到设置的时间信息。显然这样不太符合常理,不过添加按钮也是有API的
TimePickerDialog#setButton(int whichButton, CharSequence text, OnClickListener listener)
另外有几个重载的方法就不详细介绍了,接着再看下效果:
还算可以,但是没有想到点击 确定|取消|Dialog外部 都可以接收到OnTimeSetListener#onTimeSet
的回调,我擦这也不合理,不应该啊!这是什么原因呢?于是去查看了下4.4(19)的源码
TimePickerDialog.java
private void tryNotifyTimeSet() {
if (mCallback != null) {
mTimePicker.clearFocus();
mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
mTimePicker.getCurrentMinute());//mCallback就是传进去的OnTimeSetListener
}
}
@Override
protected void onStop() {
tryNotifyTimeSet();
super.onStop();
}
原因很简单,只要dialog消失就调用回调,于是去网上查了下,一致都说是系统版本的bug,那好吧,这个也好办新建一个类继承TimePickerDialog
然后重写onStop方法,大致是这样
MyTimePickerDialog.java
@Override
protected void onStop() {
// super.onStop();
Log.d(TAG, "onStop: MyTimePicker");
//android4.1和4.2存在的一个bug,点击确定和取消按钮时,会出发onTimeSet;
//在dialog的onStop(比如dialog dismiss时)中,也调用了onTimeSet方法
}
很好,继续运行测试效果,震惊竟然 点击 确定|取消|Dialog外部 都不能接收到OnTimeSetListener#onTimeSet
的回调,翻车!!!
这个问题很明显就是setButton对应的没有起到作用的问题,继续查看源码
AlertDialog.java
private AlertController mAlert;
public void setButton(int whichButton, CharSequence text, OnClickListener listener) {
mAlert.setButton(whichButton, text, listener, null);//msg是null
}
//调用到下边setButton
AlertController.java
public void setButton(int whichButton, CharSequence text,
DialogInterface.OnClickListener listener, Message msg) {
if (msg == null && listener != null) {
msg = mHandler.obtainMessage(whichButton, listener); //listener在这里消失了,包装到msg
}
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");
}
}
由于我用的setButton方法没有传入msg参数,所以 (msg == null && listener != null) == true ,不管怎样接下来就是赋值给
mButtonPositiveMessage、mButtonNegativeMessage 、mButtonNeutralMessage(以下简称msg) 三个中的一个,
Android开发肯定都了解Handler机制,了解Handler机制的肯定都知道 msg 最后处理一般都在mHandler类中,那么接下来就是看看
1.这个mHandler在哪里?
2.以及什么什么时候发送的这个msg?
3.在mHandler里边又对msg做了什么操作?
AlertController.java 的内部类
//问题1 :接着看源码可以找到mHandler就是ButtonHandler
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) {
//这里三个类型都是同一个代码
//问题三3. 都是调用了Button设置的点击监听,即我们setButton传入的对象
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();
}
}
}
那么什么时候发送的这个msg呢?
AlertController.java
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(); //问题2:在这里发送的
}:
// Post a message so we dismiss after the above handlers are executed
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialog)
.sendToTarget();
}
};
那么问题又来了mButtonHandler 这又是啥用的,可以看出来是一个View.OnClickListener对象,那无疑就是给确认&取消按钮设置的监听。
到这里可能有点乱懵逼。那就重新理一下思路,我们通过setButton给TimePickerDialog 设置不同的按钮,但其实我们并没有传Button对象进去,只是设置了一些属性、回调,其实在TimePickerDialog创建时已经创建了确认、取消等按钮并为其设置了监听即mButtonHandler,然后通过点击按钮再调用我们传进去的DialogInterface.OnClickListener#onClick()
绕了一圈又回来了,
2.解决问题
问题回溯:我们自定义MyTimePickerDialog
并重写onStop方法
@Override
protected void onStop() {
// super.onStop();
Log.d(TAG, "onStop: MyTimePicker");
//android4.1和4.2存在的一个bug,点击确定和取消按钮时,会出发onTimeSet;在dialog的onStop(比如dialog dismiss时)中,也调用了onTimeSet方法
}
这个时候我们通过setButton设置按钮,并设置监听回调,但是这个时候不能接收onTimeSet回调更新时间了。
解决方法有三个:
1.在setButton的监听回调中手动调用父类TimePickerDialog.onClick
方法;
2.通过反射父类私有的TimePickerDialog.tryNotifyTimeSet
方法,并调用;
timePickerDialog.setButton(DialogInterface.BUTTON_POSITIVE, "确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG, "showTimeSettingDialog#onClick:api " + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { //若适配其他系统版本请看源码
timePickerDialog.onClick(dialog, which); //方法一
}
// try {//这里是解决4.4源码bug ,具体原因请看源码 方法二
// Class clz = Class.forName("android.app.TimePickerDialog");
// Method tryNotifyTimeSetMethod = clz.getDeclaredMethod("tryNotifyTimeSet",null);
// tryNotifyTimeSetMethod.setAccessible(true);
// tryNotifyTimeSetMethod.invoke(timePickerDialog,null);
// } catch (Exception e) {
// Log.i(TAG, "showTimeSettingDialog#onClick: occour an exception:" + e.getMessage());
// e.printStackTrace();
// }
}
});
3.第三种不同上边,不需要自定义MyTimePickerDialog
类,重写其onStop方法,这个情况下我们点击确定、取消、dialog边缘取消 都会接收到onTimeSet的回调,我们只需要定义一个布尔变量doTryNotifyTimeSet来判断是否执行我们需要的逻辑;
private boolean doTryNotifyTimeSet;//变量
private TimePickerDialog.OnTimeSetListener mTimeSetListener = new TimePickerDialog.OnTimeSetListener() {
@Override //android4.1和4.2存在的一个bug,点击确定和取消按钮时,会出发onTimeSet;在dialog的onStop(比如dialog dismiss时)中,也调用了onTimeSet方法
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
Log.i(TAG, String.format("showTimeSettingDialog#onTimeSet: select time >> %d:%d",hourOfDay, minute));
if (doTryNotifyTimeSet) {
//做你想做的事
doTryNotifyTimeSet = false;
}
}
};
//timePickerDialog setButton 会替换监听 onClick 导致 tryNotifyTimeSet >> onTimeSet 无法调用,可以用反射重改下源码 或者重写父类的onClick
timePickerDialog.setButton(DialogInterface.BUTTON_POSITIVE, "确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
doTryNotifyTimeSet = true;
}
});
到此全剧终,感谢!