从源码角度分析实现Dialog点击按钮后界面不自动消失

用过Dialog的朋友都知道,一般我们会在界面上添加一个或几个按钮,如确定取消等,但是不管我们如何设置回调函数,按下按钮时,dialog都会自动消失,有时这个逻辑很方便,省去我们手动调用dismiss的过程。而有些时候,例如我们在让用户输入东西时,在点击button后,我们需要先做判断,当输入非法时要让用户进行修改,这时就不能直接隐藏整个dialog,然后再弹出一个新的窗口来输入,这样的用户体验比较差,今天我们就从源码入手,来试着解决一下这个问题,本文参考的源码基于Android 8.0版本,其余版本可能有细节上差异,但基本逻辑都是大同小异。

我们首先来看一下如何设置button的回调,一般我们会用AlterDialog提供的builder来设置我们Dialog的各项属性,如下

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
             Log.d(TAG,"click");
       }
});

或者调用AlertDialog的setButton方法:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
AlertDialog dialog  = builder.create();
dialog.setButton(AlertDialog.BUTTON_POSITIVE, "click", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
            Log.d(TAG,"click");
       }
});
dialog.show();

事实上,AlertDialog的几个构造方法的访问权限都不是public的,所以我们无法直接new一个对象,只能通过builder间接的创建,所以我们从第一种设置回调的方法入手进行分析,其实二者最后实质上都是一样的,感兴趣的朋友可以翻阅源码。

在使用builder设置完一系列属性之后,我们会调用show()或者create()方法,两个方法在AlertDialog类中,位置如下:

android\frameworks\base\core\java\android\app\AlertDialog.java

方法实现如下:

        public AlertDialog create() {
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

        public AlertDialog show() {
            final AlertDialog dialog = create();
            dialog.show();
            return dialog;
        }

可见show()方法也是先调用create()创建AlertDialog 实例,在进行显示。在create()中,有关键的一步P.apply(dialog.mAlert);这是将我们之前设置的各种属性应用进去,当然也包括button的回调。我们去看看apply这个方法,其中P是AlertParams的一个实例,这个类是AlertController的一个静态内部类。位置如下:

android\frameworks\base\core\java\com\android\internal\app\AlertController.java

apply方法这里就不贴出来了,内容比较简单,就是先判断各项内容是否为空,不为空就赋给AlertController的各个成员变量。其中包括我们要看的设置button回调,我们以PositiveButton为例进行分析,设置的方法如下:

if (mPositiveButtonText != null) {
                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                        mPositiveButtonListener, null);
}

可见源码中是通过判断button内容是否为空来判断我们是否设置了回调的,具体实现调用了AlertController的setButton方法,我们继续看这个方法:

public void setButton(int whichButton, CharSequence text,
            DialogInterface.OnClickListener listener, Message msg) {

        if (msg == null && listener != null) {
            msg = mHandler.obtainMessage(whichButton, listener);
        }

        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");
        }
    }

这个方法的第一个参数是Button的类型,第二个是button的内容,第三个就是我们设置的回调,最后是一个Message对象。关键就是第一个if语句,会通过obtainMessage获取一个Message对象。然后赋给具体的各种Message对象,如mButtonPositiveMessage。

可见直到这一步才最终把我们的回调设置完毕,只不过暂时放在一个Message中保存,既然是Message,就肯定要用到Handler。我们就去看看这个Handler,我们发现mHandler对象是在AlertController的构造中初始化的,有朋友可能要问了,AlertController的构造是在什么时候调用的,还记得之前的apply方法么,调用的时候需要传入一个AlertController对象,这个对象就在AlertDialog构造时产生的。

这个Handler类如下:

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) {

                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();
            }
        }
    }

看到这个类其实就很清楚了,这就是执行回调的地方,至于是谁发送的这个信息,自然是在Button被按下的时候触发的,在源码中有这样一个方法


protected void setupButtons(ViewGroup buttonPanel) {
        ...
        mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1);
        mButtonPositive.setOnClickListener(mButtonHandler);

        if (TextUtils.isEmpty(mButtonPositiveText)) {
            mButtonPositive.setVisibility(View.GONE);
        } else {
            mButtonPositive.setText(mButtonPositiveText);
            mButtonPositive.setVisibility(View.VISIBLE);
            whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
        }
        ...

这里给按钮设置了监听,接口如下:

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();
            }
            mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
                    .sendToTarget();
        }
    };

就这样,具体某个button被按下时,发送对应Message,至于为什么按钮按下后Dialog会自动消失也在这里有了答案,可以看到在OnClickListener 接口中,不但发送了对应的按钮事件,每种按钮都发送了MSG_DISMISS_DIALOG信息,对应到Handler中就是调用dismiss()方法。同时也回答了为什么我们经常在配置取消按钮时,直接传入一个null即可,当回调为null时,不执行任何动作,但最后仍然也会执行dismiss()。

整个设置回调和调用回调都分析完了,我们再来解决我们一开始开始引入的问题,一般而言有两种方法:

第一种是从button的事件监听入手,既然是在OnClickListener 中发送了dismiss信号,我们虽然不能更改源码,但是能重写回调,让他不自动隐藏,在需要隐藏的时候我们手动调用dismiss()即可。实现如下:

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.d(TAG,"click");
            }
        });
        AlertDialog dialog = builder.create();
        dialog.show();
        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG,"Override");
            }
        });

通过log发现只打印了“Override”,没有打印“click”,可见button的回调重写成功了,而且Dialog没有随着button的点击消失。需要注意的时,dialog.getButton必须在dialog.show()之后调用,否则会出错,至于为什么我们稍后再说。

第二种方法我们从dismiss()入手,既然我们无法阻止其自动调用dismiss(),我们看能不能阻止其dismiss()的生效,我们继续看源码,由于AlertDialog的dismiss()方法时调用父类的,我们去父类里看看,位置及实现如下:

android\frameworks\base\core\java\android\app\Dialog.java
    @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();
        }
    }

dismiss()是对外的接口,具体实现在dismissDialog()方法中,在这个方法中,首先会判断dialog的窗口view是否为空,以及是否在显示,前一个参数我们不方便去改动,后一个我们可以在需要他不自动消失时,手动改为false,使这个方法失效即可。但是我们发现mShowing是私有的成员变量,也没有set方法,不过我们可以用反射实现,如下:

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setPositiveButton("click", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.d(TAG,"click");
                Field field = null;
                try {
                    field = dialog.getClass().getSuperclass().getDeclaredField("mShowing" );
                    field.setAccessible( true );
                    field.set(dialog, false);
                    Log.d(TAG,"no dismiss");
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.d(TAG,e.getMessage());
                }

            }
        });
        builder.show();

这样也可以实现点击button后dialog不自动消失。

最后,我们再分析一个问题,刚才整个流程中似乎没有那个地方对button设置了监听,那么到底哪里设置的呢。这里就需要回到AlertDialog源码中,当我们设置完所有属性后,最后调用show()方法显示出dialog时,我们看看源码中到底做了什么,和dismiss()一样,show()也是走的父类方法,其中调用了dispatchOnCreate方法,这个方法里调用了一个抽象方法onCreate,这个方法在子类里实现,AlertDialog实现如下

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAlert.installContent();
    }

发现调用了AlertController的installContent()方法,这个方法很重要,通过源码我们知道,这个方法里初始化了dialog界面的各个控件,设置了如title,button,content等信息(代码比较长,这里就不贴了),总之可以理解为在这里,初始化了界面,当然也初始化了button,如获取对象,设置隐藏或者显示,添加监听等。
这也就回答了为什么我们在重写button事件监听时必须先调用show()方法,否则getButton不能正确拿到button对象,结果导致报空指针异常。

你可能感兴趣的:(从源码角度分析实现Dialog点击按钮后界面不自动消失)