日常开发中,Dialog 是一个每个 app 所必备的。
最后封装好的 BaseDialogFragment 已经添加到我的快速开发 lib 包中。
可以通过:implementation cn.smartsean:lib:0.0.7
快速引入,
也可以去 AndroidCode 查看示例源码。
通常来说,每个 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
2、dialog_out.xml
我们需要的基本属性已经好了,接下来就是如何通过构建者模式来赋值了。
我们在 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:
这个时候就需要在 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,不用写那么多的重复代码,省下的时间可以让我们做很多事情。
如果你有更好的想法,欢迎提出来~~~