一步一步使用 DialogFragment 封装链式调用 Dialog

TingTalk汀说 2018-01-30

前言

日常开发中,Dialog 是一个每个 app 所必备的。

通常来说,每个 app 的Dialog 的样式一般都是统一风格的,比如说有:

  • 确认、取消的 Dialog
  • 提示性的 Dialog
  • 列表选择的 Dialog
  • 版本更新的 Dialog
  • 带输入框的 Dialog

如果每个都要单独写,就显得有点浪费了,一般情况下,我们都需要进行封装,便于使用和阅读。

那为什么要使用 DialogFragment 呢?

使用 DialogFragment 来管理对话框,当旋转屏幕和按下后退键时可以更好的管理其生命周期,它和 Fragment 有着基本一致的生命周期。

并且 DialogFragment 也允许开发者把 Dialog 作为内嵌的组件进行重用,类似 Fragment (可以在大屏幕和小屏幕显示出不同的效果)

那么接下来我们就一步一步的来封装出一个便于我们使用的 DialogFragment。

还是先看下效果图吧,可能有点不是很好看,毕竟没有 ui,哈哈

一步一步使用 DialogFragment 封装链式调用 Dialog

源码地址 可以先去下载代码对照着看。

一、构建 BaseDialogFragment

1.1 明确我们需要的属性

在构建 BaseDialogFragment 之前,我们先分析下正常情况下,我们使用 Dialog 都需要哪些属性:

  • Dialog 的宽和高
  • Dialog 的对其方式
  • Dialog 在 x 和 y 坐标系的偏移量
  • Dialog 的显示隐藏的动画
  • Dialog 给调用者的回调
  • Dialog 消失时候的回调
  • Dialog 是否可以点击外部消失

当然,有的需求要不了这么多的属性,也有的人需要更多的属性,那就需要自己去探索了,我就讲下基于上面这些属性的封装,然后你可以基于我的 BaseDialogFragment 进行扩展。

有了上面的属性,我们就明白了在 BaseDialogFragment 中我们需要的字段:新建 BaseDialogFragment

public abstract class BaseDialogFragment extends DialogFragment {

    private int mWidth = WRAP_CONTENT;
    private int mHeight = WRAP_CONTENT;
    private int mGravity = CENTER;
    private int mOffsetX = 0;
    private int mOffsetY = 0;
    private int mAnimation = R.style.DialogBaseAnimation;
    protected DialogResultListener mDialogResultListener;
    protected DialogDismissListener mDialogDismissListener;
}
  • mWidth 是 Dialog 的宽
  • mHeight 是 Dialog 的高
  • mGravity 是 Dialog 的出现位置
  • mOffsetX 是 Dialog 在 x 方向上的偏移
  • mOffsetY 是 Dialog 在 y 方向上的偏移
  • mAnimation 是 Dialog 的动画
  • mDialogResultListener 是 Dialog 返回结果的回调
  • mDialogDismissListener 是 Dialog 取消时的回调

DialogBaseAnimation 是我自己定义的基本的动画样式,在 res-value-styles 下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="DialogBaseAnimation">
        <item name="android:windowEnterAnimation">@anim/dialog_enter</item>
        <item name="android:windowExitAnimation">@anim/dialog_out</item>
    </style>
</resources>

在 res下新建文件夹 anim ,然后在里面新建两个文件:1、dialog_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<translate
    android:fromYDelta="100%p"
    android:toYDelta="0%p"
    android:duration="200"
    xmlns:android="http://schemas.android.com/apk/res/android">
</translate>

2、dialog_out.xml

<?xml version="1.0" encoding="utf-8"?>
<translate
    android:fromYDelta="0%p"
    android:toYDelta="100%p"
    android:duration="200"
    xmlns:android="http://schemas.android.com/apk/res/android">
</translate>

我们需要的基本属性已经好了,接下来就是如何通过构建者模式来赋值了。

1.2 构建 Builder

我们在 BaseDialogFragment 中新建 Builder:

/**
 * @author SmartSean 
 */

public abstract class BaseDialogFragment extends DialogFragment {

    private int mWidth = WRAP_CONTENT;
    private int mHeight = WRAP_CONTENT;
    private int mGravity = CENTER;
    private int mOffsetX = 0;
    private int mOffsetY = 0;
    private int mAnimation = R.style.DialogBaseAnimation;
    protected DialogResultListener mDialogResultListener;
    protected DialogDismissListener mDialogDismissListener;

    public static abstract class Builder<T extends Builder, D extends BaseDialogFragment> {
        private int mWidth = WRAP_CONTENT;
        private int mHeight = WRAP_CONTENT;
        private int mGravity = CENTER;
        private int mOffsetX = 0;
        private int mOffsetY = 0;
        private int mAnimation = R.style.DialogBaseAnimation;

        public T setSize(int mWidth, int mHeight) {
            this.mWidth = mWidth;
            this.mHeight = mHeight;
            return (T) this;
        }

        public T setGravity(int mGravity) {
            this.mGravity = mGravity;
            return (T) this;
        }

        public T setOffsetX(int mOffsetX) {
            this.mOffsetX = mOffsetX;
            return (T) this;
        }

        public T setOffsetY(int mOffsetY) {
            this.mOffsetY = mOffsetY;
            return (T) this;
        }

        public T setAnimation(int mAnimation) {
            this.mAnimation = mAnimation;
            return (T) this;
        }

        protected abstract D build();

        protected void clear() {
            this.mWidth = WRAP_CONTENT;
            this.mHeight = WRAP_CONTENT;
            this.mGravity = CENTER;
            this.mOffsetX = 0;
            this.mOffsetY = 0;
        }
    }
}

可以看到:

Builder 是一个泛型抽象类,可以传入当前 Buidler 的子类 T 和 BaseDialogFragment 的子类 D,

我们在 Builder 中对可以在 Bundle 中存储的变量都进行了赋值,并且返回泛型 T,在最终的抽象方法 build() 中返回泛型 D。

这里使用抽象的 build() 方法是因为:每个最终的 Dialog 返回的内容是不一样的,需要子类去实现。

你可能会问,前面定义的 mDialogResultListener 和 mDialogDismissListener 怎么没在 Buidler 中出现呢?

我们知道 接口类型是不能存储在 Bundle 中的,所以我们放在了 BaseDialogFragment 中,后面你会看到,不要急。。。

1.3 让子类也能使用这些属性

为了能够让子类也能使用我们在上面 Builder 中构建的属性,我们需要写一个方法,把 Builder 中获取到的值放到 Bundle 中,然后在 Fragment 的 onCreate 方法中进行赋值,

获取 Bundle :

protected static Bundle getArgumentBundle(Builder b) {
        Bundle bundle = new Bundle();
        bundle.putInt("mWidth", b.mWidth);
        bundle.putInt("mHeight", b.mHeight);
        bundle.putInt("mGravity", b.mGravity);
        bundle.putInt("mOffsetX", b.mOffsetX);
        bundle.putInt("mOffsetY", b.mOffsetY);
        bundle.putInt("mAnimation", b.mAnimation);
        return bundle;
    }

在 onCreate 中赋值:

@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mWidth = getArguments().getInt("mWidth");
            mHeight = getArguments().getInt("mHeight");
            mOffsetX = getArguments().getInt("mOffsetX");
            mOffsetY = getArguments().getInt("mOffsetY");
            mAnimation = getArguments().getInt("mAnimation");
            mGravity = getArguments().getInt("mGravity");
        }
    }

这样我们就可以在子类中 通过 getArgumentBundle 方法拿到 通过 Builder 拿到的值了。并且不需要在每个子 Dialog 中获取这些值了,因为父类已经在 onCreate 中取过了。

1.4 重写 onCreateView 方法

使用 DialogFragment 必须重写 onCreateView 或者 onCreateDialog ,我们这里选择使用重写 onCreateView,因为我觉得一个项目中的 Dialog 中的样式不会有太多,重写 onCreateView 这样灵活性高,复用起来很方便。

@Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        setStyle();
        return setView(inflater, container, savedInstanceState);
    }

首先我们通过 style() 设置了 Dialog 所要遵循的样式:

/**
     * 设置统一样式
     */
    private void setStyle() {
        //获取Window
        Window window = getDialog().getWindow();
        //无标题
        getDialog().requestWindowFeature(STYLE_NO_TITLE);
        // 透明背景
        getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        //设置宽高
        window.getDecorView().setPadding(0, 0, 0, 0);
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.width = mWidth;
        wlp.height = mHeight;
        //设置对齐方式
        wlp.gravity = mGravity;
        //设置偏移量
        wlp.x = DensityUtil.dip2px(getDialog().getContext(), mOffsetX);
        wlp.y = DensityUtil.dip2px(getDialog().getContext(), mOffsetY);
        //设置动画
        window.setWindowAnimations(mAnimation);
        window.setAttributes(wlp);
    }

而 setView 则是一个抽象方法,让子类根据实际需求去实现:

protected abstract View setView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState);

1.5 实现 Dialog 回调事件

看下我们定义的两个回调:

public interface DialogResultListener<T> {
    void result(T result);
}
public interface DialogDismissListener{
    void dismiss(DialogFragment dialog);
}

给我们的 DialogFragment 回调赋值:

public BaseDialogFragment setDialogResultListener(DialogResultListener dialogResultListener) {
        this.mDialogResultListener = dialogResultListener;
        return this;
    }

    public BaseDialogFragment setDialogDismissListener(DialogDismissListener dialogDismissListener) {
        this.mDialogDismissListener = dialogDismissListener;
        return this;
    }

这里我们通过 set 方法给两个回调监听赋值,并且最终都返回 this,但是这里并不是真的返回 BaseDialogFragment,而是调用该方法的 BaseDialogFragment 的子类。

至于为什么不放到 Builder 里面,前面已经说了,接口实例不能放到 Bundle 中。

然后在 onDismiss 中回调我们的 DialogDismissListener

@Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (mDialogDismissListener != null) {
            mDialogDismissListener.dismiss(this);
        }
    }

至于 DialogResultListener 则需要根据具体的 Dialog 实现去回调不同的内容。

至此,我们的基础搭建已经完成,这里再贴下完整的代码,不需要的直接略过,往后翻去看具体实现。

BaseDialogFragment

/**
 * @author SmartSean
 */

public abstract class BaseDialogFragment extends DialogFragment {

    private int mWidth = WRAP_CONTENT;
    private int mHeight = WRAP_CONTENT;
    private int mGravity = CENTER;
    private int mOffsetX = 0;
    private int mOffsetY = 0;
    private int mAnimation = R.style.DialogBaseAnimation;
    protected DialogResultListener mDialogResultListener;
    protected DialogDismissListener mDialogDismissListener;

    protected static Bundle getArgumentBundle(Builder b) {
        Bundle bundle = new Bundle();
        bundle.putInt("mWidth", b.mWidth);
        bundle.putInt("mHeight", b.mHeight);
        bundle.putInt("mGravity", b.mGravity);
        bundle.putInt("mOffsetX", b.mOffsetX);
        bundle.putInt("mOffsetY", b.mOffsetY);
        bundle.putInt("mAnimation", b.mAnimation);
        return bundle;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mWidth = getArguments().getInt("mWidth");
            mHeight = getArguments().getInt("mHeight");
            mOffsetX = getArguments().getInt("mOffsetX");
            mOffsetY = getArguments().getInt("mOffsetY");
            mAnimation = getArguments().getInt("mAnimation");
            mGravity = getArguments().getInt("mGravity");
        }
    }

    protected abstract View setView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState);

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        setStyle();
        return setView(inflater, container, savedInstanceState);
    }

    /**
     * 设置统一样式
     */
    private void setStyle() {
        //获取Window
        Window window = getDialog().getWindow();
        //无标题
        getDialog().requestWindowFeature(STYLE_NO_TITLE);
        // 透明背景
        getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        //设置宽高
        window.getDecorView().setPadding(0, 0, 0, 0);
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.width = mWidth;
        wlp.height = mHeight;
        //设置对齐方式
        wlp.gravity = mGravity;
        //设置偏移量
        wlp.x = DensityUtil.dip2px(getDialog().getContext(), mOffsetX);
        wlp.y = DensityUtil.dip2px(getDialog().getContext(), mOffsetY);
        //设置动画
        window.setWindowAnimations(mAnimation);
        window.setAttributes(wlp);
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (mDialogDismissListener != null) {
            mDialogDismissListener.dismiss(this);
        }
    }

    public BaseDialogFragment setDialogResultListener(DialogResultListener dialogResultListener) {
        this.mDialogResultListener = dialogResultListener;
        return this;
    }

    public BaseDialogFragment setDialogDismissListener(DialogDismissListener dialogDismissListener) {
        this.mDialogDismissListener = dialogDismissListener;
        return this;
    }

    public static abstract class Builder<T extends Builder, D extends BaseDialogFragment> {
        private int mWidth = WRAP_CONTENT;
        private int mHeight = WRAP_CONTENT;
        private int mGravity = CENTER;
        private int mOffsetX = 0;
        private int mOffsetY = 0;
        private int mAnimation = R.style.DialogBaseAnimation;

        public T setSize(int mWidth, int mHeight) {
            this.mWidth = mWidth;
            this.mHeight = mHeight;
            return (T) this;
        }

        public T setGravity(int mGravity) {
            this.mGravity = mGravity;
            return (T) this;
        }

        public T setOffsetX(int mOffsetX) {
            this.mOffsetX = mOffsetX;
            return (T) this;
        }

        public T setOffsetY(int mOffsetY) {
            this.mOffsetY = mOffsetY;
            return (T) this;
        }

        public T setAnimation(int mAnimation) {
            this.mAnimation = mAnimation;
            return (T) this;
        }

        protected abstract D build();

        protected void clear() {
            this.mWidth = WRAP_CONTENT;
            this.mHeight = WRAP_CONTENT;
            this.mGravity = CENTER;
            this.mOffsetX = 0;
            this.mOffsetY = 0;
        }
    }
}

二、如何方便的构建 Dialog

这里我们以确认、取消选择框为例:

2.1 首先,我们需要新建 ConfirmDialog 继承于 我们的 BaseDialogFragment:

public class ConfirmDialog extends BaseDialogFragment {

    @Override
    protected View setView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        return null;
    }
}

2.2 构造 Dialog 正常显示需要的值

在通常的确认、取消选择框中,我们需要传入的值有什么呢?

来看下具体的展示:

一步一步使用 DialogFragment 封装链式调用 Dialog
  • 标题
  • 内容
  • 取消的提示文字
  • 确定的提示文字

这里我们定义四个 静态字符换常量:

private static final String LEFT_TEXT = "left_text";
    private static final String RIGHT_TEXT = "right_text";
    private static final String PARAM_TITLE = "title";
    private static final String PARAM_MESSAGE = "message";

接下来我们需要在 Builder 中传入这些值:

新建 Buidler 继承于 BaseDialogFragment 的 Buidler:

public static class Builder extends BaseDialogFragment.Builder<Builder, ConfirmDialog> {

        private String mTitle;
        private String mMessage;
        private String leftText;
        private String rightText;

        public Builder setTitle(String title) {
            mTitle = title;
            return this;
        }

        public Builder setMessage(String message) {
            mMessage = message;
            return this;
        }

        public Builder setLeftText(String leftText) {
            this.leftText = leftText;
            return this;
        }

        public Builder setRightText(String rightText) {
            this.rightText = rightText;
            return this;
        }

        @Override
        protected ConfirmDialog build() {
            return ConfirmDialog.newInstance(this);
        }
    }

在 build 方法中我们返回了 ConfirmDialog的实例,来看下 newInstance 方法:

private static ConfirmDialog newInstance(Builder builder) {
        ConfirmDialog dialog = new ConfirmDialog();
        Bundle bundle = getArgumentBundle(builder);
        bundle.putString(LEFT_TEXT, builder.leftText);
        bundle.putString(RIGHT_TEXT, builder.rightText);
        bundle.putString(PARAM_TITLE, builder.mTitle);
        bundle.putString(PARAM_MESSAGE, builder.mMessage);
        dialog.setArguments(bundle);
        return dialog;
    }

可以看到,我们 new 出了一个 ConfirmDialog 实例,然后通过 getArgumentBundle(builder) 获得了在 BaseDialogFragment 中获取的到值,并且放到了 Bundle 中。

很显然,我们这个 ConfirmDialog 还需要

  • 标题 builder.mTitle
  • 内容 builder.mMessage
  • 取消的提示文字 builder.leftText
  • 确定的提示文字 builder.rightText

最后通过 dialog.setArguments(bundle);传入到 ConfirmDialog 中,返回我们新建的 dialog 实例。

2.3 把值展示到界面上

我们新建 dialog_confirm.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#ffffff">
        <TextView
            android:background="#9d9d9d"
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:gravity="center"
            android:text="我是标题" />
        <TextView
            android:padding="24dp"
            android:id="@+id/message"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@+id/title"
            android:gravity="start"
            android:text="我是message" />
    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/cancel_btn"
            android:layout_width="101dp"
            android:layout_height="46dp"
            android:layout_weight="1"
            android:text="取消" />
        <Button
            android:id="@+id/confirm_btn"
            android:layout_width="103dp"
            android:layout_height="48dp"
            android:layout_weight="1"
            android:text="确定" />
    </LinearLayout>
</LinearLayout>

这个时候就需要在 setView 方法中获取到 dialog_confirm.xml 的控件,然后进行赋值和事件操作:

setView() 方法如下:

@Override
    protected View setView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.dialog_confirm, container, false);
        TextView titleTv = view.findViewById(R.id.title);
        TextView messageTv = view.findViewById(R.id.message);

        if (!TextUtils.isEmpty(getArguments().getString(PARAM_TITLE))) {
            titleTv.setText(getArguments().getString(PARAM_TITLE));
        }
        if (!TextUtils.isEmpty(getArguments().getString(PARAM_MESSAGE))) {
            messageTv.setText(getArguments().getString(PARAM_MESSAGE));
        }
        setBottomButton(view);
        return view;
    }
    
    protected void setBottomButton(View view) {
        Button cancelBtn = view.findViewById(R.id.cancel_btn);
        Button confirmBtn = view.findViewById(R.id.confirm_btn);
        if (getArguments() != null) {
            cancelBtn.setText(getArguments().getString(LEFT_TEXT));
            confirmBtn.setText(getArguments().getString(RIGHT_TEXT));
            cancelBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (mDialogResultListener != null) {
                        mDialogResultListener.result(false);
                        dismiss();
                    }
                }
            });
            confirmBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (mDialogResultListener != null) {
                        mDialogResultListener.result(true);
                        dismiss();
                    }
                }
            });
        }
    }

3.4 最后的调用:

在 MainActivity 中:

ConfirmDialog.newConfirmBuilder()
        .setTitle("这是一个带有确认、取消的dialog")
        .setMessage("这是一个带有确认、取消的dialog的message")
        .setLeftText("我点错了")
        .setRightText("我确定")
        .setAnimation(R.style.DialogAnimFromCenter)
        .build()
        .setDialogResultListener(new DialogResultListener<Boolean>() {
            @Override
            public void result(Boolean result) {
                Toast.makeText(mContext, "你点击了:" + (result ? "确定" : "取消"), Toast.LENGTH_SHORT).show();
            }
        })
        .setDialogDismissListener(new DialogDismissListener() {
            @Override
            public void dismiss(DialogFragment dialog) {
                Toast.makeText(mContext, "我的tag:" + dialog.getTag(), Toast.LENGTH_SHORT).show();
            }
        })
        .show(getFragmentManager(), "confirmDialog");

是不是调用起来很简单,当项目中的 Dialog 样式统一的时候,用这种封装是很方便的,我们只用更改传入的值就可以得到不同的 Dialog,不用写那么多的重复代码,省下的时间可以让我们做很多事情。

最后贴下代码地址:

源码地址

如果你有更好的想法,欢迎提出来~~~

相关推荐