使用DialogFragment代替Dialog

3,164 阅读3分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

使用DialogFragment代替Dialog

是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。

DialogFragment是什么

DialogFragment从它的源码得知,它继承了Fragment,其实是一个比较特殊的Fragment。那么它相对于普通的Dialog有什么不同,谷歌又为什么推荐我们使用它呢,它相对于普通的Dialog有什么优点呢。

使用过它之后用自己的感受描述:

  • 它的生命周期很清晰,方便写复杂的逻辑
  • 它于Activity的生命周期是绑定的,Activity消失,DialogFragment也会消失。
  • 它可以很简单的控制弹窗的布局。

总结就是dialogfragment能更好的管理dialog的展示与消失,以及在屏幕旋转时的一些状态保存问题。

DialogFragment的踩坑

即使它有很多的优点,但使用不当时,仍然会有很多坑。
我遇到了很多奇奇怪怪的问题。 比如

  • Fragment already added 异常
  • 快速的显示消失,无法消失的异常

当然除此之外我们还可能有以下需求:

  • 设置对话框的大小
  • 设置弹出对话框时背景灰色或者透明

下面我们就来一一实现。

如何实现DialogFragment

重点来关注两个方法。

  • onCreateDialog 新建一个Dialog即可使用
  • onCreateView 自定义一个Dialog界面

onCreateDialog

Screenshot_2021-11-04-22-04-55-27_2a27335eaa331505125090a61677c0b2.jpg

做一个简单的对话框

public class ConfirmDialog extends DialogFragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return super.onCreateView(inflater, container, savedInstanceState);
    }
    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog dialog =  new AlertDialog.Builder(getActivity())
                .setTitle("提示")
                .setMessage("确认要退出吗")
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                }).create();
        return dialog;
    }
}

显示它

ConfirmDialog dialog = new ConfirmDialog();
dialog.show(getSupportFragmentManager(), "dialogTag");

onCreateView

使用自定义视图做一个加载框。
这里有一个非常重要的地方,我出现了 Fragment already added 的问题,意思就是重复添加了,那么为什么会出现重复添加呢,因为我最初的代码是使用isAddedisVisibility进行判断,但是当快速执行的时候,这两个方法并不准确。

正确的做法有两种。

  • 添加事务时先进行移除
beginTransaction().remove(this).commit()
  • 使用变量进行判断,不能使用isAdded
private boolean isShowFragment = false;
 
    @Override
    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
        // 解决bug:Android java.lang.IllegalStateException: Fragment already added
        if (this.isShowFragment) {
            return;
        }
        this.isShowFragment = true;
        super.show(manager, tag);
    }
 
    @Override
    public void dismiss() {
        super.dismiss();
        this.isShowFragment = false;
    }
 
    // 避免有些手机兼容性问题,isShowFragment未变成false而导致无法二次打开
    @Override
    public void onDestroy() {
        super.onDestroy();
        this.isShowFragment = false;
    }

最后直接放代码,封装好的Loading框

LoadingDialog 对话框 可以看到代码中对bug的处理:在每个add事务前增加一个remove事务,防止连续的add。

public class LoadingDialog extends DialogFragment
        implements DialogInterface.OnKeyListener {
    /**
     * 加载框提示信息 设置默认
     */
    private final String hintMsg = "加载中...";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NO_TITLE, R.style.MyDialog);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Dialog dialog = getDialog();
        // 设置背景透明
        if (dialog.getWindow() != null)
            dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        // 去掉标题
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.setCanceledOnTouchOutside(false);
        View loadingView = inflater.inflate(R.layout.dialog_loading, container);
        TextView hintTextView = loadingView.findViewById(R.id.tv_ios_loading_dialog_hint);
        hintTextView.setText(hintMsg);
        //不响应返回键
        dialog.setOnKeyListener(this);
        return loadingView;
    }

    @Override
    public void show(FragmentManager manager, String tag) {
        try {
            //在每个add事务前增加一个remove事务,防止连续的add
            manager.beginTransaction().remove(this).commit();
            super.show(manager, tag);
        } catch (Exception e) {
            //同一实例使用不同的tag会异常,这里捕获一下
            e.printStackTrace();
        }
    }

    @Override
    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
//        return keyCode == KeyEvent.KEYCODE_BACK;
        // 允许按back键取消Loading
        return false;
    }

}

代理管理类

public class GlobalDialogManager {

    private LoadingDialog mLoadingDialog;

    private GlobalDialogManager() {
        init();
    }

    public static GlobalDialogManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final GlobalDialogManager INSTANCE = new GlobalDialogManager();
    }

    public void init() {
        if (mLoadingDialog == null) {
            mLoadingDialog = new LoadingDialog();
        }
    }

    /**
     * 展示加载框
     */
    public synchronized void show(FragmentManager manager) {
        if (manager != null && mLoadingDialog != null) {
            mLoadingDialog.show(manager, "tag");
        }
    }

    /**
     * 隐藏加载框
     */
    public synchronized void dismiss(FragmentManager manager) {
        if (mLoadingDialog != null && !manager.isDestroyed()) {
            mLoadingDialog.dismissAllowingStateLoss();
        }
    }
}

使用它

if (getContext() != null)
GlobalDialogManager.getInstance().show(((Activity) getContext()).getFragmentManager());

if (getContext() != null) 
GlobalDialogManager.getInstance().dismiss(((Activity) getContext()).getFragmentManager());

这里判断getContext()很有必要,避免Activity消失了,getContext为空的bug。

背景不变暗设置一个style属性行啦。

<item name="android:backgroundDimEnabled">false</item><!--activity不变暗-->