Android源碼分析之AlertDialog的啓動分析

一、相關類

  • AlertController 具體顯示邏輯
  • AlertController.AlertParams 存放Dialog相關參數信息
  • AlertDialog.Builder 用來創建AlertDialog對象的實例

二、創建代碼

通過上述相關類我們可以知道,AlertDialog的創建過程使用了Builder模式

    public void showAlertDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("標題")
                .setMessage("內容")
                .setNegativeButton("Button1", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .setNeutralButton("Button2", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .setPositiveButton("Button3", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .create()
                .show();
    }

三、流程分析

1、Builder的創建

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

從代碼中可以看出,調用Builder構造函數創建對象,我們接着看該類的構造方法

public static class Builder {
	//封裝了 AlertController.AlertParams對象,用final修飾,意味着需要在構造方法中
	//進行初始化
	 private final AlertController.AlertParams P;
	
	 public Builder(Context context) {
            this(context, resolveDialogTheme(context, Resources.ID_NULL));
    }

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

在構造方法中,我們創建了AlertController.AlertParams的實例P,接着我們開始設置參數信息,如設置標題,設置Message,設置Button等,這寫方法最終都會將相應的參數封裝在P對應的參數中

//設置Title
 public Builder setTitle(@StringRes int titleId) {
            P.mTitle = P.mContext.getText(titleId);
            return this;
 }
//set the message to display using the given resource id.
public Builder setMessage(@StringRes int messageId) {
            P.mMessage = P.mContext.getText(messageId);
            return this;
}
		 /**
         * Set a listener to be invoked when the positive button of the dialog is pressed.
         * @param textId The resource id of the text to display in the positive button
         * @param listener The {@link DialogInterface.OnClickListener} to use.
         *
         * @return This Builder object to allow for chaining of calls to set methods
         */
        public Builder setPositiveButton(@StringRes int textId, final OnClickListener listener) {
            P.mPositiveButtonText = P.mContext.getText(textId);
            P.mPositiveButtonListener = listener;
            return this;
        }

2、創建AlertDialog對象的實例

AlertDialog dialog  = builder.create();

我們接下來分析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;
        }

從源碼我們可以看出,主要分爲3步:

2.1 調用Alertdialog的構造方法創建該對象的實例dialog

最終會調用掉下面的構造方法,在構造方法中,創建了AlertController對象的實例,該對象主要用來進行Dialog的佈局準備工作

  protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
        super(context, resolveDialogTheme(context, 0));
        mWindow.alwaysReadCloseOnTouchAttr();
        setCancelable(cancelable);
        setOnCancelListener(cancelListener);
        mAlert = new AlertController(context, this, getWindow());
 }

我們接下來看AlertController類的源碼

  public AlertController(Context context, DialogInterface di, Window window) {
        mContext = context;
        mDialogInterface = di;
        mWindow = window;
        mHandler = new ButtonHandler(di);

       TypedArray a = context.obtainStyledAttributes(null,
               com.android.internal.R.styleable.AlertDialog,
                com.android.internal.R.attr.alertDialogStyle, 0);

        mAlertDialogLayout = a.getResourceId(com.android.internal.R.styleable.AlertDialog_layout,
                com.android.internal.R.layout.alert_dialog);
        mListLayout = a.getResourceId(
                com.android.internal.R.styleable.AlertDialog_listLayout,
                com.android.internal.R.layout.select_dialog);
        mMultiChoiceItemLayout = a.getResourceId(
                com.android.internal.R.styleable.AlertDialog_multiChoiceItemLayout,
                com.android.internal.R.layout.select_dialog_multichoice);
        mSingleChoiceItemLayout = a.getResourceId(
                com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout,
               com.android.internal.R.layout.select_dialog_singlechoice);
        mListItemLayout = a.getResourceId(
                com.android.internal.R.styleable.AlertDialog_listItemLayout,
                com.android.internal.R.layout.select_dialog_item);

        a.recycle();
}

從構造方法中我們可以看出,主要的工作就是去加載默認的佈局文件,當我們沒有爲AlertDialog設置佈局信息時,會顯示默認的佈局,以下是默認的佈局文件

<LinearLayout
22    xmlns:android="http://schemas.android.com/apk/res/android"
23    android:id="@+id/parentPanel"
24    android:layout_width="match_parent"
25    android:layout_height="wrap_content"
26    android:orientation="vertical"
27    android:paddingTop="9dip"
28    android:paddingBottom="3dip"
29    android:paddingStart="3dip"
30    android:paddingEnd="1dip">
31
32    <LinearLayout android:id="@+id/topPanel"
33        android:layout_width="match_parent"
34        android:layout_height="wrap_content"
35        android:minHeight="54dip"
36        android:orientation="vertical">
37        <LinearLayout android:id="@+id/title_template"
38            android:layout_width="match_parent"
39            android:layout_height="wrap_content"
40            android:orientation="horizontal"
41            android:gravity="center_vertical"
42            android:layout_marginTop="6dip"
43            android:layout_marginBottom="9dip"
44            android:layout_marginStart="10dip"
45            android:layout_marginEnd="10dip">
46            <ImageView android:id="@+id/icon"
47                android:layout_width="wrap_content"
48                android:layout_height="wrap_content"
49                android:layout_gravity="top"
50                android:paddingTop="6dip"
51                android:paddingEnd="10dip"
52                android:src="@drawable/ic_dialog_info" />
53            <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
54                style="?android:attr/textAppearanceLarge"
55                android:singleLine="true"
56                android:ellipsize="end"
57                android:layout_width="match_parent"
58                android:layout_height="wrap_content"
59                android:textAlignment="viewStart" />
60        </LinearLayout>
61        <ImageView android:id="@+id/titleDivider"
62            android:layout_width="match_parent"
63            android:layout_height="1dip"
64            android:visibility="gone"
65            android:scaleType="fitXY"
66            android:gravity="fill_horizontal"
67            android:src="@android:drawable/divider_horizontal_dark" />
68        <!-- If the client uses a customTitle, it will be added here. -->
69    </LinearLayout>
70
71    <LinearLayout android:id="@+id/contentPanel"
72        android:layout_width="match_parent"
73        android:layout_height="wrap_content"
74        android:layout_weight="1"
75        android:orientation="vertical">
76        <ScrollView android:id="@+id/scrollView"
77            android:layout_width="match_parent"
78            android:layout_height="wrap_content"
79            android:paddingTop="2dip"
80            android:paddingBottom="12dip"
81            android:paddingStart="14dip"
82            android:paddingEnd="10dip"
83            android:overScrollMode="ifContentScrolls">
84            <TextView android:id="@+id/message"
85                style="?android:attr/textAppearanceMedium"
86                android:layout_width="match_parent"
87                android:layout_height="wrap_content"
88                android:padding="5dip" />
89        </ScrollView>
90    </LinearLayout>
91
92    <FrameLayout android:id="@+id/customPanel"
93        android:layout_width="match_parent"
94        android:layout_height="wrap_content"
95        android:layout_weight="1">
96        <FrameLayout android:id="@+android:id/custom"
97            android:layout_width="match_parent"
98            android:layout_height="wrap_content"
99            android:paddingTop="5dip"
100            android:paddingBottom="5dip" />
101    </FrameLayout>
102
103    <LinearLayout android:id="@+id/buttonPanel"
104        android:layout_width="match_parent"
105        android:layout_height="wrap_content"
106        android:minHeight="54dip"
107        android:orientation="vertical" >
108        <LinearLayout
109            style="?android:attr/buttonBarStyle"
110            android:layout_width="match_parent"
111            android:layout_height="wrap_content"
112            android:orientation="horizontal"
113            android:paddingTop="4dip"
114            android:paddingStart="2dip"
115            android:paddingEnd="2dip"
116            android:measureWithLargestChild="true">
117            <LinearLayout android:id="@+id/leftSpacer"
118                android:layout_weight="0.25"
119                android:layout_width="0dip"
120                android:layout_height="wrap_content"
121                android:orientation="horizontal"
122                android:visibility="gone" />
123            <Button android:id="@+id/button1"
124                android:layout_width="0dip"
125                android:layout_gravity="start"
126                android:layout_weight="1"
127                style="?android:attr/buttonBarButtonStyle"
128                android:maxLines="2"
129                android:layout_height="wrap_content" />
130            <Button android:id="@+id/button3"
131                android:layout_width="0dip"
132                android:layout_gravity="center_horizontal"
133                android:layout_weight="1"
134                style="?android:attr/buttonBarButtonStyle"
135                android:maxLines="2"
136                android:layout_height="wrap_content" />
137            <Button android:id="@+id/button2"
138                android:layout_width="0dip"
139                android:layout_gravity="end"
140                android:layout_weight="1"
141                style="?android:attr/buttonBarButtonStyle"
142                android:maxLines="2"
143                android:layout_height="wrap_content" />
144            <LinearLayout android:id="@+id/rightSpacer"
145                android:layout_width="0dip"
146                android:layout_weight="0.25"
147                android:layout_height="wrap_content"
148                android:orientation="horizontal"
149                android:visibility="gone" />
150        </LinearLayout>
151     </LinearLayout>
152</LinearLayout>

2.2 將P中的參數與AlertController對象的屬性項綁定

接着我們看apply方法。從代碼中我們可以看出,apply方法的主要作用是將AlertParams存放的有關AlertDialog的參數信息傳遞給AlertController對象

public void apply(AlertController dialog) {
811            if (mCustomTitleView != null) {
812                dialog.setCustomTitle(mCustomTitleView);
813            } else {
814                if (mTitle != null) {
815                    dialog.setTitle(mTitle);
816                }
817                if (mIcon != null) {
818                    dialog.setIcon(mIcon);
819                }
820                if (mIconId >= 0) {
821                    dialog.setIcon(mIconId);
822                }
823                if (mIconAttrId > 0) {
824                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
825                }
826            }
827            if (mMessage != null) {
828                dialog.setMessage(mMessage);
829            }
830            if (mPositiveButtonText != null) {
831                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
832                        mPositiveButtonListener, null);
833            }
834            if (mNegativeButtonText != null) {
835                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
836                        mNegativeButtonListener, null);
837            }
838            if (mNeutralButtonText != null) {
839                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
840                        mNeutralButtonListener, null);
841            }
842            if (mForceInverseBackground) {
843                dialog.setInverseBackgroundForced(true);
844            }
845            // For a list, the client can either supply an array of items or an
846            // adapter or a cursor
847            if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
848                createListView(dialog);
849            }
850            if (mView != null) {
851                if (mViewSpacingSpecified) {
852                    dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
853                            mViewSpacingBottom);
854                } else {
855                    dialog.setView(mView);
856                }
857            }
858
859            /*
860            dialog.setCancelable(mCancelable);
861            dialog.setOnCancelListener(mOnCancelListener);
862            if (mOnKeyListener != null) {
863                dialog.setOnKeyListener(mOnKeyListener);
864            }
865            */
866        }

2.3 設置dialog的監聽信息並返回dialog

返回dilaog對象

dialog.setCancelable(P.mCancelable);
934            if (P.mCancelable) {
935                dialog.setCanceledOnTouchOutside(true);
936            }
937            dialog.setOnCancelListener(P.mOnCancelListener);
938            dialog.setOnDismissListener(P.mOnDismissListener);
939            if (P.mOnKeyListener != null) {
940                dialog.setOnKeyListener(P.mOnKeyListener);
941            }

3、顯示Dialog

dialog.show();

該方法最終會調用**父類的show()**方法

  public void show() {
249        if (mShowing) {
250            if (mDecor != null) {
251                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
252                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
253                }
254                mDecor.setVisibility(View.VISIBLE);
255            }
256            return;
257        }
258
259        mCanceled = false;
260
261        if (!mCreated) {
262            dispatchOnCreate(null);
263        }
264
265        onStart();
266        mDecor = mWindow.getDecorView();
267
268        if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
269            final ApplicationInfo info = mContext.getApplicationInfo();
270            mWindow.setDefaultIcon(info.icon);
271            mWindow.setDefaultLogo(info.logo);
272            mActionBar = new ActionBarImpl(this);
273        }
274
275        WindowManager.LayoutParams l = mWindow.getAttributes();
276        if ((l.softInputMode
277                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
278            WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
279            nl.copyFrom(l);
280            nl.softInputMode |=
281                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
282            l = nl;
283        }
284
285        try {
286            mWindowManager.addView(mDecor, l);
287            mShowing = true;
288
289            sendShowMessage();
290        } finally {
291        }
292    }

前面部分的代碼主要是判斷dialog是否顯示,如果dialog目前爲顯示狀態,則直接結束。重要的方法是dispatchOnCreate,該方法會設置相應的佈局信息,跟蹤源碼我們發現最終調用AlertDialog的onCreate方法,而該方法又調用了AlertController的installContent方法,饒了一圈,又回到了AlertController。

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

我們來看看installContent做了哪些事情?

 public void installContent() {
233        /* We use a custom title so never request a window title */
234        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
235
236        if (mView == null || !canTextInput(mView)) {
237            mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
238                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
239        }
240        mWindow.setContentView(mAlertDialogLayout);
241        setupView();
242    }
243

從代碼中我們不難發現,主要是設置窗體信息,然後設置佈局信息,大家可能奇怪,這個mWindow是如何初始化的呢?其實在調用AlertController的構造方法是傳遞進來的,我們跟蹤源碼可以發現,mWindow是調用Dialog的getWindow()方法返回的,他本質上是一個PhoneWindow,可以發現,它是在Dialog的構造方法中創建的(調用AlertDialog的構造方法會默認調用父類的構造方法),看到這,我們就差不多明白,原來它其實是個PhoneWindow,我們在看看這個方法 mWindow.setContentView(mAlertDialogLayout),是不是很眼熟,setContentView,創建界面的時候設置佈局的方法,通過這個方法,我們就可以將佈局設置到PhoneWindow了,然後設置佈局的其他信息。

    private void setupView() {
394        LinearLayout contentPanel = (LinearLayout) mWindow.findViewById(R.id.contentPanel);
395        setupContent(contentPanel);
396        boolean hasButtons = setupButtons();
397
398        LinearLayout topPanel = (LinearLayout) mWindow.findViewById(R.id.topPanel);
399        TypedArray a = mContext.obtainStyledAttributes(
400                null, com.android.internal.R.styleable.AlertDialog, com.android.internal.R.attr.alertDialogStyle, 0);
401        boolean hasTitle = setupTitle(topPanel);
402
403        View buttonPanel = mWindow.findViewById(R.id.buttonPanel);
404        if (!hasButtons) {
405            buttonPanel.setVisibility(View.GONE);
406            mWindow.setCloseOnTouchOutsideIfNotSet(true);
407        }
408
409        FrameLayout customPanel = null;
410        if (mView != null) {
411            customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel);
412            FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
413            custom.addView(mView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
414            if (mViewSpacingSpecified) {
415                custom.setPadding(mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
416                        mViewSpacingBottom);
417            }
418            if (mListView != null) {
419                ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
420            }
421        } else {
422            mWindow.findViewById(R.id.customPanel).setVisibility(View.GONE);
423        }
424
425        /* Only display the divider if we have a title and a
426         * custom view or a message.
427         */
428        if (hasTitle) {
429            View divider = null;
430            if (mMessage != null || mView != null || mListView != null) {
431                divider = mWindow.findViewById(R.id.titleDivider);
432            } else {
433                divider = mWindow.findViewById(R.id.titleDividerTop);
434            }
435
436            if (divider != null) {
437                divider.setVisibility(View.VISIBLE);
438            }
439        }
440
441        setBackground(topPanel, contentPanel, customPanel, hasButtons, a, hasTitle, buttonPanel);
442        a.recycle();
443    }

可以看出,setupView方法主要用來判斷是用默認的佈局還是自定義的佈局以及設置其他信息,至此,佈局的初始化工作就做完了,然後開始顯示佈局。我們回到show方法,接着分析
可以看到,主要的顯示在這段代碼。

        WindowManager.LayoutParams l = mWindow.getAttributes();
276        if ((l.softInputMode
277                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
278            WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
279            nl.copyFrom(l);
280            nl.softInputMode |=
281                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
282            l = nl;
283        }
284
285        try {
286            mWindowManager.addView(mDecor, l);
287            mShowing = true;
288
289            sendShowMessage();
290        } finally {
291        }

設置佈局參數信息,向WindowManager添加布局。自此,Dialog就顯示在屏幕上了。然後通過sendShowMessage發送顯示監聽調用,前提是你設置了顯示監聽。

 private void sendShowMessage() {
 //當mShowMessage 不爲空時才調用
        if (mShowMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mShowMessage).sendToTarget();
        }
    }

當我們設置了setOnShowListener時,就會初始化mShowMessage

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

最終消息通過Handler機制,交由我們創建的Hander去處理

  private static final class ListenersHandler extends Handler {
        private final WeakReference<DialogInterface> mDialog;

        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<>(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;
            }
        }
    }
    dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialog) {
                //todo 顯示後的邏輯
            }
        });

同理,setOnCancelListener,setOnDismissListener也是如此。

4、Dialog 的dismiss

當我們調用dismiss方法時,會從WinddowManager移除添加的View,這樣Dialog就消失了。

@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 {
        //真正移除dialog的方法
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;
			//移除監聽回調
            sendDismissMessage();
        }
    }

四、總結

本文代碼基於Android4.4,文章中存在錯誤或不妥之處,請各位看官留言斧正,謝謝!另附上相關源碼。
Dialog源碼
AlertDialog源碼
AlertController源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章