如何从0构造出一个万能的Dialog

前言

  • 第一次在简书发表文章,突然就有点小紧张,写不好,会不会有人打我啊。
  • 该片文章所讲的Dialog适用于那种漂亮可爱妹纸设计的多种弹框,各种弹,各种动画,各种方位、姿势和变化。
  • 写该文章的初衷在于刚好我们公司就有这么一对可爱的设计师,这样多样式的弹框,如果一个个单独写会造成代码臃肿和可读性十分差,所以做了一下二次封装。
  • 本文章其实早在两个月前就打算写了,由于项目时间十分紧张(通宵加班加得我怀疑猿生),所以拖到了现在,终于开始写了(我在调休,嘿嘿嘿...)。

本文会用到Builder设计模式:Android开发---Builder 模式必知必会

  • 给大家献上效果图,弹框自行yy出来的,可能有点丑,不过这不是重点,因为出图是设计妹纸的事情。
效果一.png
效果二.png
效果三.png
效果四.png
效果五.png
效果六.png

以上是部分效果展示,下面我们将进入正题:

目录

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也可以配置啊。如果真的有这样的人,我劝你还是多看看源码,再出来装逼,其实它也还是走了这一步的,如下图。

show之前也执行了create.png

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,为程序猿而生


请点赞,因为您的鼓励将是我写作的最大动力!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容