IT行业,一直讲一句话,拼到最后都拼的是“内功”,而内功往往就是指我们处理问题的思路、经验、想法,而对于开发者来说,甚至对于产品也一样,都离不开一个“宝典”,就是设计模式。今天我们一起借助Android源码去探索一下建造者模式的优缺点,以及它所想要去解决的问题。同时结合我工作经验中的一个小例子,来总结实践一下。
1.背景&定义
理解:
建造者模式是创建性设计模式的一种。是我们最常见、也可能是开发者肯定会使用的一种设计模式。
先从建造者
这个词来理解,应该是用于建造一个东西而存在的设计模式。现实生活中对应的人或物或者事情,在代码的世界中,都可以通过行为和属性抽象为一个对象,而往往对象越复杂,new一个对象时,我们需要不断的set去创建、而且,复杂对象的组装过程,也是需要特定的顺序,这时,为了解耦构建过程和组装过程,建造者模式应运而生。
**我的理解:**该模式是为了将复杂对象的构建过程和组装过程相分离,对外不可见。我们知道设计模式离不开一个词解耦
,建造者模式,为了解耦 构建过程和组装过程,使构建过程可动态扩展,对组装过程进行封装
定义:
将一个复杂对象的构建与表示相分离,使得同样的构建过程可以创建不同的表示。
2.UML类图设计
3.源码中的建造者模式
在Android源码中,最常使用的Builder模式就是AlertDialog.Builder。使用该Builder创建不同的复杂的AlertDialog对象。我们接下来分析一下AlertDialog源码。I看一下android源码如何实现的?是否和我们上面想的UML一样。
首先看到AlertDialog内部有一个内部类,Builder类,不出所料的话,这应该是一个静态内部类(为什么是静态内部类?记得【Android进阶】篇章里面我们说到的内部类引起的内存泄露了吗?如果忘记了,快去复习一下吧!!!)
AlertDialog.Builder
//看到了没有,这里是静态内部类哦
public static class Builder {
@UnsupportedAppUsage
//的确是使用了builder方式,但是AlertDialog这里使用了一个AlertParams
private final AlertController.AlertParams P;
/**
* Creates a builder for an alert dialog that uses the default alert
* dialog theme.
* <p>
* The default alert dialog theme is defined by
* {@link android.R.attr#alertDialogTheme} within the parent
* {@code context}'s theme.
*
* @param context the parent context
*/
public Builder(Context context) {
this(context, resolveDialogTheme(context, Resources.ID_NULL));
}
//先省略很多代码
//....
}
从这里源码分析,的确是使用了builder方式,但是AlertDialog这里使用了一个AlertParams,从字面意思理解,是Dialog的参数的一个类,我们点进去看一下,果不其然,AlertParams是封装所有Dialog属性参数的一个类而已。AlertController.AlertParams.java
public static class AlertParams {
public final Context mContext;
public final LayoutInflater mInflater;
//省略很多代码,从这里已经能看到,这里包含了很多属性参数
public int mIconId = 0;
public Drawable mIcon;
public int mIconAttrId = 0;
public CharSequence mTitle;
public View mCustomTitleView;
public CharSequence mMessage;
public CharSequence mPositiveButtonText;
public Drawable mPositiveButtonIcon;
public DialogInterface.OnClickListener mPositiveButtonListener;
public CharSequence mNegativeButtonText;
public Drawable mNegativeButtonIcon;
public DialogInterface.OnClickListener mNegativeButtonListener;
public CharSequence mNeutralButtonText;
public Drawable mNeutralButtonIcon;
public DialogInterface.OnClickListener mNeutralButtonListener;
public boolean mCancelable;
public DialogInterface.OnCancelListener mOnCancelListener;
public DialogInterface.OnDismissListener mOnDismissListener;
public DialogInterface.OnKeyListener mOnKeyListener;
public CharSequence[] mItems;
public ListAdapter mAdapter;
public DialogInterface.OnClickListener mOnClickListener;
public int mViewLayoutResId;
public View mView;
public int mViewSpacingLeft;
public int mViewSpacingTop;
public int mViewSpacingRight;
public int mViewSpacingBottom;
public boolean mViewSpacingSpecified = false;
public boolean[] mCheckedItems;
public boolean mIsMultiChoice;
public boolean mIsSingleChoice;
public int mCheckedItem = -1;
public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener;
public Cursor mCursor;
public String mLabelColumn;
public String mIsCheckedColumn;
public boolean mForceInverseBackground;
public AdapterView.OnItemSelectedListener mOnItemSelectedListener;
public OnPrepareListViewListener mOnPrepareListViewListener;
public boolean mRecycleOnMeasure = true;
}
从这里已经能看到,这里包含了很多属性参数,这也是AlertDialog源码比较巧妙的一点,以后大家自己实现复杂对象的Builder模式时,我们也可以不要把所有的代码写在一个类中,可以把这部分参数属性分离(单一原则 & 其他模块也可复用)。
好了,我们接着看,AlertDialog.Builder
里面的具体方法
public static class Builder {
public Builder(Context context, int themeResId) {
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
}
public Context getContext() {
return P.mContext;
}
public Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
public Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}
public Builder setCustomTitle(View customTitleView) {
P.mCustomTitleView = customTitleView;
return this;
}
public Builder setMessage(@StringRes int messageId) {
P.mMessage = P.mContext.getText(messageId);
return this;
}
//构建过程中,一直填充的是AlertParams属性
public Builder setMessage(CharSequence message) {
P.mMessage = message;
return this;
}
//真正调用create时,才去创建了AlertDialog 对象,并且把AlertParams的参数,一一对应赋值给了AlertDialog
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;
}
我们看一下 P.apply(dialog.mAlert);
``AlertParams.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);
}
//......
}
可以看到这里,的确是把AlertParams的参数,一一对应赋值给了AlertDialog 。看到这里,这时完成了AlertDialog的创建过程。但是最重要的show还没有看,我们继续扒一扒。
知识点引申扩展:Dialog show的源码分析
public void show() {
//当前是否正在显示
if (mShowing) {
//当前view不为null,并且有菜单标题栏,则去创建并显示
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
//如果正在显示中,则return
return;
}
mCanceled = false;
if (!mCreated) {
//如果没有创建完成,代码view的contentview还未创建完成,则执行view的创建过程,就是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);
}
//执行Dialog的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;
}
//将当前视图按照布局参数,添加到当前activity所处window的视图中
mWindowManager.addView(mDecor, l);
if (restoreSoftInputMode) {
l.softInputMode &=
~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
}
mShowing = true;
sendShowMessage();
}
Dialog.show函数关键做了以下三步:
1)dispatchOnCreate,调用Dialog的onCreate,创建视图view
2)执行Dialog的onStart方法
3)将当前视图按照布局参数,添加到当前dialog所处window的视图中
Dialog.dispatchOnCreate
很明显,其实Dialog的创建,也是一系列自定义生命周期函数的调用过程,我们接下来看一下AlertDialog的onCreate
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//mAlert在上面参数构建分析的时候,我们知道是一个封装类AlertController
mAlert.installContent();
}
AlertController.installContent
AlertController#installContent
public void installContent() {
//选择指定的视图布局
final int contentView = selectContentView();
//window设置内容视图布局
mDialog.setContentView(contentView);
//初始化视图内容
setupView();
}
默认视图结构
Dialog.setContentView
这里获取到layoutID之后,调用了Dialog.setContentView
方法
/**
* Set the screen content from a layout resource. The resource will be
* inflated, adding all top-level views to the screen.
*
* @param layoutResID Resource ID to be inflated.
*/
public void setContentView(@LayoutRes int layoutResID) {
//这里的window是个啥,我们在Dialog的源码中找一找
mWindow.setContentView(layoutResID);
}
我们跟踪源码,可以看到是在Dialog的构造函数里面创建的Dialog#构造函数·
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == Resources.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;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//可以看到创建了一个phonwwindow,这里很关键,我们知道单独的activity本身就是一个PhoneWindow,里面有DecorView,从这里我们可以看出,Dialog的创建,实际是单独的一个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);
mListenersHandler = new ListenersHandler(this);
}
小结
调用AlertDialog的show函数之后,其实就是调用了AlertDialog的一系列生命函数,完成PhoneWindow的创建、视图的创建、视图的内容设置,然后通过WiindowManager,将创建的view add进去,最终用户就可以看到这个Dialog。
–引入一个小的课后作用知识点,大家通过上面代码,知道了DIalog其实是创建了一个新的PhoneWindow,我们之前【Android进阶】中讲到过,activity实际上在Acitviy中,也会创建PhoneWindow对象, 这两者有什么区别吗?
4.使用场景&优缺点总结
Builder模式在Android开发中较为常用,通常作为配置类的构建器将配置的构建和表示分离开来,同时也是将配置从目标类中隔离出来,避免过多的setter方法。Builder模式比较常见的实现形式是通过调用链实现,这样的代码更简洁、易懂。
4.1 使用场景
(1)相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式
(2)多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
(3)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适
(4)在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程
4.2 优点
(1)良好的封装性,使用构建者模式可以使客户端不必知道内部的组成细节。
(2)建造者独立,容易扩展。
4.3 缺点
会产生多于的Builder对象以及Director对象,消耗内存。
5.实践经验总结
5.1 自定义NavigationBar的设计
android开发中,顶部导航栏是常用的一个控件,如下
其实导航栏无非以下几个步骤:
1)加载导航栏布局文件
2)构建视图元素
3)设置视图元素的文本、事件
4)添加到父视图,最后展示
5)这里不妨,我们增加一条,写一个固定的view很简单,如果支持扩展,需要考虑一下
package com.itbird.design.builder.navigationbar;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
/**
* 自定义NavigationBar
* Created by itbird on 2022/5/26
*/
public class NavigationBar {
protected NavigationBar() {
}
public static class Builder {
/**
* 视图布局id
*/
int layoutID;
View mCurrentView;
ViewGroup parentView;
/**
* 初始化view
*
* @param context
* @param layoutID
* @param rootView
*/
public Builder(Context context, int layoutID, ViewGroup rootView) {
this.layoutID = layoutID;
this.parentView = rootView;
mCurrentView = LayoutInflater.from(context).inflate(layoutID, rootView, false);
}
public Builder setBackColor(int color) {
mCurrentView.setBackgroundColor(color);
return this;
}
/**
* 设置textview文本
*
* @param viewId
* @param text
* @return
*/
public Builder setTextToTextView(int viewId, String text) {
TextView textView = findViewByID(viewId);
textView.setText(text);
return this;
}
/**
* 设置button文本
*
* @param viewId
* @param text
* @return
*/
public Builder setTextToButtonView(int viewId, String text) {
Button button = findViewByID(viewId);
button.setText(text);
return this;
}
/**
* 设置button事件
*
* @param viewId
* @param onClickListener
* @return
*/
public Builder setOnClickListenerToButtonView(int viewId, View.OnClickListener onClickListener) {
Button button = findViewByID(viewId);
button.setOnClickListener(onClickListener);
return this;
}
/**
* 展示view
*
* @return
*/
public ViewGroup show() {
parentView.addView(mCurrentView, 0);
return parentView;
}
public <T extends View> T findViewByID(int viewID) {
return mCurrentView.findViewById(viewID);
}
}
}
使用一下
private void testBuilderPatterm() {
new NavigationBar.Builder(MainActivity.this, R.layout.navigation_layout, (ViewGroup) getWindow().getDecorView())
.setBackColor(com.google.android.material.R.color.design_default_color_on_secondary)
.setTextToButtonView(R.id.back_button, "返回")
.setTextToTextView(R.id.title_textview, "我是标题")
.setOnClickListenerToButtonView(R.id.back_button, new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
}).show();
}
5.2 通用Dialog框架设计
业务开发中,经常会开发各种各样的弹窗样式,例如:倒计时弹窗、进度条弹窗、等待弹窗、通知弹窗、两个button的弹窗、单个button的弹窗等等,在一个app或者一个系统中,往往弹窗风格肯定是统一的,所以大家为了方便使用,一般都会封装各种框架,这里小编一起和大家封装一个Dialog框架,满足以下需求
1)弹窗可以根据是否设置了哪些信息,去自动选择不同的dialog布局,例如:如果只设置了一个button的文本和事件,那么选择只有一个button的layout;如果设置了消息,则有通知区域,反之没有通知区域;
2)弹窗框架,需要做一些异常规避,而不是导致调用者的app崩溃,例如用户调用没有设置title,那么抛出相应异常或者错误给到调用者;例如没有找到相应layoutID,则通知调用者,而不是应用崩溃;
3)弹窗框架,需要包含多种样式,例如进度弹窗、倒计时弹窗
效果如下:
话不多说,我们直接上代码。
5.2.1 公共弹窗控件封装
package com.itbird.design.builder.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import com.itbird.design.R;
import java.lang.ref.WeakReference;
/**
* 公共弹窗框架封装
* Created by xfkang on 2020/5/23.
*/
public class CommonDialog extends Dialog implements DialogInterface {
private static final int DIALOG_STYLE_SMALL = 1;
private static final int DIALOG_STYLE_NORMAL = 2;
private static final int DIALOG_STYLE_HIGH = 3;
private ButtonHandler handler;
private View rootView;
private int dialogStyle;
private TextView titleTextView;
private TextView messageTextView;
private Button positiveButton;
private Button negativeButton;
private Message positiveMessage;
private Message negativeMessage;
public CommonDialog(@NonNull Context context) {
super(context);
}
public CommonDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, themeResId);
}
protected CommonDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
CommonDialog(Builder builder) {
super(builder.context, R.style.common_dialog_style);
rootView = LayoutInflater.from(builder.context).inflate(getInflateLayout(builder), null);
setContentView(rootView);
setupView();
handler = new ButtonHandler(this);
setWindowStyle();
}
private void setWindowStyle() {
Window window = getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
int width = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_width);
int height = 0;
switch (dialogStyle) {
case DIALOG_STYLE_SMALL:
height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_small_height);
break;
case DIALOG_STYLE_NORMAL:
height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_normal_height);
break;
default:
height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_small_height);
break;
}
layoutParams.width = width;
layoutParams.height = height;
window.setAttributes(layoutParams);
}
private void setupView() {
titleTextView = (TextView) findViewById(R.id.title);
messageTextView = (TextView) findViewById(R.id.message);
positiveButton = (Button) findViewById(R.id.positive_button);
if (positiveButton != null) {
positiveButton.setOnClickListener(mButtonHandler);
}
negativeButton = (Button) findViewById(R.id.negative_button);
if (negativeButton != null) {
negativeButton.setOnClickListener(mButtonHandler);
}
}
public void setTitle(String title) {
if (titleTextView != null) {
titleTextView.setText(title);
}
}
public void setMessage(String message) {
if (messageTextView != null) {
messageTextView.setText(message);
}
}
public void setPositiveButton(String text, final OnClickListener onClickListener) {
if (positiveButton != null) {
positiveButton.setText(text);
if (onClickListener != null) {
positiveMessage = handler.obtainMessage(DialogInterface.BUTTON_POSITIVE, onClickListener);
}
}
}
public void setNegativeButton(String text, final OnClickListener onClickListener) {
if (negativeButton != null) {
negativeButton.setText(text);
if (onClickListener != null) {
negativeMessage = handler.obtainMessage(DialogInterface.BUTTON_NEGATIVE, onClickListener);
}
}
}
private int getInflateLayout(Builder builder) {
if (TextUtils.isEmpty(builder.title)) {
throw new IllegalStateException("No title for dialog");
}
int layoutResID = 0;
if (!TextUtils.isEmpty(builder.message)
&& TextUtils.isEmpty(builder.positiveText)
&& TextUtils.isEmpty(builder.negativeText)) {
layoutResID = R.layout.common_no_button_dialog;
dialogStyle = 1;
}
if (TextUtils.isEmpty(builder.message)
&& !TextUtils.isEmpty(builder.positiveText)
&& TextUtils.isEmpty(builder.negativeText)) {
layoutResID = R.layout.common_no_message_one_button_dialog;
dialogStyle = 1;
}
if (TextUtils.isEmpty(builder.message)
&& !TextUtils.isEmpty(builder.positiveText)
&& !TextUtils.isEmpty(builder.negativeText)) {
layoutResID = R.layout.common_no_message_two_button_dialog;
dialogStyle = 1;
}
if (!TextUtils.isEmpty(builder.message)
&& !TextUtils.isEmpty(builder.positiveText)
&& !TextUtils.isEmpty(builder.negativeText)) {
layoutResID = R.layout.common_message_two_button_dialog;
dialogStyle = 2;
}
if (!TextUtils.isEmpty(builder.message)
&& !TextUtils.isEmpty(builder.positiveText)
&& TextUtils.isEmpty(builder.negativeText)) {
layoutResID = R.layout.common_message_one_button_dialog;
dialogStyle = 2;
}
if (layoutResID == 0) {
throw new IllegalStateException("Not have this dialog");
}
return layoutResID;
}
private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
@Override
public void onClick(View v) {
final Message m;
if (v == positiveButton && positiveMessage != null) {
m = Message.obtain(positiveMessage);
} else if (v == negativeButton && negativeMessage != null) {
m = Message.obtain(negativeMessage);
} else {
m = null;
}
if (m != null) {
m.sendToTarget();
}
handler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, CommonDialog.this)
.sendToTarget();
}
};
public static class Builder {
private String title;
private String message;
private String positiveText;
private OnClickListener positiveOnClickListener;
private String negativeText;
private OnClickListener negativeOnCLickListener;
private boolean cancelable = true;
private OnCancelListener onCancelListener;
private OnDismissListener onDismissListener;
private OnKeyListener onKeyListener;
private final Context context;
public Builder(Context context) {
this.context = context;
}
public Builder setTitle(@StringRes int resID) {
this.title = context.getResources().getString(resID);
return this;
}
public Builder setTitle(String title) {
this.title = title;
return this;
}
public Builder setMessage(@StringRes int resID) {
this.message = context.getResources().getString(resID);
return this;
}
public Builder setMessage(String message) {
this.message = message;
return this;
}
public Builder setPositiveButton(@StringRes int resID, OnClickListener onClickListener) {
this.positiveText = context.getResources().getString(resID);
this.positiveOnClickListener = onClickListener;
return this;
}
public Builder setPositiveButton(String text, OnClickListener onClickListener) {
this.positiveText = text;
this.positiveOnClickListener = onClickListener;
return this;
}
public Builder setNegativeButton(@StringRes int resID, OnClickListener onClickListener) {
this.negativeText = context.getResources().getString(resID);
this.negativeOnCLickListener = onClickListener;
return this;
}
public Builder setNegativeButton(String text, OnClickListener onClickListener) {
this.negativeText = text;
this.negativeOnCLickListener = onClickListener;
return this;
}
public Builder setCancelable(boolean cancelable) {
this.cancelable = cancelable;
return this;
}
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
this.onCancelListener = onCancelListener;
return this;
}
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
return this;
}
public Builder setOnKeyListener(OnKeyListener onKeyListener) {
this.onKeyListener = onKeyListener;
return this;
}
public CommonDialog create() {
CommonDialog commonDialog = new CommonDialog(this);
apply(commonDialog);
commonDialog.setCancelable(cancelable);
if (cancelable) {
commonDialog.setCanceledOnTouchOutside(true);
}
commonDialog.setOnCancelListener(onCancelListener);
commonDialog.setOnDismissListener(onDismissListener);
if (onKeyListener != null) {
commonDialog.setOnKeyListener(onKeyListener);
}
return commonDialog;
}
public CommonDialog show() {
CommonDialog commonDialog = create();
commonDialog.show();
return commonDialog;
}
private void apply(CommonDialog commonDialog) {
commonDialog.setTitle(title);
commonDialog.setMessage(message);
commonDialog.setPositiveButton(positiveText, positiveOnClickListener);
commonDialog.setNegativeButton(negativeText, negativeOnCLickListener);
}
}
/**
* 使用handler进行事件转发
* 为了防止内存泄露,使用弱引用
*/
private static final class ButtonHandler extends Handler {
private static final int MSG_DISMISS_DIALOG = 1;
private WeakReference<DialogInterface> mDialog;
public ButtonHandler(DialogInterface dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DialogInterface.BUTTON_POSITIVE:
case DialogInterface.BUTTON_NEGATIVE:
case DialogInterface.BUTTON_NEUTRAL:
((OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
break;
case MSG_DISMISS_DIALOG:
((DialogInterface) msg.obj).dismiss();
}
}
}
}
5.2.2 倒计时控件封装
倒计时实现方式有很多种,例如Rxjava、TimerTask等,但是我们是为了去封装一个控件,所以肯定不会去在框架中引用各种第三方框架的,应该去研究他们内部怎么去实现。不过我想应该逃脱不了handler、thread这些关键词吧。
实现方式
1)基于android.os.CountDownTimer
的源码来设计实现,我们知道android原生这个控件是通过handler.postDelay来实现的,而且里面有进度回调,但是没有暂停和恢复,所以我们需要添加onPause、onRestart自定义方法,
2)基于android.widget.TextClock
的源码来设计实现,们知道android原生这个控件是通过handler.postAtTime + thread来实现的,我们之前是通过postDelay来触发消息事件的,但这里系统使用了postAtTime,这样就是设置了在某一个时间点抛出handler消息,前面的
long now = SystemClock.uptimeMillis();
long next = now + (1000 - now % 1000);
精确控制了1秒的整点时间,因此系统可以在每一个整秒的时间点发出消息。
CustomCountDownTimer.java
package com.itbird.design.builder.dialog;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
/**
* 使用android.os.CountDownTimer的源码
* 添加了onPause、onRestart自定义方法
* Created by xfkang on 16/3/18.
*/
public abstract class CustomCountDownTimer {
private static final int MSG = 1;
/**
* 总倒计时时间
* Millis since epoch when alarm should stop.
*/
private final long mMillisInFuture;
/**
* 倒计时间隔时间
* The interval in millis that the user receives callbacks
*/
private final long mCountdownInterval;
/**
* 记录开始之后,应该停止的时间节点
*/
private long mStopTimeInFuture;
/**
* 记录暂停的时间节点
*/
private long mPauseTimeInFuture;
/**
* 对应于源码中的cancle,即计时停止时
* boolean representing if the timer was cancelled
*/
private boolean isStop = false;
private boolean isPause = false;
/**
* @param millisInFuture 总倒计时时间
* @param countDownInterval 倒计时间隔时间
*/
public CustomCountDownTimer(long millisInFuture, long countDownInterval) {
// 解决秒数有时会一开始就减去了2秒问题(如10秒总数的,刚开始就8999,然后没有不会显示9秒,直接到8秒)
if (countDownInterval > 1000) {
millisInFuture += 15;
}
mMillisInFuture = millisInFuture;
mCountdownInterval = countDownInterval;
}
private synchronized CustomCountDownTimer start(long millisInFuture) {
isStop = false;
if (millisInFuture <= 0) {
onFinish();
return this;
}
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return this;
}
/**
* 开始倒计时
*/
public synchronized final void start() {
start(mMillisInFuture);
}
/**
* 停止倒计时
*/
public synchronized final void stop() {
isStop = true;
mHandler.removeMessages(MSG);
}
/**
* 暂时倒计时
* 调用{@link #restart()}方法重新开始
*/
public synchronized final void pause() {
if (isStop) return;
isPause = true;
mPauseTimeInFuture = mStopTimeInFuture - SystemClock.elapsedRealtime();
mHandler.removeMessages(MSG);
}
/**
* 重新开始
*/
public synchronized final void restart() {
if (isStop || !isPause) return;
isPause = false;
start(mPauseTimeInFuture);
}
/**
* 倒计时间隔回调
*
* @param millisUntilFinished 剩余毫秒数
*/
public abstract void onTick(long millisUntilFinished);
/**
* 倒计时结束回调
*/
public abstract void onFinish();
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (CustomCountDownTimer.this) {
if (isStop || isPause) {
return;
}
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
if (millisLeft <= 0) {
onFinish();
} else if (millisLeft < mCountdownInterval) {
// no tick, just delay until done
sendMessageDelayed(obtainMessage(MSG), millisLeft);
} else {
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
// take into account user's onTick taking time to execute
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
// special case: user's onTick took more than interval to
// complete, skip to next interval
while (delay < 0) delay += mCountdownInterval;
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};
}
小结
不管使用哪种方式实现倒计时,一般绕不过去handler和thread,使用这两者需要解决两个问题,一个是handler持有引用导致内存泄露问题,一个是handler postDealy会有消息处理第一次的跳变问题(如果使用handler.postDealyed(……, 1000)方式来进行每秒的计时,是不准确的,是的,有很大误差,误差的原因在于在你收到消息,到你重新发出handler.postDealyed的时间,并不是瞬间完成的,这里面有很多逻辑处理的时间,即使没有逻辑处理的时间,handler本身也是耗损性能的,所以消息并不可能按照理想的1000延迟来进行发送,这就导致了误差的累积)
1)内存泄露的问题解决方法:弱引用
2)跳变问题的解决方法:通过时间校准(实现方式也有多种,例如TextClock的postAttime(now+1000-now%1000)
,或者CountDownTimer去加一定的时间,但是这个不太好控制,不建议使用这种),来确保消息是在整数节点发出
6.第三方框架中的建造者模式(Glide、Retrofit)
Retrofit相信大家不陌生了,我们也知道里面依赖了okhttp,OkHttpClient,这个是整个OkHttp的核心管理类,内部包含了请求调度器(Dispatcher),请求拦截器(interceptors),代理,读写超时时间等各种需要配置的对象。
我们反过来结合之前所总结的建造者设计模式适用的场景,来理解一下。这里不就符合第一条吗?
多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
这些配置的对象就是构建OkhttpClient的多个部件/零件。配置不同,导致的运行结果也不同。就像读写超时配置的超时时间不同,导致请求结果允许超时时间也不同。
接下来,我们看一下OkHttpClient中如何创建对象的。(由于okttp这个类代码比较多,我们直接截图说明重点)
我们看到OkHttpClient构造器里面传入了Builder,OkHttpClient的所有属性其实都依赖获取于Builder,这个Builder是个啥?
其实就是OkHttpClient的一个内部类而已,这不正符合了使用场景中的3吗?
(3)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适
还有一点,其实在这个过程中也用到了,不知道大家感觉到了没有,就是第4点
(4)在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程
我们通过Builder去设置各种参数属性时,对于使用者来说,只需要关注这些需要设置的属性,而不需要关心,OkHttpClient内部用这些属性,去组合构造了不同的对象。这就是上面说的这点了。其实这也是符合我们之前所讲的,面向对象六大基本原则中的迪米特原则(最少知道原则)
。
其实说白了,就是您封装一个框架(通过各种设计模式),肯定要做到一点,对于调用者来说,不用关心内部细节,例如用哪些属性组合什么对象,而且可以尽量规避各种由于调用者输入异常、输入丢失导致的框架异常等问题。
封装一个框架需要做到的:
1)对于调用者来说,集成简单、使用简单,只需关注需要设置的参数即可
2)框架内部,对于异常参数输入、内部执行异常,有回调、规避手段
3)最重要的一点,设计一个框架,不可能完美覆盖所有的用户需求,所以要通过接口和抽象,去支持扩展,而非去修改您的框架去达到目的
整体设计模式Demo代码