TingTalk汀说 2018-01-30
日常开发中,Dialog 是一个每个 app 所必备的。
通常来说,每个 app 的Dialog 的样式一般都是统一风格的,比如说有:
如果每个都要单独写,就显得有点浪费了,一般情况下,我们都需要进行封装,便于使用和阅读。
那为什么要使用 DialogFragment 呢?
使用 DialogFragment 来管理对话框,当旋转屏幕和按下后退键时可以更好的管理其生命周期,它和 Fragment 有着基本一致的生命周期。
并且 DialogFragment 也允许开发者把 Dialog 作为内嵌的组件进行重用,类似 Fragment (可以在大屏幕和小屏幕显示出不同的效果)
那么接下来我们就一步一步的来封装出一个便于我们使用的 DialogFragment。
还是先看下效果图吧,可能有点不是很好看,毕竟没有 ui,哈哈
源码地址 可以先去下载代码对照着看。
在构建 BaseDialogFragment 之前,我们先分析下正常情况下,我们使用 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; }
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>
我们需要的基本属性已经好了,接下来就是如何通过构建者模式来赋值了。
我们在 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 中,后面你会看到,不要急。。。
为了能够让子类也能使用我们在上面 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 中取过了。
使用 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);
看下我们定义的两个回调:
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; } } }
这里我们以确认、取消选择框为例:
public class ConfirmDialog extends BaseDialogFragment { @Override protected View setView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return null; } }
在通常的确认、取消选择框中,我们需要传入的值有什么呢?
来看下具体的展示:
这里我们定义四个 静态字符换常量:
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 实例。
我们新建 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(); } } }); } }
在 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,不用写那么多的重复代码,省下的时间可以让我们做很多事情。
最后贴下代码地址:
源码地址
如果你有更好的想法,欢迎提出来~~~