本系列博客基于android-28版本
【Window系列】——Toast源码解析
【Window系列】——PopupWindow的前世今生
【Window系列】——Dialog源码解析
【Window系列】——Window中的Token
前言
前面两篇博客分别分析了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
,至此,Builder
和AlertController.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();
}
可以看到最终调用了Window
的setContentView
方法,看过前面一篇博客(【重拾View(一)】——setContentView()源码解析)的应该熟悉这个方法,这个方法是创建DecorView
并,把我们的布局文件,添加到DecorView
中。这里注意两点
Window
的创建的时机,- 根布局文件
这里看到其实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
。
这里简单的看一下这个布局的布局结构,可以看到和我们设置的属性基本是相同。
最后看一下
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
对象,然后设置到了Dialog
到ContentView
中,也就是说我们在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
中,dismiss
了Dialog
。
总结
所以最终我们会发现,其实DialogFragment
整个生命周期中贯穿着对于Dialog
的使用,DialogFragment
其实是对于Dialog
的一种包装类的思想,不仅将Dialog
单独抽出来成为一个个体,并且利用Fragment
的特性,赋予了Dialog
生命周期的能力,可以看出Google
对于Frament
感知生命周期的特性的利用其实很早就已经开始了,而Google
新出的JetPack
框架中也是重复利用了Fragment
的特性完成的。
本篇博客只是讲解了Dialog
的源码实现和DialogFragment
的拓展使用,但是Dialog
还有一个更为重要的知识点这里没有分析,和前面几篇博客一样,就是对于token
对象的分析。熟悉Dialog
的应该清楚,Dialog
的构建必须传入一个Activity
类型的Context
,如果传入的是Application
,则会抛异常,这里面的缘由也是由于token
对象引起的,所以下一篇博客应该是【Window】系列的终篇,讲一讲关于Window
中token
和type
的那些事。