本文是基于 Android 14 的源码解析
在 Android 源码中,最常用到的建造者模式就是 AlertDialog.Builder,使用该建造者来构建复杂的 AlertDialog 对象。在开发过程中,我们经常用到 AlertDialog,具体示例如下:
private fun showDialog(context: Context) {
val builder = AlertDialog.Builder(context)
with(builder) {
setIcon(R.mipmap.ic_launcher)
setTitle("Title")
setMessage("Message")
setPositiveButton("Positive") { dialog, which ->
...
}
setNeutralButton("Neutral") { dialog, which ->
...
}
setNegativeButton("Negative") { dialog, which ->
...
}
create()
}.show()
}
显示结果如图1所示:
从类名就可以看出这就是一个建造者模式,通过建造者对象来组装 Dialog 的各个部分,如 title、button、message 等,将 Dialog 的构造和表示进行分离。下面看看 AlertDialog 的相关源码:
public class AlertDialog extends AppCompatDialog implements DialogInterface {
// AlertController 接收建造者成员变量 P 中的各个参数
final AlertController mAlert;
...
// 构造函数
protected AlertDialog(@NonNull Context context) {
this(context, 0);
}
// 构造 AlertDialog
protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, resolveDialogTheme(context, themeResId));
// 构造AlertController
mAlert = new AlertController(getContext(), this, getWindow());
}
...
// 实际上调用的是 mAlert 的 setTitle 方法
@Override
public void setTitle(CharSequence title) {
super.setTitle(title);
mAlert.setTitle(title);
}
// 实际上调用的是 mAlert 的 setCustomTitle 方法
public void setCustomTitle(View customTitleView) {
mAlert.setCustomTitle(customTitleView);
}
...
public static class Builder {
// 存储 AlertDialog 的各个参数,如 title、message、icon 等
private final AlertController.AlertParams P;
...
public Builder(@NonNull Context context) {
this(context, resolveDialogTheme(context, 0));
}
public Builder(@NonNull Context context, @StyleRes int themeResId) {
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
mTheme = themeResId;
}
...
// 设置各种参数
public Builder setTitle(@Nullable CharSequence title) {
P.mTitle = title;
return this;
}
public Builder setMessage(@Nullable CharSequence message) {
P.mMessage = message;
return this;
}
public Builder setView(View view) {
P.mView = view;
P.mViewLayoutResId = 0;
P.mViewSpacingSpecified = false;
return this;
}
...
// 构建 AlertDialog, 传递参数
@NonNull
public AlertDialog create() {
// 调用 new AlertDialog 构造对象,并且将参数传递给个体 AlertDialog
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
// 将 P 中的参数应用到 dialog 中的 mAlert 对象中
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;
}
public AlertDialog show() {
final AlertDialog dialog = create();
dialog.show();
return dialog;
}
}
}
上述代码中,Builder 类可以设置 AlertDialog 中的 title、message、button 等参数,这些参数都存储在类型为 AlertController.AlertParams 的成员变量 P 中,AlertController.AlertParams 中包含了与 AlertDialog 视图中对应的成员变量。在调用 Builder 类的 create 函数时会创建 AlertDialog,并且将 Builder 成员变量 P 中保存的参数应用到 AlertDialog 的 mAlert 对象中,即 P.apply(dialog.mAlert) 代码段。我们再看看 apply 函数的实现:
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 || mPositiveButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
mPositiveButtonListener, null, mPositiveButtonIcon);
}
if (mNegativeButtonText != null || mNegativeButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
mNegativeButtonListener, null, mNegativeButtonIcon);
}
if (mNeutralButtonText != null || mNeutralButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
mNeutralButtonListener, null, mNeutralButtonIcon);
}
// 如果设置了 mItems,则表示是单选或者多选列表,此时创建—个 ListView
if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
createListView(dialog);
}
// 将 mView 设置给 Dialog
if (mView != null) {
if (mViewSpacingSpecified) {
dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
mViewSpacingBottom);
} else {
dialog.setView(mView);
}
} else if (mViewLayoutResId != 0) {
dialog.setView(mViewLayoutResId);
}
}
在 apply 函数中,只是将 AlertParams 参数设置到 AlertController 中,例如,将标题设置到 Dialog 对应的标题视图中,将 Message 设置到内容视图中等。当我们获取到 AlertDialog 对象后,通过 show 函数就可以显示这个对话框。我们看看 Dialog 的 show 函数:
public void show() {
...
if (!mCreated) {
dispatchOnCreate(null);
} else {
final Configuration config = mContext.getResources().getConfiguration();
mWindow.getDecorView().dispatchConfigurationChanged(config);
}
onStart();
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
mWindowManager.addView(mDecor, l);
...
}
在 show 函数中主要做了如下几个事情:
- 通过 dispatchOnCreate 函数来调用 AlertDialog 的 onCreate 函数。
- 然后调用 AlertDialog 的 onStart 函数。
- 最后将 Dialog 的 DecorView 添加到 WindowManager 中。
很明显,这就是一系列典型的生命周期函数。那么按照惯例,AlertDialog 的内容视图构建按理应该在 onCreate 函数中,我们来看看是不是:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
}
在 onCreate 函数中主要调用 了AlertController 的 installContent 方法,Dialog 中的 onCreate 函数只是一个空实现而己,可以忽略它。那么 AlertDialog 的内容视图必然就在 installContent 函数中:
public void installContent() {
final int contentView = selectContentView();
mDialog.setContentView(contentView);
setupView();
}
installContent 函数的代码很少,但极为重要,它调用了 Window 对象的 setContentView,这个 setContentView 就与 Activity 中的一模一样,实际上 Activity 最终也是调用 Window 对象的 setContentView 函数。因此,这里就是设置 AlertDialog 的内容布局,这个布局就是 mAlertDialogLayout 字段的值,这个值在 AlertController 的构造函数中进行了初始化,具体代码如下:
public AlertController(Context context, AppCompatDialog di, Window window) {
...
final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
R.attr.alertDialogStyle, 0);
mAlertDialogLayout = a.getResourceId(R.styleable.AlertDialog_android_layout, 0);
...
a.recycle();
...
}
用图2来大致描述一下 AlertDialog 的布局结构:
当通过 Builder 对象的 setTitle、setMessage 等方法设置具体内容时,就是将这些内容填充到对应的视图中。而 AlertDialog 也允许你通过 setView 传入内容视图,这个内容视图就是替换掉图2的内容区域,AlertDialog 预留了一个 customPanel 区域用来显示用户自定义的内容视图。我们来看看 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);
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);
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;
if (!hasButtonPanel) {
if (contentPanel != null) {
final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
if (spacer != null) {
spacer.setVisibility(View.VISIBLE);
}
}
}
if (hasTopPanel) {
if (mScrollView != null) {
mScrollView.setClipToPadding(true);
}
View divider = null;
if (mMessage != null || mListView != null) {
divider = topPanel.findViewById(R.id.titleDividerNoCustom);
}
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);
}
if (!hasCustomPanel) {
final View content = mListView != null ? mListView : mScrollView;
if (content != null) {
final int indicators = (hasTopPanel ? ViewCompat.SCROLL_INDICATOR_TOP : 0)
| (hasButtonPanel ? ViewCompat.SCROLL_INDICATOR_BOTTOM : 0);
setScrollIndicators(contentPanel, content, indicators,
ViewCompat.SCROLL_INDICATOR_TOP | ViewCompat.SCROLL_INDICATOR_BOTTOM);
}
}
final ListView listView = mListView;
if (listView != null && mAdapter != null) {
listView.setAdapter(mAdapter);
final int checkedItem = mCheckedItem;
if (checkedItem > -1) {
listView.setItemChecked(checkedItem, true);
listView.setSelection(checkedItem);
}
}
}
这个 setupView 方法的名字已经很直观了:它用于初始化 AlertDialog 布局中的各个部分,例如标题区域、按钮区域和内容区域。在调用此函数之后,整个对话框的视图内容都会被设置完毕。这些不同区域的视图都是 mAlertDialogLayout 布局的子元素。Window 对象与整个 mAlertDialogLayout 的布局树相关联。当 setupView 调用完成后,整个视图树的数据都被填充完毕。当用户调用 show 函数时,WindowManager 会将 Window 对象的 DecorView(也就是对应于 mAlertDialogLayout 的视图)添加到用户的窗口上,并显示出来。
总之,AlertDialog 的建造者模式使得创建和配置对话框变得更加灵活和易于维护。通过链式调用,我们可以按需设置对话框的各个属性,而不必关心参数的顺序或数量。