本系列博客基于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
,至此,Builder
和AlertController.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)用了Window
的setContentView
方法,看過前面一篇博客(【重拾View(一)】——setContentView()源碼解析)的應(yīng)該熟悉這個(gè)方法,這個(gè)方法是創(chuàng)建DecorView
并,把我們的布局文件,添加到DecorView
中。這里注意兩點(diǎn)
Window
的創(chuàng)建的時(shí)機(jī),- 根布局文件
這里看到其實(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è)置的屬性基本是相同。
最后看一下
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è)置到了Dialog
到ContentView
中,也就是說我們在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
中,dismiss
了Dialog
。
總結(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)于Window
中token
和type
的那些事。