Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

源码中的建造者模式 #31

Open
cnwutianhao opened this issue Mar 13, 2024 · 0 comments
Open

源码中的建造者模式 #31

cnwutianhao opened this issue Mar 13, 2024 · 0 comments

Comments

@cnwutianhao
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant