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

前言

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


2018-01-31更新

最后封装好的 BaseDialogFragment 已经添加到我的快速开发 lib 包中。

可以通过:implementation cn.smartsean:lib:0.0.7 快速引入,

也可以去 AndroidCode 查看示例源码。


通常来说,每个 app 的Dialog 的样式一般都是统一风格的,比如说有:
- 确认、取消的 Dialog
- 提示性的 Dialog
- 列表选择的 Dialog
- 版本更新的 Dialog
- 带输入框的 Dialog

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

那为什么要使用 DialogFragment 呢?

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

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

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

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

扫描二维码关注公众号,回复: 2657035 查看本文章

效果图

一、构建 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 正常显示需要的值

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

来看下具体的展示:

  • 标题
  • 内容
  • 取消的提示文字
  • 确定的提示文字

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

    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,不用写那么多的重复代码,省下的时间可以让我们做很多事情。

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

猜你喜欢

转载自blog.csdn.net/Sean_css/article/details/79239929
今日推荐