【W(wǎng)indow系列】——Dialog源碼解析

本系列博客基于android-28版本
【W(wǎng)indow系列】——Toast源碼解析
【W(wǎng)indow系列】——PopupWindow的前世今生
【W(wǎng)indow系列】——Dialog源碼解析
【W(wǎng)indow系列】——Window中的Token

前言

前面兩篇博客分別分析了Toast和PopupWindow,本篇博客來分析Dialog和DialogFragment,在早期Android,Dialog一直是彈窗的主力軍,自從出了DialogFragment后,其兼容Dialog的特性和Fragment感知生命周期的優(yōu)勢,逐漸替代了Dialog。

Dialog源碼解析

關(guān)于Dialog的使用方式,首先我們想到的是AlertDialog,常規(guī)我們的使用方式是如下代碼:

AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle("問題:");
        builder.setMessage("請問你滿十八歲了嗎?");
        AlertDialog dialog = builder.create();
        //顯示對話框
        dialog.show();

看到Builder我們第一時(shí)間應(yīng)該就能想到Builder模式,Dialog的Builder應(yīng)該是我們最早接觸Builder模式的實(shí)際應(yīng)用之一,了,從這可以看出Dialog涉及的參數(shù)很多,所以Google選用里Builder模式來構(gòu)建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的構(gòu)造函數(shù)里創(chuàng)建了一個(gè)AlertController.AlertParams對象,而Builder設(shè)置的參數(shù)都是給AlertController.AlertParams設(shè)置,也就是說AlertController.AlertParams是一個(gè)Dialog參數(shù)的包裝集成類。
那么來看一下create方法。

public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            //參數(shù)賦值
            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;
        }

可以看到代碼很簡單,構(gòu)造了一個(gè)AlertDialog后,執(zhí)行了apply方法,將剛才設(shè)置給AlertController.AlertParams賦值給AlertDialog,不知道怎么了,看到這個(gè)方法名,感覺有點(diǎn)看到Glide源碼中關(guān)于GlideOptions的身影,Glide源碼中對于GlideOptions最終也是使用一個(gè)apply的方式,進(jìn)行賦值,不知道Glide是否是對于這個(gè)有一定的參考。

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)) {
            //創(chuàng)建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);
            }
            */
        }

可以看到,這里基本上將剛才設(shè)置給AlertController.AlertParams都賦值給了AlertDialog,至此,BuilderAlertController.AlertParams的都完成了自己的作用,最終構(gòu)建出了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) {
                //執(zhí)行onCreate回調(diào)
            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);
        }
        //執(zhí)行onStart回調(diào)
        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發(fā)送回調(diào)
        sendShowMessage();
    }

首先來看一下onCreate中執(zhí)行了什么。

//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();
        //設(shè)置根布局文件
        mWindow.setContentView(contentView);
        //設(shè)置View相關(guān)屬性
        setupView();
    }

可以看到最終調(diào)用了WindowsetContentView方法,看過前面一篇博客(【重拾View(一)】——setContentView()源碼解析)的應(yīng)該熟悉這個(gè)方法,這個(gè)方法是創(chuàng)建DecorView并,把我們的布局文件,添加到DecorView中。這里注意兩點(diǎn)

  1. Window的創(chuàng)建的時(shí)機(jī),
  2. 根布局文件
    這里看到其實(shí)Window是已經(jīng)完成了,整個(gè)文件中找Window的構(gòu)建過程,可以找到在Dialog的構(gòu)造函數(shù)中。
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);
        //構(gòu)建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);
        //創(chuàng)建Handler對象
        mListenersHandler = new ListenersHandler(this);
    }

可以看到這里和Activity的構(gòu)建過程相同,也是利用WindowManager創(chuàng)建了一個(gè)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);
        //默認(rèn)的布局文件
        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構(gòu)造函數(shù)時(shí)通過讀取屬性參數(shù),而默認(rèn)的布局文件是R.layout.alert_dialog
這里簡單的看一下這個(gè)布局的布局結(jié)構(gòu),可以看到和我們設(shè)置的屬性基本是相同。

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

不出意外,就是將我們設(shè)置的屬性,分別設(shè)置到布局文件上對應(yīng)的View上,至此,我們通過Builder設(shè)置的參數(shù)屬性,就設(shè)置到DecorView上。剩下的就是將DecorView加入到PhoneWindow上,然后調(diào)用mWindowManager.addView(mDecor, l);這個(gè)方法后會執(zhí)行到ViewRootImpl,等到下個(gè)屏幕信號到來時(shí)就會刷新出來。

DialogFragment源碼解析

本篇博客主要講解的是Dialog相關(guān)的源碼解析,所以側(cè)重點(diǎn)主要是和Dialog相關(guān)的,涉及到Fragment相關(guān)的知識點(diǎn),這里就不做詳細(xì)的講解了。
查看DialogFragment相關(guān)的源碼會發(fā)現(xiàn)Dialog的身影。

@Override
    @NonNull
    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        if (!mShowsDialog) {
            return super.onGetLayoutInflater(savedInstanceState);
        }
        //創(chuàng)建Dialog
        mDialog = onCreateDialog(savedInstanceState);

        if (mDialog != null) {
              //設(shè)置Dialog屬性
            setupDialog(mDialog, mStyle);

            return (LayoutInflater) mDialog.getContext().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
        }
        return (LayoutInflater) mHost.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }

可以看到這里創(chuàng)建了一個(gè)Dialog,但我們可能對于這個(gè)方法比較陌生onGetLayoutInflater,找尋這個(gè)方法對調(diào)用鏈,最終我們會在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回調(diào)onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,@Nullable Bundle savedInstanceState)方法的時(shí)候,就會創(chuàng)建一個(gè)Dialog對象。
我們接下來沿著Fragment生命周期繼續(xù)向下看,在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");
            }
            //設(shè)置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對象,然后設(shè)置到了DialogContentView中,也就是說我們在DialogFragment中設(shè)置到布局,最終其實(shí)是以Dialog到形式展示的。

@Override
    public void onStart() {
        super.onStart();

        if (mDialog != null) {
            mViewDestroyed = false;
            mDialog.show();
        }
    }

緊接著在onStart方法中,直接調(diào)用了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;
        }
    }

后面對應(yīng)的生命周期中,分別在onStop方法中利用hide方法隱藏了Dialog,而在onDestroyView中,dismissDialog

總結(jié)

所以最終我們會發(fā)現(xiàn),其實(shí)DialogFragment整個(gè)生命周期中貫穿著對于Dialog的使用,DialogFragment其實(shí)是對于Dialog的一種包裝類的思想,不僅將Dialog單獨(dú)抽出來成為一個(gè)個(gè)體,并且利用Fragment的特性,賦予了Dialog生命周期的能力,可以看出Google對于Frament感知生命周期的特性的利用其實(shí)很早就已經(jīng)開始了,而Google新出的JetPack框架中也是重復(fù)利用了Fragment的特性完成的。
本篇博客只是講解了Dialog的源碼實(shí)現(xiàn)和DialogFragment的拓展使用,但是Dialog還有一個(gè)更為重要的知識點(diǎn)這里沒有分析,和前面幾篇博客一樣,就是對于token對象的分析。熟悉Dialog的應(yīng)該清楚,Dialog的構(gòu)建必須傳入一個(gè)Activity類型的Context,如果傳入的是Application,則會拋異常,這里面的緣由也是由于token對象引起的,所以下一篇博客應(yīng)該是【W(wǎng)indow】系列的終篇,講一講關(guān)于Windowtokentype的那些事。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容