美文网首页Android开发Android开发经验谈Android开发
【Window系列】——Dialog源码解析

【Window系列】——Dialog源码解析

作者: 被代码淹没的小伙子 | 来源:发表于2019-06-29 15:27 被阅读16次

    本系列博客基于android-28版本
    【Window系列】——Toast源码解析
    【Window系列】——PopupWindow的前世今生
    【Window系列】——Dialog源码解析

    前言

    前面两篇博客分别分析了Toast和PopupWindow,本篇博客来分析Dialog和DialogFragment,在早期Android,Dialog一直是弹窗的主力军,自从出了DialogFragment后,其兼容Dialog的特性和Fragment感知生命周期的优势,逐渐替代了Dialog。

    Dialog源码解析

    关于Dialog的使用方式,首先我们想到的是AlertDialog,常规我们的使用方式是如下代码:

    AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("问题:");
            builder.setMessage("请问你满十八岁了吗?");
            AlertDialog dialog = builder.create();
            //显示对话框
            dialog.show();
    

    看到Builder我们第一时间应该就能想到Builder模式,Dialog的Builder应该是我们最早接触Builder模式的实际应用之一,了,从这可以看出Dialog涉及的参数很多,所以Google选用里Builder模式来构建Dialog。
    简单的先来看一下Builder的源码

    public Builder(Context context, int themeResId) {
                P = new AlertController.AlertParams(new ContextThemeWrapper(
                        context, resolveDialogTheme(context, themeResId)));
            }
    public Builder setTitle(CharSequence title) {
                P.mTitle = title;
                return this;
            }
    public Builder setCustomTitle(View customTitleView) {
                P.mCustomTitleView = customTitleView;
                return this;
            }
    

    可以看到Builder的构造函数里创建了一个AlertController.AlertParams对象,而Builder设置的参数都是给AlertController.AlertParams设置,也就是说AlertController.AlertParams是一个Dialog参数的包装集成类。
    那么来看一下create方法。

    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后,执行了apply方法,将刚才设置给AlertController.AlertParams赋值给AlertDialog,不知道怎么了,看到这个方法名,感觉有点看到Glide源码中关于GlideOptions的身影,Glide源码中对于GlideOptions最终也是使用一个apply的方式,进行赋值,不知道Glide是否是对于这个有一定的参考。

    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)) {
                //创建ListView
                    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);
                }
    
                /*
                dialog.setCancelable(mCancelable);
                dialog.setOnCancelListener(mOnCancelListener);
                if (mOnKeyListener != null) {
                    dialog.setOnKeyListener(mOnKeyListener);
                }
                */
            }
    

    可以看到,这里基本上将刚才设置给AlertController.AlertParams都赋值给了AlertDialog,至此,BuilderAlertController.AlertParams的都完成了自己的作用,最终构建出了AlertDialog对象,那么接下来就是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) {
                    //执行onCreate回调
                dispatchOnCreate(null);
            } else {
                // Fill the DecorView in on any configuration changes that
                // may have occured while it was removed from the WindowManager.
                final Configuration config = mContext.getResources().getConfiguration();
                mWindow.getDecorView().dispatchConfigurationChanged(config);
            }
            //执行onStart回调
            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();
            boolean restoreSoftInputMode = false;
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
                l.softInputMode |=
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                restoreSoftInputMode = true;
            }
            //加入View
            mWindowManager.addView(mDecor, l);
            if (restoreSoftInputMode) {
                l.softInputMode &=
                        ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
            }
    
            mShowing = true;
            //利用Handler发送回调
            sendShowMessage();
        }
    

    首先来看一下onCreate中执行了什么。

    //Dialog.java
    void dispatchOnCreate(Bundle savedInstanceState) {
            if (!mCreated) {
                onCreate(savedInstanceState);
                mCreated = true;
            }
        }
    //AlertDialog.java
    @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mAlert.installContent();
        }
    //AlertController.java
    public void installContent() {
            int contentView = selectContentView();
            //设置根布局文件
            mWindow.setContentView(contentView);
            //设置View相关属性
            setupView();
        }
    

    可以看到最终调用了WindowsetContentView方法,看过前面一篇博客(【重拾View(一)】——setContentView()源码解析)的应该熟悉这个方法,这个方法是创建DecorView并,把我们的布局文件,添加到DecorView中。这里注意两点

    1. Window的创建的时机,
    2. 根布局文件
      这里看到其实Window是已经完成了,整个文件中找Window的构建过程,可以找到在Dialog的构造函数中。
    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            if (createContextThemeWrapper) {
                if (themeResId == ResourceId.ID_NULL) {
                    final TypedValue outValue = new TypedValue();
                    context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                    themeResId = outValue.resourceId;
                }
                mContext = new ContextThemeWrapper(context, themeResId);
            } else {
                mContext = context;
            }
            //获取WindowManager
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            //构建PhoneWindow
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            w.setCallback(this);
            w.setOnWindowDismissedCallback(this);
            w.setOnWindowSwipeDismissedCallback(() -> {
                if (mCancelable) {
                    cancel();
                }
            });
            w.setWindowManager(mWindowManager, null, null);
            w.setGravity(Gravity.CENTER);
            //创建Handler对象
            mListenersHandler = new ListenersHandler(this);
        }
    

    可以看到这里和Activity的构建过程相同,也是利用WindowManager创建了一个PhoneWindow。具体逻辑可以看(【重拾View(一)】——setContentView()源码解析
    再看一下根布局文件。

    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;
        }
    
    protected AlertController(Context context, DialogInterface di, Window window) {
            mContext = context;
            mDialogInterface = di;
            mWindow = window;
            mHandler = new ButtonHandler(di);
    
            final TypedArray a = context.obtainStyledAttributes(null,
                    R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
            //默认的布局文件
            mAlertDialogLayout = a.getResourceId(
                    R.styleable.AlertDialog_layout, R.layout.alert_dialog);
            mButtonPanelSideLayout = a.getResourceId(
                    R.styleable.AlertDialog_buttonPanelSideLayout, 0);
            mListLayout = a.getResourceId(
                    R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
    
            mMultiChoiceItemLayout = a.getResourceId(
                    R.styleable.AlertDialog_multiChoiceItemLayout,
                    R.layout.select_dialog_multichoice);
            mSingleChoiceItemLayout = a.getResourceId(
                    R.styleable.AlertDialog_singleChoiceItemLayout,
                    R.layout.select_dialog_singlechoice);
            mListItemLayout = a.getResourceId(
                    R.styleable.AlertDialog_listItemLayout,
                    R.layout.select_dialog_item);
            mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
    
            a.recycle();
    
            /* We use a custom title so never request a window title */
            window.requestFeature(Window.FEATURE_NO_TITLE);
        }
    

    这里可以看到mAlertDialogLayout对象是在AlertController构造函数时通过读取属性参数,而默认的布局文件是R.layout.alert_dialog
    这里简单的看一下这个布局的布局结构,可以看到和我们设置的属性基本是相同。

    dialog_layout.png
    最后看一下setupView()
    private void setupView() {
            final View parentPanel = mWindow.findViewById(R.id.parentPanel);
            final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
            final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
            final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);
    
            // Install custom content before setting up the title or buttons so
            // that we can handle panel overrides.
            final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel);
            setupCustomContent(customPanel);
    
            final View customTopPanel = customPanel.findViewById(R.id.topPanel);
            final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
            final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);
    
            // Resolve the correct panels and remove the defaults, if needed.
            final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
            final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
            final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);
    
            setupContent(contentPanel);
            setupButtons(buttonPanel);
            setupTitle(topPanel);
    
            final boolean hasCustomPanel = customPanel != null
                    && customPanel.getVisibility() != View.GONE;
            final boolean hasTopPanel = topPanel != null
                    && topPanel.getVisibility() != View.GONE;
            final boolean hasButtonPanel = buttonPanel != null
                    && buttonPanel.getVisibility() != View.GONE;
    
            // Only display the text spacer if we don't have buttons.
            if (!hasButtonPanel) {
                if (contentPanel != null) {
                    final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
                    if (spacer != null) {
                        spacer.setVisibility(View.VISIBLE);
                    }
                }
                mWindow.setCloseOnTouchOutsideIfNotSet(true);
            }
    
            if (hasTopPanel) {
                // Only clip scrolling content to padding if we have a title.
                if (mScrollView != null) {
                    mScrollView.setClipToPadding(true);
                }
    
                // Only show the divider if we have a title.
                View divider = null;
                if (mMessage != null || mListView != null || hasCustomPanel) {
                    if (!hasCustomPanel) {
                        divider = topPanel.findViewById(R.id.titleDividerNoCustom);
                    }
                    if (divider == null) {
                        divider = topPanel.findViewById(R.id.titleDivider);
                    }
    
                } else {
                    divider = topPanel.findViewById(R.id.titleDividerTop);
                }
    
                if (divider != null) {
                    divider.setVisibility(View.VISIBLE);
                }
            } else {
                if (contentPanel != null) {
                    final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle);
                    if (spacer != null) {
                        spacer.setVisibility(View.VISIBLE);
                    }
                }
            }
    
            if (mListView instanceof RecycleListView) {
                ((RecycleListView) mListView).setHasDecor(hasTopPanel, hasButtonPanel);
            }
    
            // Update scroll indicators as needed.
            if (!hasCustomPanel) {
                final View content = mListView != null ? mListView : mScrollView;
                if (content != null) {
                    final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0)
                            | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0);
                    content.setScrollIndicators(indicators,
                            View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
                }
            }
    
            final TypedArray a = mContext.obtainStyledAttributes(
                    null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
            setBackground(a, topPanel, contentPanel, customPanel, buttonPanel,
                    hasTopPanel, hasCustomPanel, hasButtonPanel);
            a.recycle();
        }
    

    不出意外,就是将我们设置的属性,分别设置到布局文件上对应的View上,至此,我们通过Builder设置的参数属性,就设置到DecorView上。剩下的就是将DecorView加入到PhoneWindow上,然后调用mWindowManager.addView(mDecor, l);这个方法后会执行到ViewRootImpl,等到下个屏幕信号到来时就会刷新出来。

    DialogFragment源码解析

    本篇博客主要讲解的是Dialog相关的源码解析,所以侧重点主要是和Dialog相关的,涉及到Fragment相关的知识点,这里就不做详细的讲解了。
    查看DialogFragment相关的源码会发现Dialog的身影。

    @Override
        @NonNull
        public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
            if (!mShowsDialog) {
                return super.onGetLayoutInflater(savedInstanceState);
            }
            //创建Dialog
            mDialog = onCreateDialog(savedInstanceState);
    
            if (mDialog != null) {
                  //设置Dialog属性
                setupDialog(mDialog, mStyle);
    
                return (LayoutInflater) mDialog.getContext().getSystemService(
                        Context.LAYOUT_INFLATER_SERVICE);
            }
            return (LayoutInflater) mHost.getContext().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
        }
    

    可以看到这里创建了一个Dialog,但我们可能对于这个方法比较陌生onGetLayoutInflater,找寻这个方法对调用链,最终我们会在FragmentManagerImpl中找到。

    f.performCreateView(f.performGetLayoutInflater(
                                        f.mSavedFragmentState), container, f.mSavedFragmentState);
                                        
    @NonNull
        LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) {
            LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState);
            mLayoutInflater = layoutInflater;
            return mLayoutInflater;
        }
    

    所以我们就知道了,在DialogFragment回调onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,@Nullable Bundle savedInstanceState)方法的时候,就会创建一个Dialog对象。
    我们接下来沿着Fragment生命周期继续向下看,在onActivityCreate中又一次看到了Dialog的身影。

    @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
    
            if (!mShowsDialog) {
                return;
            }
    
            View view = getView();
            if (view != null) {
                if (view.getParent() != null) {
                    throw new IllegalStateException(
                            "DialogFragment can not be attached to a container view");
                }
                //设置ContentView到Dialog中
                mDialog.setContentView(view);
            }
            final Activity activity = getActivity();
            if (activity != null) {
                mDialog.setOwnerActivity(activity);
            }
            mDialog.setCancelable(mCancelable);
            mDialog.setOnCancelListener(this);
            mDialog.setOnDismissListener(this);
            if (savedInstanceState != null) {
                Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
                if (dialogState != null) {
                    mDialog.onRestoreInstanceState(dialogState);
                }
            }
        }
    @Nullable
        public View getView() {
            return mView;
        }
    

    这里可以看到,首先保存了我们在onCreateView中返回的View对象,然后设置到了DialogContentView中,也就是说我们在DialogFragment中设置到布局,最终其实是以Dialog到形式展示的。

    @Override
        public void onStart() {
            super.onStart();
    
            if (mDialog != null) {
                mViewDestroyed = false;
                mDialog.show();
            }
        }
    

    紧接着在onStart方法中,直接调用了show方法,将Dialog显示出来了。

    @Override
        public void onStop() {
            super.onStop();
            if (mDialog != null) {
                mDialog.hide();
            }
        }
    
        /**
         * Remove dialog.
         */
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            if (mDialog != null) {
                // Set removed here because this dismissal is just to hide
                // the dialog -- we don't want this to cause the fragment to
                // actually be removed.
                mViewDestroyed = true;
                // Instead of waiting for a posted onDismiss(), null out
                // the listener and call onDismiss() manually to ensure
                // that the callback happens before onDestroy()
                mDialog.setOnDismissListener(null);
                mDialog.dismiss();
                if (!mDismissed) {
                    // Don't send a second onDismiss() callback if we've already
                    // dismissed the dialog manually in dismissInternal()
                    onDismiss(mDialog);
                }
                mDialog = null;
            }
        }
    

    后面对应的生命周期中,分别在onStop方法中利用hide方法隐藏了Dialog,而在onDestroyView中,dismissDialog

    总结

    所以最终我们会发现,其实DialogFragment整个生命周期中贯穿着对于Dialog的使用,DialogFragment其实是对于Dialog的一种包装类的思想,不仅将Dialog单独抽出来成为一个个体,并且利用Fragment的特性,赋予了Dialog生命周期的能力,可以看出Google对于Frament感知生命周期的特性的利用其实很早就已经开始了,而Google新出的JetPack框架中也是重复利用了Fragment的特性完成的。
    本篇博客只是讲解了Dialog的源码实现和DialogFragment的拓展使用,但是Dialog还有一个更为重要的知识点这里没有分析,和前面几篇博客一样,就是对于token对象的分析。熟悉Dialog的应该清楚,Dialog的构建必须传入一个Activity类型的Context,如果传入的是Application,则会抛异常,这里面的缘由也是由于token对象引起的,所以下一篇博客应该是【Window】系列的终篇,讲一讲关于Windowtokentype的那些事。

    相关文章

      网友评论

        本文标题:【Window系列】——Dialog源码解析

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