【Android】源码中的建造者模式

本文是基于 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所示:
请添加图片描述

图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 函数中主要做了如下几个事情:

  1. 通过 dispatchOnCreate 函数来调用 AlertDialog 的 onCreate 函数。
  2. 然后调用 AlertDialog 的 onStart 函数。
  3. 最后将 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 的布局结构:
请添加图片描述

图2

当通过 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 的建造者模式使得创建和配置对话框变得更加灵活和易于维护。通过链式调用,我们可以按需设置对话框的各个属性,而不必关心参数的顺序或数量。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴同学是个程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值