如何从0构造出一个万能的Dialog
前言
- 第一次在简书发表文章,突然就有点小紧张,写不好,会不会有人打我啊。
- 该片文章所讲的Dialog适用于那种漂亮可爱妹纸设计的多种弹框,各种弹,各种动画,各种方位、姿势和变化。
- 写该文章的初衷在于刚好我们公司就有这么一对可爱的设计师,这样多样式的弹框,如果一个个单独写会造成代码臃肿和可读性十分差,所以做了一下二次封装。
- 本文章其实早在两个月前就打算写了,由于项目时间十分紧张(通宵加班加得我怀疑猿生),所以拖到了现在,终于开始写了(我在调休,嘿嘿嘿...)。
本文会用到Builder设计模式:Android开发---Builder 模式必知必会
- 给大家献上效果图,弹框自行yy出来的,可能有点丑,不过这不是重点,因为出图是设计妹纸的事情。
以上是部分效果展示,下面我们将进入正题:
目录
1.继承Dialog,高仿一个AlertDialog
1.1 开始按照v7包的AlertDialog撸一个轮子
- 阅读并理解AlertDialog源码
上一个链接让大家看看如何简单自定义:Android AlertDialog/AlertDialog.builder 以及 自定义AlertDialog方法
- 在package android.support.v7.app下面我们找到这个类AlertDialog.class 我精简了一些 剩下的主要内容如下:
private final AlertController mAlert;
static final int LAYOUT_HINT_NONE = 0;
static final int LAYOUT_HINT_SIDE = 1;
protected AlertDialog(@NonNull Context context) {
this(context, 0);
}
protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, resolveDialogTheme(context, themeResId));
mAlert = new AlertController(getContext(), this, getWindow());
}
protected AlertDialog(@NonNull Context context, boolean cancelable,
@Nullable OnCancelListener cancelListener) {
this(context, 0);
setCancelable(cancelable);
setOnCancelListener(cancelListener);
}
private static int resolveDialogTheme(@NonNull Context context, @StyleRes int resid) {
if (resid >= 0x01000000) { // start of real resource IDs.
return resid;
} else {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true);
return outValue.resourceId;
}
}
public void setView(View view) {
mAlert.setView(view);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
}
public static class Builder {
private final AlertController.AlertParams P;
private final int mTheme;
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;
}
@NonNull
public Context getContext() {
return P.mContext;
}
public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
P.mOnCancelListener = onCancelListener;
return this;
}
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
public Builder setOnKeyListener(OnKeyListener onKeyListener) {
P.mOnKeyListener = onKeyListener;
return this;
}
public Builder setView(int layoutResId) {
P.mView = null;
P.mViewLayoutResId = layoutResId;
P.mViewSpacingSpecified = false;
return this;
}
public Builder setView(View view) {
P.mView = view;
P.mViewLayoutResId = 0;
P.mViewSpacingSpecified = false;
return this;
}
@Deprecated
public Builder setView(View view, int viewSpacingLeft, int viewSpacingTop,
int viewSpacingRight, int viewSpacingBottom) {
P.mView = view;
P.mViewLayoutResId = 0;
P.mViewSpacingSpecified = true;
P.mViewSpacingLeft = viewSpacingLeft;
P.mViewSpacingTop = viewSpacingTop;
P.mViewSpacingRight = viewSpacingRight;
P.mViewSpacingBottom = viewSpacingBottom;
return this;
}
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
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;
}
}
从源码可以看出,类中有个AlertController类型的属性,我们进入这个类文件可以看到定义了很多属性,比如Window View 上下左右边距等等,这个类的作用我们将会在2.1中提到。
- 开始自定义PowerfulDialog
我们新建一个PowerfulDialog仿照这个类,去继承Dialog,这里有人拿着刀就要说了,人家是继承的AppCompatDialog(这里我解释一下AppCompatDialog ,点进去看源码,发现它还是继承的Dialog,是为了简便的实现Material Design风格的弹框),所以很多人使用AlertDialog,基本都会是长这样:
md风格弹框.png当然了,原生的AlertDialog可以通过Biulder中的setView方法去设置自定义的View但是,这样写达不到我们理想中的代码量尽可能少的要求,好了,现在我们直接继承Dialog,不熟悉Builder的建议可以直接copy一份源码,直接在源码上面修改。
我们会在PowerFulDialog中写一个Builder的静态内部类,这个静态类主要是用来通知控制器来设置这个弹框的事件、动画、属性及view的操作,所以我们一开始要扩展Dialog的功能,就可以在这里面配置编写相应的方法,在Builder内部的方法一般是可以链式操作的,达到灵活配置的作用。
在Builder静态内部类外部的方法除了构造函数以外,其他的方法应该是可以直接通过自定义的Dialog获取到的属性或事件等,这些操作大多是单个操作,主要是获取某些属性或者事件,比如获取Dialog中View的某个控件等。
好了 ,PowerFulDialog这个类的组成大概就是这个样子。下面我们来说说直接和弹框打交道的控制类PowerfulController.class
2.编写一个控制类(统一配置事件、参数和弹窗属性)
2.1 为什么要编写一个控制类
-
增强代码可读性,逻辑清晰,该类主要是设置弹框属性和通过辅助类设置弹框内容。而PowerFulDialog类主要是负责去操作Dialog的状态(create show dismiss...)
-
方便后期需求扩展,增加属性时候从一定程度上减少代码间的耦合度
2.2 如何编写这个控制类
-
首先我们可以参考一下源码,在这个类里面我们会写一些必要的参数,被传入的当前需要显示的Dialog、当前Dialog需展示的Window、还有一个就是我们为了专门处理View而新建的一个ViewHelper辅助类,并且会写一些设置view相关的方法给AlertParams内部类调用。
-
其次,看AlertController源码我们也可以发现,我们会在PowerfulController这个类里面建立一个静态内部类AlertParams,作用主要是用来存放需要配置的参数属性(显示的view、上下文、文字、颜色、大小、位置等),一个Dialog能够显示的基本属性条件基本上都必须写在这个内部类里面。
-
在内部类方法apply中,我们会传当前Dialog的Controller 对象,然后去设置这个传进来的Controller对象,并获取到当前处理view的辅助类,并完成view的处理。
-
那么,什么时候应该调用这个apply方法呢,上面说过了,apply方法是去给这个Dialog的Controller(控制器)下达配置Dialog(包括设置view、大小、字体、位置等)的指令,所以,这个方法应该是我们在Dialog执行create方法的时候调用。链式配置完成后调用create方法,我们就可以得到我们配置后建造出来的Dialog了。
-
有的人就有疑问了,链式表达后有的直接用show也可以配置啊。如果真的有这样的人,我劝你还是多看看源码,再出来装逼,其实它也还是走了这一步的,如下图。
3.编写一个View辅助类(用于实际直接操作view)
2.1 为什么要编写一个辅助类
-
增强代码可读性,逻辑清晰,该类主要是获取到指定view并直接设置View的属性,而PowerfulController主要是设置整个window的状态、动画、位置等,还有操控辅助类设置view
-
方便后期需求扩展,增加属性时候从一定程度上减少代码间的耦合度
2.2 如何编写一个辅助类
- 既然该类的主要作用是用于设置view的属性,那么必不可少的参数有Context、View(或者布局ID),所以构造函数需要传递这两个参数,这个类内部应该提供设置view的方法。DialogViewHelper.class代码如下:
private View mDialogView=null;
//防止泄露
private SparseArray<WeakReference<View>> mViews;
public DialogViewHelper(Context mContext, int mViewLayoutResId) {
this();
mDialogView= LayoutInflater.from(mContext).inflate(mViewLayoutResId,null);
}
public DialogViewHelper() {
mViews=new SparseArray<>();
}
/**
* 设置布局
* @param mView
* */
public void setDialogView(View mView) {
this.mDialogView=mView;
}
/**
* 设置文本
* @param viewId
* @param text
*/
public void setText(int viewId, CharSequence text) {
TextView tv=getView(viewId);
if(tv!=null){
tv.setText(text==null?"":text);
}
}
/**
* 设置文本颜色
* @param viewId
* @param colorRes
*/
public void setTextColor(Context context, int viewId, int colorRes) {
TextView tv=getView(viewId);
if(tv!=null){
tv.setTextColor(context.getResources().getColor(colorRes==0? R.color.text_gray_normal:colorRes));
}
}
/**
* 设置图片
* @param viewId
* @param imgRes
*/
public void setImage(int viewId, int imgRes) {
ImageView iv=getView(viewId);
if(iv!=null){
iv.setImageResource(imgRes==0? R.mipmap.default_head:imgRes);
}
}
/**
* 设置view的显示和隐藏
* @param viewId
* @param visibilityMode
*/
public void setVisiable(int viewId, int visibilityMode) {
View v=getView(viewId);
if(v!=null){
v.setVisibility(visibilityMode);
}
}
/**
* 优化findViewById
* @param viewId
* @param <T>
* @return
*/
public <T extends View> T getView(int viewId) {
View v=null;
WeakReference<View> viewRefrence=mViews.get(viewId);
if(viewRefrence!=null){
v=viewRefrence.get();
}
if(v==null){
v=mDialogView.findViewById(viewId);
if(v!=null){
mViews.put(viewId,new WeakReference<View>(v));
}
}
return (T)v;
}
/**
* 设置点击事件
* @param viewId
* @param onClickListener
*/
public void setOnclickListener(int viewId, View.OnClickListener onClickListener) {
View v=getView(viewId);
if(v!=null){
v.setOnClickListener(onClickListener);
}
}
public View getDialogView() {
return mDialogView;
}
总结
1.做一个“懒”的程序员
- 这里的懒,不是指整天喜欢装逼,不干事儿,不敲代码,不在实践中求真知的那个懒,指的是,会用简短高效的代码,去完成项目中的需求。
- “用最少的代码实现最牛逼的效果”相信这是每个“懒”程序员的终极目标,所以,我们平时在项目实际开发中,多去从中思考,有没有更加优秀的实现方式,多尝试,多积累。
2.时刻想着这段代码是否还可以被优化
- 在这个demo编写过程中,大家可以看到,有很多地方用到了SparseArray和弱引用。
- 其中我们主要是用SparseArray这个集合去存储已经从Dialog中取到的view id和对应的值,刚好利用view id为int类型,使用SparseArray更加节约内存,从而提高性能,当某个拥有id的view通过findviewbyid被查询到后都会放入集合,下次使用时不会再经过findviewbyid而是直接从SparseArray去取出,如若没有查询到该id则又通过findviewbyid查询,这样就提高了性能,减少了内存开支。
- 使用弱引用主要是为了系统内存不足时候造成内存泄漏的问题。
老司机带你理解SparseArray:Android内存优化(使用SparseArray和ArrayMap代替HashMap)
老司机带你理解为什么要使用弱引用:Java 理论与实践
用弱引用堵住内存泄漏)
3.要善于去学习源码
- 讲真,本文demo其实就是一个源码扩展和优化版本,其实其中的思维有百分之七八十的相似,无论是设计模式还是实现思路。所以,多看源码,你会学到很多东西的。
在线查看Android源码地址:在线查看android源码
4.最后奉上我的demo
- 最近上传到的GitHub,未来会长期更新和完善,如果有什么好的建议或者问题,欢迎提issue。
- 在简书的第一篇文章,如果写得有不好的地方或有误的地方,还希望指正,在下感激不尽。
- 最后,欢迎大家star and fork。
一个万能的Dialog demo地址:PowerfulDialog,为程序猿而生