Android 4.x(API 19) TimePickerDialog 中 onTimeSet 回调问题

1.问题发现

什么!!!都0202年的了,竟然还有人在用4.x的系统。
是的这两天接手了一个项目,需要设置一个起止时间,想到系统已有的轮子TimePickerDialog

TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute,boolean is24HourView)

一行代码搞定还蛮简单,接着运行到设备看了下效果:


nobtn.png

竟然没有确定&取消按钮,而是直接在dialog消失时候回调到OnTimeSetListener#onTimeSet方法,虽然可以接收到设置的时间信息。显然这样不太符合常理,不过添加按钮也是有API的

TimePickerDialog#setButton(int whichButton, CharSequence text, OnClickListener listener)

另外有几个重载的方法就不详细介绍了,接着再看下效果:


hasbtn1.png

还算可以,但是没有想到点击 确定|取消|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;

            }
        });

到此全剧终,感谢!

你可能感兴趣的:(Android 4.x(API 19) TimePickerDialog 中 onTimeSet 回调问题)