美文网首页
Android 4.x(API 19) TimePickerDi

Android 4.x(API 19) TimePickerDi

作者: SoldierWIN | 来源:发表于2020-10-27 19:20 被阅读0次

    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<DialogInterface> 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) TimePickerDi

          本文链接:https://www.haomeiwen.com/subject/dddxvktx.html