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

前言

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


2018-01-31更新

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

能够经过:implementation cn.smartsean:lib:0.0.7 快速引入,git

也能够去 AndroidCode 查看示例源码。github


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

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

若是每一个都要单独写,就显得有点浪费了,通常状况下,咱们都须要进行封装,便于使用和阅读。app

那为何要使用 DialogFragment 呢?ide

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

而且 DialogFragment 也容许开发者把 Dialog 做为内嵌的组件进行重用,相似 Fragment (能够在大屏幕和小屏幕显示出不一样的效果)ui

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

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

效果图

1、构建 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 ,而后在里面新建两个文件: 一、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>
复制代码

二、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;
        }
    }
}
复制代码

2、如何方便的构建 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,不用写那么多的重复代码,省下的时间可让咱们作不少事情。

若是你有更好的想法,欢迎提出来~~~

相关文章
相关标签/搜索