目录
写在前面
最近在看辉哥的视频,看到视频里他写Dialog的方式,又想到了自己写的,瞬间觉得尴尬无比,自己的代码真的就像一坨XIANG!不说了,这是辉哥的简书地址:https://www.jianshu.com/u/35083fcb7747,有兴趣的可以加波关注,反正我是对他佩服的五体投地,不只是写代码哦,各方面都是!
再回到本篇中来,不知不觉又快到五一了,天气好自然而然的心情也变好了,这么好的天气不撸代码真的可惜了,于是乎就把辉哥视频中讲的内容自己跟着搞了一把,就有了今天这一篇——建造者模式构建万能Dialog了!看过我博客的小伙伴们应该都还记得关于Dialog的使用,我之前也是写过的,而且还写了两篇,有兴趣的可以看一下:
- Android自定义通用的Dialog:https://blog.youkuaiyun.com/JArchie520/article/details/79157471
- Android自定义View之通用Dialog:https://blog.youkuaiyun.com/JArchie520/article/details/103454231
对于Dialog的使用没有任何的难度,但是如何让你写的Dialog功能上更加通用,架构设计上更加简洁并且具有可扩展性,这就需要你认真的思考了,大家看我之前写的两篇就能够看出代码层次上也是一次比一次清晰,易用性一次比一次要好,虽然很low,但是也说明了咱也是在不断进步的对吧,那为了更加易用,程序的可移植性更好,我决定继续对它改造升级和优化,结合设计模式来封装一个通用型的Dialog,之所以选择Builder设计模式,是因为系统的AlertDialog也是采用这种模式来实现的,可以链式调用非常方便。
效果展示:
一、什么是Builder模式
定义:将一个复杂对象的构建与它的表示分离,使得不同的构建过程可以创建不同的显示,但其根本还是不变。
使用场景:
- ①、相同的方法,不同的执行顺序,产生不同的事件结果时;
- ②、多个部件或零件都可以装配到一个对象中,但是产生的运行结果又不相同时;
- ③、产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能时;
二、AlertDialog源码分析
2.1、源码阅读
既然我们要采用构建者模式来实现一个Dialog,又因为Android系统的AlertDialog就是采用构建者模式来实现的,那这一部分就先来看下谷歌的大神们是如何实现的?下面这行代码是我们在应用层最简单的一个api调用了,接下来我们就按照这行代码来依次去到Android系统的源码中查看一下底层是如何实现的:
new AlertDialog.Builder(this).setTitle("测试").setIcon(R.mipmap.ic_launcher).create().show();
以下源码基于Android6.0源码分析:文件目录:frameworks/base/core/java/android/app/AlertDialog.java(不要找错了)
首先进入AlertDialog类中,可以看到它的构造方法是protected类型的,这也就意味着你不能直接去new一个AlertDialog对象:
protected AlertDialog(Context context) {
this(context, 0);
}
它是通过一个内部类Builder在构造方法中new了一个对象P,它是AlertController类的一个静态内部类对象AlertParams:
public Builder(Context context, int themeResId) {
private final AlertController.AlertParams P;
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
}
然后我们再来看一下setTitle()、setIcon()这些方法又是做了什么?
public Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
public Builder setIcon(Drawable icon) {
P.mIcon = icon;
return this;
}
从这个代码中可以看出,这些set方法其实就是给P对象内部去放置一些参数,然后返回Builder自身,也就是这里的this。
然后接着看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);
...省略部分代码
}
这个方法中首先就是new了一个Dialog,然后通过P调用了一个apply方法,到这一步就是设置参数了,把Dialog中的参数都从P里面拿,现在跟到这个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));
}
}
...省略一大串代码
}
可以看到在这里就是开始组装P内部的一系列参数,有什么就拼装什么,里面有一系列的if判断。
最后是调用了Dialog的show()方法去展示,这个show注意是在Dialog中的,因为AlertDialog是继承自Dialog的,这个源码就不再贴了,里面的实现还是比较复杂的,涉及到了Window对象的一些概念,有兴趣的可以研究研究,因为不是本篇的重点,所以就不多说了。
2.2、Builder模式工作流程
添加参数(P)--->组装参数(添加多少就组装多少)--->显示
主要涉及的对象:
- AlertDialog:整体的弹出框对象
- AlertDialog.Builder:规范一系列的组装过程
- AlertController:具体的构建器
- AlertController.AlertParams:存放参数以及一部分设置参数的功能
三、代码实战——Builder模式构建通用型Dialog
3.1、基本框架搭建
从这里开始我们就来封装这个通用型Dialog了,首先仿照源码把基本框架搭建起来,我们先来创建一个类CommonDialog让它继承自Dialog,以及它的内部类Builder,这些代码都是仿照源码来写的,其中有些代码是直接从源码中拷贝过来然后修改的:
/**
* 作者: 乔布奇
* 日期: 2020-04-26 22:42
* 邮箱: jarchie520@gmail.com
* 描述: 自定义通用型Dialog
*/
public class CommonDialog extends Dialog {
private CommonController mController;
public CommonDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
mController = new CommonController(this,getWindow());
}
//创建内部类构建器
public static class Builder{
private final CommonController.CommonParams P;
public Builder(Context context){
this(context, R.style.dialog);
}
public Builder(Context context,int themeId){
P = new CommonController.CommonParams(context,themeId);
}
public CommonDialog create(){
// Context has already been wrapped with the appropriate theme.
final CommonDialog dialog = new CommonDialog(P.mContext,P.mThemeResId);
P.apply(dialog.mController);
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 CommonDialog show(){
final CommonDialog dialog = create();
dialog.show();
return dialog;
}
}
}
这里给它一个默认的style,也就是在Builder的构造方法中设置的这个R.style.dialog,主题的代码如下:
<style name="dialog" parent="@android:style/Theme.Dialog">
<!--边框-->
<item name="android:windowFrame">@null</item>
<!--是否浮现在Activity之上-->
<item name="android:windowIsFloating">true</item>
<!--背景透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--模糊-->
<item name="android:backgroundDimEnabled">true</item>
<!--无标题-->
<item name="android:windowNoTitle">true</item>
</style>
然后接着创建Builder中的Dialog的构建器类CommonController以及它的构建参数的内部类CommonParams:
/**
* 作者: 乔布奇
* 日期: 2020-04-26 22:43
* 邮箱: jarchie520@gmail.com
* 描述: 通用型Dialog构建器
*/
class CommonController {
private CommonDialog mDialog;
private Window mWindow;
public CommonController(CommonDialog dialog, Window window) {
this.mDialog = dialog;
this.mWindow = window;
}
//获取Dialog
public CommonDialog getDialog(){
return mDialog;
}
//获取Dialog的Window对象
public Window getWindow() {
return mWindow;
}
public static class CommonParams {
public Context mContext;
public int mThemeResId;
//点击空白是否能够取消
public boolean mCancelable = false;
//dialog Cancel监听
public DialogInterface.OnCancelListener mOnCancelListener;
//dialog Dismiss监听
public DialogInterface.OnDismissListener mOnDismissListener;
//dialog Key监听
public DialogInterface.OnKeyListener mOnKeyListener;
public CommonParams(Context context, int themeResId) {
this.mContext = context;
this.mThemeResId = themeResId;
}
/**
* 绑定和设置参数
* @param mController
*/
public void apply(CommonController mController) {
}
}
}
这里面的变量及事件监听都是仿照源码来的,拷贝修改就OK了,这样最基本的架子就先搭建起来了!
3.2、完善Builder
这一部分我们来给Builder类的内部添加一系列的setXXX()方法,比如系统Dialog中的设置标题图标这些东西,这些设置的方法都是一些套路代码,定义一个方法,返回值类型为Builder,方法内部进行设置操作,最后返回this即可。
首先我们需要在CommonController中的CommonParams类中添加几个需要用到的变量:
//布局View
public View mView;
//布局Layout ID
public int mViewLayoutResId;
//存放文本的修改,文本可能有多个,需要使用Map存储,这里选择SparseArray因为它更加高效
public SparseArray<CharSequence> mTextArray = new SparseArray<>();
//存放点击事件
public SparseArray<View.OnClickListener> mClickArray = new SparseArray<>();
这里需要注意的点我也在代码注释中写了,因为我们的Dialog可能是各式各样的,所以对于文本和点击事件的设置都是不可控的,无法确定数量上有多少,位置上在哪里点击,所以这里需要采用Map集合去存储这种多个的情况,又因为我们都是通过控件id去操作文本及点击事件的,它符合HashMap<Integer,Object>这种int--->Object的格式,所以这里选择使用SparseArray<T>去存储,它比Map在性能上更加高效。
然后就是Builder中的一系列设置操作了:
//设置布局View
public Builder setContentView(View view){
P.mView = view;
P.mViewLayoutResId = 0;
return this;
}
//设置布局内容LayoutId
public Builder setContentView(int layoutId){
P.mView = null;
P.mViewLayoutResId = layoutId;
return this;
}
//设置文本
public Builder setText(int viewId,CharSequence text){
P.mTextArray.put(viewId,text);
return this;
}
//设置点击事件
public Builder setOnClickListener(int viewId,View.OnClickListener listener){
P.mClickArray.put(viewId,listener);
return this;
}
//设置是否可以取消
public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}
//设置Cancel监听
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
P.mOnCancelListener = onCancelListener;
return this;
}
//设置Dismiss监听
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
//设置key监听
public Builder setOnKeyListener(OnKeyListener onKeyListener) {
P.mOnKeyListener = onKeyListener;
return this;
}
3.3、完善真正的构建器
在上面分析源码的时候我们说过,真正的设置参数的操作是P对象调用apply()方法实现的:P.apply(dialog.mAlert),那接下来就来写我们自己的这个apply()方法,在写之前我们先定义一个DialogViewHelper类用于View的辅助处理:
/**
* 作者: 乔布奇
* 日期: 2020-04-26 22:44
* 邮箱: jarchie520@gmail.com
* 描述: Dialog View的辅助处理类
*/
class DialogViewHelper {
private View mContentView = null;
//WeakReference防止内存泄漏
private SparseArray<WeakReference<View>> mViews;
public DialogViewHelper(Context context, int layoutResId) {
this();
mContentView = LayoutInflater.from(context).inflate(layoutResId, null);
}
public DialogViewHelper() {
mViews = new SparseArray<>();
}
//设置布局
public void setContentView(View contentView) {
this.mContentView = contentView;
}
//设置文本
public void setText(int viewId, CharSequence text) {
TextView textView = getView(viewId);
if (textView != null) {
textView.setText(text);
}
}
//设置点击事件
public void setOnclickListener(int viewId, View.OnClickListener listener) {
View view = getView(viewId);
if (view != null) {
view.setOnClickListener(listener);
}
}
//获取ContentView
public View getContentView() {
return mContentView;
}
//通用fv获取控件
private <T extends View> T getView(int viewId) {
WeakReference<View> viewReference = mViews.get(viewId);
View view = null;
if (viewReference != null) {
view = viewReference.get();
}
if (view == null) {
view = mContentView.findViewById(viewId);
if (view != null) {
mViews.put(viewId, new WeakReference<>(view));
}
}
return (T) view;
}
}
这里需要注意的点是我们定义了一个通用的防止重复绑定控件的方法,已经绑定过的不用再次通过findViewById获取了,直接从弱引用中拿就行,定义好了这个类,我们就可以先来填充apply方法了,先让我们的Dialog显示出来:
/**
* 绑定和设置参数
*
* @param mController
*/
public void apply(CommonController mController) {
DialogViewHelper viewHelper = null;
//设置Dialog的布局
if (mViewLayoutResId != 0) {
viewHelper = new DialogViewHelper(mContext, mViewLayoutResId);
}
if (mView != null) {
viewHelper = new DialogViewHelper();
viewHelper.setContentView(mView);
}
if (viewHelper == null){
throw new IllegalArgumentException("请设置布局setContentView()");
}
//给Dialog设置布局
mController.getDialog().setContentView(viewHelper.getContentView());
//设置文本
int textArraySize = mTextArray.size();
for (int i=0;i<textArraySize;i++){
viewHelper.setText(mTextArray.keyAt(i),mTextArray.valueAt(i));
}
//设置点击事件
int clickArraySize = mClickArray.size();
for (int i=0;i<textArraySize;i++){
viewHelper.setOnclickListener(mClickArray.keyAt(i),mClickArray.valueAt(i));
}
}
OK,到这里其实我们的Dialog就已经能够显示出来了,只不过还有很多细节需要处理,来继续往下看吧!
3.4、自定义参数配置
首先我们在CommonController的内部类CommonParams中添加我们需要的自定义参数:
//宽度
public int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
//动画
public int mAnimations = 0;
//位置
public int mGravity = Gravity.CENTER;
//高度
public int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
然后在Builder中对添加的这些变量同样的去赋值也即是set操作,这里的dialog_scale_anim就是一个简单的缩放动画:
//配置一些通用参数
public Builder fullWidth(){
P.mWidth = ViewGroup.LayoutParams.MATCH_PARENT;
return this;
}
//从底部弹出,是否有动画
public Builder fromBottom(boolean isAnimation){
if (isAnimation){
P.mAnimations = R.style.dialog_from_bottom_anim;
}
P.mGravity = Gravity.BOTTOM;
return this;
}
//设置宽高
public Builder setWidthAndHeight(int width,int height){
P.mWidth = width;
P.mHeight = height;
return this;
}
//添加默认动画
public Builder addDefaultAnimation(){
P.mAnimations = R.style.dialog_scale_anim;
return this;
}
//自行设置动画
public Builder setAnimations(int styleAnimation){
P.mAnimations = styleAnimation;
return this;
}
最后在apply()方法中进行配置,这里就要用到我们的Window对象了:
//配置自定义效果:全屏,从底部弹出,动画等
Window window = mController.getWindow();
//设置位置
window.setGravity(mGravity);
//设置动画
if (mAnimations != 0) {
window.setWindowAnimations(mAnimations);
}
//设置宽高
WindowManager.LayoutParams params = window.getAttributes();
params.width = mWidth;
params.height = mHeight;
window.setAttributes(params);
OK,到这里我们的dialog基本上就已经搞定了,后续你如果需要进行相关的拓展,直接添加你需要的属性就OK了。好,写了这么多,你会觉得这不是更加复杂了吗?是吗?造轮子的过程是比较复杂,但是用轮子的时候可不复杂哦,甚至你还会偷着乐,下面就让我们一起来看一下调用的时候是不是变得相当简单了呢?
四、使用Dialog
我们先来看一个最简单的情况,比如我们有些场景下会弹出一些⚠️警告提示之类的Dialog,这种直接给用户看的,无需操作:
先来自定义一个Dialog的布局dialog_test_1.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@color/color_white"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher_round"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<TextView
android:id="@+id/mContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:lineSpacingExtra="3dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_marginBottom="20dp"
android:text="我是弹出内容"
android:textColor="@color/colorPrimary"
android:textSize="16sp" />
</LinearLayout>
一个图片一个文本,这是很简单的一个场景了,这种调用就很爽了,一直往下点就行了:
new CommonDialog.Builder(MainActivity.this)
.setContentView(R.layout.dialog_test_1)
.setWidthAndHeight(DensityUtil.dp2px(300), LinearLayout.LayoutParams.WRAP_CONTENT)
.addDefaultAnimation()
.create()
.show();
就是这么直接,就是这么简单,一行(如果你的屏幕足够宽)下来搞定!
再来看一个有用户交互的场景,带有确认取消按钮的,同样的我们创建一个dialog_test_0.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@color/color_white"
android:orientation="vertical">
<TextView
android:id="@+id/mTitle"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:text="我是标题"
android:textColor="@color/colorAccent"
android:textSize="18sp" />
<TextView
android:id="@+id/mContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:lineSpacingExtra="3dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="Android自定义View,Android JetPack,常用第三方库源码,Android Framework源码,C/C++/JNI/NDK,MVC/MVP/MVVM/模块化/组件化/插件化,热更新热修复,设计模式,线程间通信,进程间通信"
android:textColor="@color/colorPrimary"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:background="@color/colorE9" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/colorE9"
android:orientation="horizontal">
<TextView
android:id="@+id/mCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginRight="1dp"
android:layout_weight="1"
android:background="@color/color_white"
android:gravity="center"
android:text="取消"
android:textColor="@color/color_666"
android:textSize="18sp" />
<TextView
android:id="@+id/mConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="1dp"
android:layout_weight="1"
android:background="@color/color_white"
android:gravity="center"
android:text="确认"
android:textColor="@color/colorAccent"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
然后在Java代码中调用,这里考虑到某些场景下需要拿到相关的数据,所以将点击事件在Dialog中也提供了一份,这样方便我们进行一些数据处理的操作,所以最后的调用成了这个样子:
CommonDialog dialog = new CommonDialog.Builder(MainActivity.this)
.setContentView(R.layout.dialog_test_0)
.setCancelable(true)
.fromBottom(true)
.fullWidth()
.setText(R.id.mTitle,"Android高级进阶")
.create();
dialog.setOnclickListener(R.id.mConfirm, v -> {
Toast.makeText(MainActivity.this,"点击确定了",Toast.LENGTH_SHORT).show();
dialog.dismiss();
});
dialog.setOnclickListener(R.id.mCancel, v -> {
Toast.makeText(MainActivity.this,"点击取消了",Toast.LENGTH_SHORT).show();
dialog.dismiss();
});
dialog.show();
其实跟上面的也是差不多的,没有太大的区别,代码稍作改动即可,最后我会把完整的代码贴出来供大家参考!
写到这里基本上就要和大家说再见了,如有问题,欢迎留言或者私信我进行探讨!因为我本人比较懒,所以没有新建项目,我直接在把代码封装了一个lib_common放到了我之前写的《Android架构设计之MVC/MVP/MVVM浅析》的源码中了,有需要的可以下载或者clone!