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源碼