美文网首页
AlertDialog 详解

AlertDialog 详解

作者: 一航jason | 来源:发表于2017-09-12 17:22 被阅读0次

    AlertDialog是经常使用控件,今天我来分析一下源码。

    android新手经常会用遇到几个问题?

    • 窗体溢出的bug;这个可以参照前面的《android Context 详解》。
    • dialog为什么只接受activity的context;这个问题下面就可以得到答案。

    我们分析了Activity的布局文件加载、绘制流程,算是对整个Android系统中界面的显示流程有了一个大概的了解,其实Android系统中所有的显示控件(注意这里是控件,而不是组件)的加载绘制流程都是类似的,包括:Dialog的加载绘制流程,PopupWindow的加载绘制流程,Toast的显示原理等,上一篇文章中,我说在介绍了Activity界面的加载绘制流程之后,就会分析一下剩余几个控件的显示控制流程,这里我打算先分析一下Dialog的加载绘制流程。

    可能有的同学问这里为什么没有Fragment?其实严格意义上来说Fragment并不是一个显示控件,而只是一个显示组件。为什么这么说呢?其实像我们的Activity,Dialog,PopupWindow以及Toast类的内部都管理维护着一个Window对象,这个Window对象不但是一个View组件的集合管理对象,它也实现了组件的加载与绘制流程,而我们的Fragment组件如果看过源码的话,严格意义上来说,只是一个View组件的集合并通过控制变量实现了其特定的生命周期,但是其由于并没有维护Window类型的成员变量,所以其不具备组件的加载与绘制功能,因此其不能单独的被绘制出来,这也是我把它称之为组件而不是控件的原因。(在分析完这几个控件的加载绘制流程之后,有时间的话,也会分析一下Fragment的相关源码)

    好吧,开始我们今天关于Dialog的讲解,相信大家在平时的开发过程中经常会使用到Dialog弹窗,使用Dialog可以在Activity弹出弹窗,确认消息等。为了更好的分析Dialog的源码,我们这里暂时写一个简单的demo,看一下Dialog的使用实例。

    title.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setIcon(R.mipmap.ic_launcher);
                    builder.setMessage("this is the content view!!!");
                    builder.setTitle("this is the title view!!!");
                    builder.setView(R.layout.activity_second);
                    builder.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            alertDialog.dismiss();
                        }
                    });
                    alertDialog = builder.create();
                    alertDialog.show();
                }
            });
    

    我们在Activity中获取一个textView组件,并监听TextView的点击事件,并在点击事件中,初始化一个AlertDialog弹窗,并执行AlertDialog的show方法展示弹窗,在弹窗中定义一个按钮,并监听弹窗按钮的点击事件,若用户点击了弹窗的按钮,则执行AlertDialog的dismiss方法,取消展示AlertDialog。

    可以看到我们定义的icon,title,message和button都已经显示出来了,这时候我们点击弹窗按钮知道了,这时候弹窗就会消失了。

    一般我们使用Dialog的大概流程都是这样的,可能定制Dialog的时候有一些定制化的操作,但是基本操作流程还是这样的。

    那么我们先来看一下AlertDialog.Builder的构造方法,这里的Builder是AlertDialog的内部类,用于封装AlertDialog的构造过程,看一下Builder的构造方法:

    public Builder(Context context) {
                this(context, resolveDialogTheme(context, 0));
            }
    

    这里调用的是Builder的重载构造方法:

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

    那么这里的P是AlertDialog.Builder中的一个AlertController.AlertParams类型的成员变量,可见在这里执行了P的初始化操作。

    public AlertParams(Context context) {
                mContext = context;
                mCancelable = true;
                mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            }
    

    可以看到这里主要执行了AlertController.AlertParams的初始化操作,初始化了一些成员变量。这样执行了一系列操作之后我们的代码:

    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
    

    就已经执行完成了,然后我们调用了builder.setIcon方法,这里看一下setIcon方法的具体实现:

    public Builder setIcon(@DrawableRes int iconId) {
                P.mIconId = iconId;
                return this;
            }
    

    可以看到AlertDialog的Builder的setIcon方法,这里执行的就是给类型为AlertController.AlertParams的P的mIconId赋值为传递的iconId,并且这个方法返回的类型就是Builder。

    然后我们调用了builder.setMessage方法,可以看一下builder.setMessage方法的具体实现:

    public Builder setMessage(CharSequence message) {
                P.mMessage = message;
                return this;
            }
    

    好吧,这里跟setIcon方法的实现逻辑类似,都是给成员变量的mMessage赋值为我们传递的Message值,且和setIcon方法类似的,这个方法返回值也是Builder。

    再看一下builder.setTitle方法:

    public Builder setTitle(CharSequence title) {
                P.mTitle = title;
                return this;
            }
    

    可以发现builder的setIcon、setMessage、setTitle等方法都是给Builder的成员变量P的icon,message,title赋值。

    然后我们看一下builder.setView方法:

    public Builder setView(int layoutResId) {
                P.mView = null;
                P.mViewLayoutResId = layoutResId;
                P.mViewSpacingSpecified = false;
                return this;
            }
    

    可以发现这里的setView和setIcon,setMessage,setTitle等方法都是类似的,都是将我们传递的数据值赋值给Builder的成员变量P。

    然后我们调用了builder.setPositiveButton方法:

    builder.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            alertDialog.dismiss();
                        }
                    });
    

    这里我们看一下builder的setPositiveButton的源码:

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

    好吧,可以发现跟上面几个方法还是类似的,都是为Builder的成员变量P的相应成员变量赋值。。。

    上面使用的链式编程,不仅代码简单而且清晰,平时可以多使用这种方式。

    然后我们调用了builder.create方法,并且这个方法返回了AlertDialog。

    public AlertDialog create() {
                // Context has already been wrapped with the appropriate theme.
                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;
            }
    

    可以看到这里首先构造了一个AlertDialog,我们可以看一下这个构造方法的具体实现:

    AlertDialog(Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0,
                    createContextThemeWrapper);
    
            mWindow.alwaysReadCloseOnTouchAttr();
            mAlert = new AlertController(getContext(), this, getWindow());
        }
    

    可以看到这里首先调用了super的构造方法,而我们的AlertDialog继承于Dialog,所以这里执行的就是Dialog的构造方法,好吧,继续看一下Dialog的构造方法:

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            if (createContextThemeWrapper) {
                if (themeResId == 0) {
                    final TypedValue outValue = new TypedValue();
                    context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                    themeResId = outValue.resourceId;
                }
                mContext = new ContextThemeWrapper(context, themeResId);
            } else {
                mContext = context;
            }
    
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            w.setCallback(this);
            w.setOnWindowDismissedCallback(this);
            w.setWindowManager(mWindowManager, null, null);
            w.setGravity(Gravity.CENTER);
    
            mListenersHandler = new ListenersHandler(this);
        }
    

    可以发现在Dialog的构造方法中直接直接构造了一个PhoneWindow,并赋值给Dialog的成员变量mWindow,从这里可以看出其实Dialog和Activity的显示逻辑都是类似的,都是通过对应的Window变量来实现窗口的加载与显示的。然后我们执行了一些Window对象的初始化操作,比如设置回调函数为本身,然后调用了Window类的setWindowManager方法,并传入了WindowManager,可以发现这里的WindowManager对象是通过方法:

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    

    获取的,而我们的context传入的是Activity对象,所以这里的WindowManager对象其实和Activity获取的WindowManager对象是一致的。然后我们看一下window类的setWindowManager方法:

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
                boolean hardwareAccelerated) {
            mAppToken = appToken;
            mAppName = appName;
            mHardwareAccelerated = hardwareAccelerated
                    || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
            if (wm == null) {
                wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            }
            mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
        }
    

    可以看到跟Activity的Window对象的windowManager的获取方式是相同的,都是通过new的方式创建一个新的WindowManagerImpl对象。好吧,继续回到我们的AlertDialog的构造方法中,在构造方法中,我们除了调用Dialog的构造方法之外还执行了:

    mAlert = new AlertController(getContext(), this, getWindow());
    

    相当于初始化了AlertDiaog的成员变量mAlert。

    继续回到我们的AlertDialog.Builder.create方法,在创建了一个AlertDialog之后,又执行了P.apply(dialog.mAlert);
    我们知道这里的P是一个AlertController.AlertParams的变量,而dialog.mAlert是我们刚刚创建的AlertDialog中的一个AlertController类型的变量,我们来看一下apply方法的具体实现:

    public void apply(AlertController dialog) {
                if (mCustomTitleView != null) {
                    dialog.setCustomTitle(mCustomTitleView);
                } else {
                    if (mTitle != null) {
                        dialog.setTitle(mTitle);
                    }
                    if (mIcon != null) {
                        dialog.setIcon(mIcon);
                    }
                    if (mIconId != 0) {
                        dialog.setIcon(mIconId);
                    }
                    if (mIconAttrId != 0) {
                        dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                    }
                }
                if (mMessage != null) {
                    dialog.setMessage(mMessage);
                }
                if (mPositiveButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                            mPositiveButtonListener, null);
                }
                if (mNegativeButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                            mNegativeButtonListener, null);
                }
                if (mNeutralButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                            mNeutralButtonListener, null);
                }
                if (mForceInverseBackground) {
                    dialog.setInverseBackgroundForced(true);
                }
                // For a list, the client can either supply an array of items or an
                // adapter or a cursor
                if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
                    createListView(dialog);
                }
                if (mView != null) {
                    if (mViewSpacingSpecified) {
                        dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                                mViewSpacingBottom);
                    } else {
                        dialog.setView(mView);
                    }
                } else if (mViewLayoutResId != 0) {
                    dialog.setView(mViewLayoutResId);
                }
            }
    

    看到了么?就是我们在初始化AlertDialog.Builder的时候设置的icon、title、message赋值给了AlertController.AlertParams,这里就是将我们初始化时候设置的属性值赋值给我们创建的Dialog对象的mAlert成员变量。。。。

    继续我们的AlertDialog.Builder.create方法,在执行了AlertController.AlertParams.apply方法之后又调用了:

    dialog.setCancelable(P.mCancelable);
    

    可以发现这个也是AertController.AlertParams的一个成员变量,我们在初始化AlertDialog.Builder的时候也可以通过设置builder.setCancelable赋值,由于该属性为成员变量,所以默认值为false,而我们并没有通过builder.setCancelable修改这个属性值,所以这里设置的dialog的cancelable的值为false。然后我们的create方法有设置了dialog的cancelListener和dismissListener并返回了我们创建的Dialog对象。这样我们就获取到了我们的Dialog对象,然后我们调用了dialog的show方法用于显示dialog,好吧,这里我们看一下show方法的具体实现:

    public void show() {
            if (mShowing) {
                if (mDecor != null) {
                    if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                        mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                    }
                    mDecor.setVisibility(View.VISIBLE);
                }
                return;
            }
    
            mCanceled = false;
    
            if (!mCreated) {
                dispatchOnCreate(null);
            }
    
            onStart();
            mDecor = mWindow.getDecorView();
    
            if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                final ApplicationInfo info = mContext.getApplicationInfo();
                mWindow.setDefaultIcon(info.icon);
                mWindow.setDefaultLogo(info.logo);
                mActionBar = new WindowDecorActionBar(this);
            }
    
            WindowManager.LayoutParams l = mWindow.getAttributes();
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
                WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
                nl.copyFrom(l);
                nl.softInputMode |=
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                l = nl;
            }
    
            try {
                mWindowManager.addView(mDecor, l);
                mShowing = true;
    
                sendShowMessage();
            } finally {
            }
        }
    

    方法体的内容比较多,我们慢慢看,由于一开始mShowing变量用于表示当前dialog是否正在显示,由于我们刚刚开始调用执行show方法,所以这里的mShowing变量的值为false,所以if分支的内容不会被执行,继续往下看:

    if (!mCreated) {
                dispatchOnCreate(null);
            }
    

    mCreated这个控制变量控制dispatchOnCreate方法只被执行一次,由于我们是第一次执行,所以这里会执行dispatchOnCreate方法,好吧,我们看一下dispatchOnCreate方法的执行逻辑:

    void dispatchOnCreate(Bundle savedInstanceState) {
            if (!mCreated) {
                onCreate(savedInstanceState);
                mCreated = true;
            }
        }
    

    好吧,可以看到代码的执行逻辑很简单就是回调了Dialog的onCreate方法,那么onCreate方法内部又执行了那些逻辑呢?由于我们创建的是AlertDialog对象,该对象继承于Dialog,所以我们这时候需要看一下AlertDialog的onCreate方法的执行逻辑:

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

    可以看到这里面除了调用super.onCreate方法之外就是调用了mAlert.installContent方法,而这里的super.onCreate方法就是调用的Dialog的onCreate方法,Dialog的onCreate方法只是一个空的实现逻辑,所以我们具体来看一下mAlert.installContent的实现逻辑。

    public void installContent() {
            /* We use a custom title so never request a window title */
            mWindow.requestFeature(Window.FEATURE_NO_TITLE);
            int contentView = selectContentView();
            mWindow.setContentView(contentView);
            setupView();
            setupDecor();
        }
    

    可以看到这里实现Window窗口的页面设置布局初始化等操作,这里设置了mWindow对象为NO_TITLE,然后通过调用selectContentView设置Window对象的布局文件。

    private int selectContentView() {
            if (mButtonPanelSideLayout == 0) {
                return mAlertDialogLayout;
            }
            if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
                return mButtonPanelSideLayout;
            }
            // TODO: use layout hint side for long messages/lists
            return mAlertDialogLayout;
        }
    

    可以看到这里通过执行selectContentView方法返回布局文件的id值,这里的默认值是mAlertDialogLayout。从这个方法开始我们就把指定布局文件的内容加载到内存中的Window对象中。我们这里看一下具体的布局文件。

    mAlertDialogLayout = a.getResourceId(
                    R.styleable.AlertDialog_layout, R.layout.alert_dialog);
    

    也就是R.layout.alert_dialog的布局文件,有兴趣的同学可以看一下该布局文件的源码.
    继续回到我们的installContent方法,在执行了mWindow.setContentView方法之后,又调用了setupView方法和setupDector方法,这两个方法的主要作用就是初始化布局文件中的组件和Window对象中的mDector成员变量,这里就不在详细的说明。

    然后回到我们的show方法,在执行了dispatchOnCreate方法之后我们又调用了onStart方法,这个方法主要用于设置ActionBar,这里不做过多的说明,然后初始化WindowManager.LayoutParams对象,并最终调用我们的mWindowManager.addView()方法。

    O(∩_∩)O哈哈~,到了这一步大家如果看了上一篇Acitivty布局绘制流程的话,就应该知道顺着这个方法整个Dialog的界面就会被绘制出来了。

    最后我们调用了sendShowMessage方法,可以看一下这个方法的实现:

    private void sendShowMessage() {
            if (mShowMessage != null) {
                // Obtain a new message so this dialog can be re-used
                Message.obtain(mShowMessage).sendToTarget();
            }
        }
    

    这里会发送一个Dialog已经显示的异步消息,该消息最终会在ListenersHandler中的handleMessage方法中被执行:

    private static final class ListenersHandler extends Handler {
            private WeakReference<DialogInterface> mDialog;
    
            public ListenersHandler(Dialog dialog) {
                mDialog = new WeakReference<DialogInterface>(dialog);
            }
    
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case DISMISS:
                        ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                        break;
                    case CANCEL:
                        ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                        break;
                    case SHOW:
                        ((OnShowListener) msg.obj).onShow(mDialog.get());
                        break;
                }
            }
        }
    

    由于我们的msg.what = SHOW,所以会执行OnShowListener.onShow方法,那么这个OnShowListener是何时赋值的呢?还记得我们构造AlertDialog.Builder么?

    alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
                        @Override
                        public void onShow(DialogInterface dialog) {
    
                        }
                    });
    

    这样就为我们的AlertDialog.Builder设置了OnShowListener,可以看一下setOnShowListener方法的具体实现:

    public void setOnShowListener(OnShowListener listener) {
            if (listener != null) {
                mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
            } else {
                mShowMessage = null;
            }
        }
    

    这样就为我们的Dialog中的mListenersHandler构造了Message对象,并且当我们在Dialog中发送showMessage的时候被mListenersHandler所接收。。。。

    注:
    这里说一下我们平时开发中若创建的Dialog使用的Context对象不是Activity,就会报出:

    android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
    

    的异常,这是由于WindowManager.addView方法最终会调用ViewRootImpl.setView方法,而这时候会有mToken的检查,若我们传入的Context对象不是Activity,这时候的mToken为空,就会出现上述问题。。。

    总结:

    Dialog和Activity的显示逻辑是相似的都是内部管理这一个Window对象,用WIndow对象实现界面的加载与显示逻辑;

    Dialog中的Window对象与Activity中的Window对象是相似的,都对应着一个WindowManager对象;

    Dialog相关的几个类:Dialog,AlertDialog,AlertDialog.Builder,AlertController,AlertController.AlertParams,其中Dialog是窗口的父类,主要实现Window对象的初始化和一些共有逻辑,而AlertDialog是具体的Dialog的操作实现类,AlertDialog.Builder类是AlertDialog的内部类,主要用于构造AlertDialog,AlertController是AlertDialog的控制类,AlertController.AlertParams类是控制参数类;

    构造显示Dialog的一般流程,构造AlertDialog.Builder,然后设置各种属性,最后调用AlertDialog.Builder.create方法获取AlertDialog对象,并且create方法中会执行,构造AlertDialog,设置dialog各种属性的操作。最后我们调用Dialog.show方法展示窗口,初始化Dialog的布局文件,Window对象等,然后执行mWindowManager.addView方法,开始执行绘制View的操作,并最终将Dialog显示出来;

    相关文章

      网友评论

          本文标题:AlertDialog 详解

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