Android基于DialogFragment封装一个通用的Dialog

一、背景

Dialog是项目中最常用的一个功能之一了,接手项目的时候发现项目中是封装了一个dialog的,但是发现是用单例封装的,大致如下:

    private MyDialog() {
     }

    public static MyDialog getInstance() {
        return DialogHolder.instance;
    }

    private static class DialogHolder {
        private static MyDialog instance = new MyDialog();
    }
   
   public void show(){}
   public void dismiss(){}

使用单例除了可能会有内存泄漏问题,在使用过程中还发现一个问题:不同页面的dialog可以相互影响,想想也对,因为全局只有一个dialog嘛,项目中有一个场景:A页面跳B页面,一进B页面的onCreate()时需要弹一个dialog,发现每次都弹不出来,debug发现原来在A页面的onStop()方法里调用了dismiss()方法,A页面跳B页面生命周期走的是:

 A页面:   onPause()
 B页面:    onCreate()
 B页面:    onStart()
 B页面:    onResume()
 A页面:    onStop()

所以原因也找到了,每次在B的onCreate()里面刚调用了show(),紧接着又调用了A的onStop()中的dismiss()给关掉了,用单例方式显然不太合适。趁着版本大改版,花了点时间重新撸了一个。根据我们的项目需要,调研了下,大概需要符合以下场景:
1、不用提供布局,内置项目中常用默认的样式
2、 支持自定义复杂的布局、动画、对话框大小、背景色等设置
3、统一管理多个dialog并顺序弹出

第一点:大部分情况下,使用对话框的样式都是一致的,所以内置了默认的dialog样式,可以避免调用方每次再去找布局文件,尽可能的简化调用。ps:内置dialog样式可以根据需求自行修改。
第二点:如果需要自定义复杂的布局,需要支持布局子View的创建及一系列交互事件。
第三点: 项目中有个需求,可能一次会产生多个dialog,需要依次弹出dialog

基于以上需求点,使用DialogFragment封装了一个通用Dialog——SYDialog,先看最终效果图

二、效果图

dialog.gif

gif图比较模糊,直接扫二维码下载APK吧!

Android基于DialogFragment封装一个通用的Dialog_第1张图片
image

三、为什么选择DialogFragment?

DialogFragment继承自Fragment,即可以用Fragment来展示Dialog,相比于用AlertDialog或者DialogDialogFragment 更有优势:

  • 当手机配置变化导致Activity重建时(比如旋转屏幕)或点击物理返回键时,DialogFragment可以管理好自己的生命周期
  • DialogFragment继承自Fragment,所以DialogFragment也可以当做一个内嵌的组件来使用,所以DialogFragment有更好的复用性

四、UML图

用一个UML图大致来表示一下类之间的关系:


Android基于DialogFragment封装一个通用的Dialog_第2张图片
SYDialog.png

五、使用文档

写在前面,目前此dialog库已发布到jcenter,可以直接通过下面的方式引用:

compile 'com.ninetripods:lib-dialog:1.1.0'

1、使用内置dialog:

  • 内置一个Button的样式:
  new SYDialog.Builder(this)
           .setTitle("我是标题")
          .setContent("您好,我们将在30分钟处理,稍后通知您订单结果!")
          .setPositiveButton(new IDialog.OnClickListener() {
               @Override
               public void onClick(IDialog dialog) {
                   dialog.dismiss();
               }
           })
           .show();

效果图:


Android基于DialogFragment封装一个通用的Dialog_第3张图片
IMG_20180919_192552.jpg
  • 内置二个Button的样式:
  new SYDialog.Builder(this)
           .setTitle("我是标题")
          .setContent("您好,我们将在30分钟处理,稍后通知您订单结果!")
          .setPositiveButton(new IDialog.OnClickListener() {
               @Override
               public void onClick(IDialog dialog) {
                   dialog.dismiss();
               }
           })
          .setNegativeButton(new IDialog.OnClickListener() {
               @Override
               public void onClick(IDialog dialog) {
                   dialog.dismiss();
               }
            })
           .show();

效果图:


Android基于DialogFragment封装一个通用的Dialog_第4张图片
IMG_20180919_192531.jpg

2、使用自定义布局的样式:

  new SYDialog.Builder(this)
           .setDialogView(R.layout.layout_dialog)//设置dialog布局
           .setAnimStyle(R.style.translate_style)//设置动画 默认没有动画
           .setScreenWidthP(0.85f) //设置屏幕宽度比例 0.0f-1.0f
           .setGravity(Gravity.CENTER)//设置Gravity
           .setWindowBackgroundP(0.2f)//设置背景透明度 0.0f-1.0f 1.0f完全不透明
           .setCancelable(true)//设置是否屏蔽物理返回键 true不屏蔽  false屏蔽
           .setCancelableOutSide(true)//设置dialog外点击是否可以让dialog消失
           .setBuildChildListener(new IDialog.OnBuildListener() {
                    //设置子View
                    @Override
                    public void onBuildChildView(final IDialog dialog, View view, int layoutRes) {
                        //dialog: IDialog
                        //view: DialogView
                        //layoutRes :Dialog的资源文件 如果一个Activity里有多个dialog 可以通过layoutRes来区分
                        final EditText editText = view.findViewById(R.id.et_content);
                        Button btn_ok = view.findViewById(R.id.btn_ok);
                        btn_ok.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                String editTextStr = null;
                                if (!TextUtils.isEmpty(editText.getText())) {
                                    editTextStr = editText.getText().toString();
                                }
                                dialog.dismiss();
                                Toast.makeText(MyApplication.getApplication(), editTextStr, Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
              }).show();

代码中注释已经很详细了,如果是自定义布局并且需要处理交互事件,可以通过设置setBuildChildListener并实现其回调,并在回调接口中创建子View并处理交互事件,使用起来还是很方便的。

3、统一管理多个Dialog依次弹出

SYDialog.Builder builder1 = new SYDialog.Builder(this);
SYDialog.Builder builder2 = new SYDialog.Builder(this)
//添加第一个Dialog
SYDialogsManager.getInstance().requestShow(new DialogWrapper(builder1));
//添加第二个Dialog
SYDialogsManager.getInstance().requestShow(new DialogWrapper(builder2));

DialogWrapper 来包装一层Dialog,方便后续添加数据信息。SYDialogsManager通过单例来实现,确保只有一个实例,内部有一个容器队列ConcurrentLinkedQueue来保存多个DialogrequestShow()方法中首先会判断当前是否有正在显示的弹窗,如果有,则在队列中等待,否则从队列中取出并展示,并在队列中清空该数据,当一个Dialog展示完毕,会继续尝试在队列中取出Dialog并展示,直到队列是空为止。

六、源码地址

上述例子源码:https://github.com/crazyqiang/AndroidStudy

引用:
【1】https://developer.android.com/reference/android/app/DialogFragment
【2】https://blog.csdn.net/lmj623565791/article/details/37815413

七、更新

1、2018/10/27 update
在使用过程中发现一个问题,通过setScreenHeightP(1.0f)设置屏幕高度的时候有问题,最下面的布局会被挤掉,先看之前计算屏幕高度的代码:

  private static Point point = new Point();
  public static int getScreenHeight(Activity activity) {
        Display display = activity.getWindowManager().getDefaultDisplay();
        if (display != null) {
            display.getSize(point);
            return point.y ;
        }
        return 0;
    }

可以看到我们计算屏幕的高度包含了 statusBar的高度,但是我们dialog的布局都是在statusBar下面的,所以当然还要减去statusBar的高度,代码如下:

   private static Point point = new Point();
   public static int getScreenHeight(Activity activity) {
        Display display = activity.getWindowManager().getDefaultDisplay();
        if (display != null) {
            display.getSize(point);
            return point.y - getStatusBarHeight(activity);
        }
        return 0;
    }
    /**
     * 计算statusBar高度
     *
     * @param activity Activity
     * @return statusBar高度
     */
    public static int getStatusBarHeight(Activity activity) {
        Resources resources = activity.getResources();
        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
        return resources.getDimensionPixelSize(resourceId);
    }

通过point.y - getStatusBarHeight(activity)得到dialog全屏时的高度,那么还有一个问题,既然要减去statusBar的高度,那么是否还需要减去navigationBar的高度呢,因为如果navigationBar可见时,dialog同样不会绘制到navigationBar上去,结论是:不用减!为什么不用减呢,看一下我们计算屏幕高度用的是display.getSize(point),并且是通过windowManager获取的display,那么计算高度时已经减去了navigationBar的高度;如果使用display.getRealSize(point),那得到的是包含虚拟键盘navigationBar高度的,那此时则需要减去这个navigationBar的高度。

2、2018/10/27 添加dialog消失时的回调,代码如下:

setOnDismissListener(new IDialog.OnDismissListener() {
                    @Override
                    public void onDismiss(IDialog dialog) {
                        //监听dialog dismiss的回调
                        toast("dismiss回调");
                    }
                })

dialog消失时会收到此回调。

3、2019/06/20

发布lib-dialog库,引入方式:compile 'com.ninetripods:lib-dialog:1.1.0'

优化内容:
1、优化dialog默认布局
2、修复Activity横竖屏切换导致空指针问题
3、优化DialogFragment在Activity onSaveInstanceState()之后调用导致的问题
  • Activity横竖屏切换导致空指针问题:表现为当Activity不设置android:screenOrientation导致手机横竖屏切换时Activity重建,DialogFragment本质上也是个Fragment,如果Activity 里没有判断Bundle,可能会导致Fragment也重建,引起getFragmentManager()报空指针问题

  • DialogFragment在Activity onSaveInstanceState()之后调用导致的问题:当Activity置于后台或按home键,Activity的生命周期会走下面这几个:

onPause()
onSaveInstanceState()
onStop()

如果在ActivityonSaveInstanceState()调用之后继续通过FragmentManager去添加DialogFragmentActivity,如果使用commit()去提交,系统会直接抛异常,这里改用另一种方式去提交:

 /**
     * 解决 Can not perform this action after onSaveInstanceState问题
     *
     * @param manager FragmentManager
     * @param tag     tag
     */
    public void showAllowingLoss(FragmentManager manager, String tag) {
        try {
            Class cls = DialogFragment.class;
            Field mDismissed = cls.getDeclaredField("mDismissed");
            mDismissed.setAccessible(true);
            mDismissed.set(this, false);
            Field mShownByMe = cls.getDeclaredField("mShownByMe");
            mShownByMe.setAccessible(true);
            mShownByMe.set(this, true);
        } catch (Exception e) {
            //调系统的show()方法
            show(manager, tag);
            return;
        }
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commitAllowingStateLoss();
    }

    @Override
    public void dismiss() {
        //防止横竖屏切换时 getFragmentManager置空引起的问题:
        //Attempt to invoke virtual method 'android.app.FragmentTransaction
        //android.app.FragmentManager.beginTransaction()' on a null object reference
        if (getFragmentManager() == null) return;
        super.dismissAllowingStateLoss();
    }

FragmentTransaction不去调用commit(),而是通过反射去调用commitAllowingStateLoss(),这样允许在onSaveInstanceState()之后继续操作DialogFragment

你可能感兴趣的:(Android基于DialogFragment封装一个通用的Dialog)