1.Android原生的AlertDialog
我们今天看一下AlertDialog的创建方式以及它使用到的builder设计模式
我们先看看原生Android的AlertDialog创建方式:
AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("标题")
.setNegativeButton("取消", (dialog, which) -> {
dialog.dismiss();
Toast.makeText(MainActivity.this, "点击了取消", Toast.LENGTH_SHORT).show();
})
.setPositiveButton("确定", (dialog, which) -> Toast.makeText(MainActivity.this, "点击了确定", Toast.LENGTH_SHORT).show())
.setCancelable(false)
.setIcon(R.drawable.ic_launcher_background)
.setMessage("这是消息这是消息这是消息这是消息")
.setView(button)
.create();
alertDialog.show();
创建效果
如果理解了Builer设计模式 那么以上的代码是很好理解的,关于Builder设计模式 我之前也有过总结,不过不是很容易懂,找到一篇更好理解的博客:
https://www.jianshu.com/p/afe090b2e19c
简单来说 Builder设计模式用于构建复杂对象,一般用于该对象有很多可有可无的参数
一般该设计模式分为以下几个部分
Director:创建者 用于获取要创建的对象(Product)
Builder:一般定义为接口(如果有继承的子类的话),定义了产品的设置接口,可以创建产品
实际Builder:(可有可无 取决于系统有多复杂)Builder的继承者,实际的Builder
Product:需要创建的目标对象
将AlertDialog套用到以上这个模板那么有如下结果
AlertDialog代表产品(Product)
AlertDialog.Builder代表builder
AlertController代表Director 用于控制产品具体显示哪些部分
让我们再细看一下AlertDialog的源码 分析这三者的结构
2.Android原生AlertDialog源码结构
AlertDialog含有内部类Builder
AlertDialog在调用create创建AlertDialog对象的时候 实际调用的是AlertController的create方法
AlertController内部还有个关键的内部类AlertParams
从上面的demo可以看出AlertDialog的使用顺序大概是
1.调用各种set方法
2.调用create方法
3.调用show方法
调用set方法是将各种参数设置进Builder的内部属性AlertParams中去
调用create方法时 创建了AlertController对象 同时将之前设置的属性应用到dialog中
最后调用show方法
让我们看一下源码的调用顺序
首先调用new AlertDialog.Builder(MainActivity.this)创建builder对象 同时创建AlertParams的实例P
public Builder(Context context) {
this(context, resolveDialogTheme(context, ResourceId.ID_NULL));
}
public Builder(Context context, int themeResId) {
//这里创建了AlertParams的实例P
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
}
第二步调用builder的各种set方法 比如
public Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}
public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}
public Builder setIcon(@DrawableRes int iconId) {
P.mIconId = iconId;
return this;
}
可以看到虽然这些是builder的方法 但是实际操作的却是AlertParams的实例P 即AlertParams用来存放Dialog的各种参数和style
第三步调用builder的create方法
public AlertDialog create() {
// Context has already been wrapped with the appropriate theme.
final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);//跟下去
P.apply(dialog.mAlert);//应用之前设置在AlertController.AlertParams的各种参数
dialog.setCancelable(P.mCancelable);//应用之前设置在AlertController.AlertParams的各种参数
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
AlertDialog(Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0,
createContextThemeWrapper);
mWindow.alwaysReadCloseOnTouchAttr();
mAlert = AlertController.create(getContext(), this, getWindow());//创建AlertController对象
}
public static final AlertController create(Context context, DialogInterface di, Window window) {
final TypedArray a = context.obtainStyledAttributes(
null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
int controllerType = a.getInt(R.styleable.AlertDialog_controllerType, 0);
a.recycle();
switch (controllerType) {
case MICRO:
return new MicroAlertController(context, di, window);
default:
return new AlertController(context, di, window);//我这里走的这个case
}
}
//设置各种style和layout
protected AlertController(Context context, DialogInterface di, Window window) {
mContext = context;
mDialogInterface = di;
mWindow = window;
mHandler = new ButtonHandler(di);
final TypedArray a = context.obtainStyledAttributes(null,
R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
mAlertDialogLayout = a.getResourceId(
R.styleable.AlertDialog_layout, R.layout.alert_dialog);
mButtonPanelSideLayout = a.getResourceId(
R.styleable.AlertDialog_buttonPanelSideLayout, 0);
mListLayout = a.getResourceId(
R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
mMultiChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_multiChoiceItemLayout,
R.layout.select_dialog_multichoice);
mSingleChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_singleChoiceItemLayout,
R.layout.select_dialog_singlechoice);
mListItemLayout = a.getResourceId(
R.styleable.AlertDialog_listItemLayout,
R.layout.select_dialog_item);
mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
a.recycle();
/* We use a custom title so never request a window title */
window.requestFeature(Window.FEATURE_NO_TITLE);
}
第四步调用show方法
3.自己写一个万能Dialog
思路 按照之前分析的 builder的构造方法,设置属性,create,show等顺序 参考原生Android的代码进行代码编写
代码展示
public class AlertDialog extends Dialog implements DialogInterface {
private final AlertController mAlert;
//protected方法 让外部无法直接new对象 只能通过Builder创建对象
protected AlertDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
mAlert = new AlertController(this, getWindow());
}
//通过AlertDialog的引用可以setText 虽然和builder中的setText方法最终调用的都是AlertController的方法
//但是builder 一般在初次创建的时候才会使用 如果后期需要变化文字 可以通过AlertDialog的引用设置
public void setText(int viewId, CharSequence text) {
mAlert.setText(viewId,text);
}
//可以通过AlertDialog的引用根据viewId获取指定的view
public <T extends View> T getView(int viewId) {
return mAlert.getView(viewId);
}
//可以通过AlertDialog的引用设置点击事件
public void setOnclickListener(int viewId, View.OnClickListener listener) {
mAlert.setOnclickListener(viewId,listener);
}
public static class Builder {
private final AlertController.AlertParams P;
//各种set方法 都是直接将参数保存到AlertParams中
//存储标题
public AlertDialog.Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}
//存储标题方式2
public AlertDialog.Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
//存储消息体
public AlertDialog.Builder setMessage(@StringRes int messageId) {
P.mMessage = P.mContext.getText(messageId);
return this;
}
//存储dialog的布局
public Builder setContentView(View view) {
P.mView = view;
P.mViewLayoutResId = 0;
return this;
}
//另一种方法存储dialog的布局
public Builder setContentView(int layoutId) {
P.mView = null;
P.mViewLayoutResId = layoutId;
return this;
}
//和通过AlertDialog的引用setText的最终方式一样
//这里只是存储变量
public Builder setText(int viewId,CharSequence text){
P.mTextArray.put(viewId,text);
return this;
}
//存储dialog取消的listener
public AlertDialog.Builder setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) {
P.mOnCancelListener = onCancelListener;
return this;
}
//存储dialog的宽度
public Builder fullWidth(){
P.mWidth = ViewGroup.LayoutParams.MATCH_PARENT;
return this;
}
//存储是否从底部弹出 并且是否使用动画
public Builder formBottom(boolean isAnimation){
if(isAnimation){
P.mAnimations = R.style.dialog_from_bottom_anim;
}
P.mGravity = Gravity.BOTTOM;
return this;
}
//存储dialog的宽高
public Builder setWidthAndHeight(int width, int height){
P.mWidth = width;
P.mHeight = height;
return this;
}
//存储dialog的默认动画
public Builder addDefaultAnimation(){
P.mAnimations = R.style.dialog_scale_anim;
return this;
}
//存储dialog的动画
public Builder setAnimations(int styleAnimation){
P.mAnimations = styleAnimation;
return this;
}
//存储dialog点击空白区域是否可以取消
public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}
//存储dialog消失的监听
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
//存储dialog键值监听
public Builder setOnKeyListener(OnKeyListener onKeyListener) {
P.mOnKeyListener = onKeyListener;
return this;
}
//end各种set方法
//构造方法
public Builder(Context context) {
this(context, R.style.dialog);
}
//构造方法 带有主题
public Builder(Context context, int themeResId) {
P = new AlertController.AlertParams(context, themeResId);
}
//创建dialog对象
public AlertDialog create() {
// Context has already been wrapped with the appropriate theme.
final AlertDialog dialog = new AlertDialog(P.mContext, P.mThemeResId);
P.apply(dialog.mAlert);//将AlertParams中的参数应用到dialog中 间接应用 (将参数传递给AlertController 接着还有可能通过DialogViewHelper最终影响dialog)
dialog.setCancelable(P.mCancelable);//将AlertParams中的参数应用到dialog中 直接应用
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);//将AlertParams中的参数应用到dialog中 直接应用
}
dialog.setOnCancelListener(P.mOnCancelListener);//将AlertParams中的参数应用到dialog中 直接应用
dialog.setOnDismissListener(P.mOnDismissListener);//将AlertParams中的参数应用到dialog中 直接应用
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);//将AlertParams中的参数应用到dialog中 直接应用
}
return dialog;
}
//显示方法
public AlertDialog show() {
final AlertDialog dialog = create();
dialog.show();
return dialog;
}
}
}
public class AlertController {
//AlertController控制的Dialog
private final AlertDialog mDialog;
//Dialog显示的window
private final Window mWindow;
//辅助类 通过AlertController控制Dialog的布局显示内容 view的事件监听等
private DialogViewHelper mViewHelper;
//构造方法在创建dialog对象时调用
public AlertController(AlertDialog alertDialog, Window window) {
this.mDialog = alertDialog;
this.mWindow = window;
}
//设置辅助类 在apply AlertController中的属性时调用
public void setViewHelper(DialogViewHelper viewHelper) {
this.mViewHelper = viewHelper;
}
//外部通过调用controller的该方法setText
public void setText(int viewId, CharSequence text) {
//调用DialogViewHelper操作具体的view setText
mViewHelper.setText(viewId, text);
}
//外部通过调用controller的该方法取得view对象
public <T extends View> T getView(int viewId) {
//内部调用DialogViewHelper操作具体的view 得到view对象
return mViewHelper.getView(viewId);
}
//外部通过调用controller的该方法set点击事件
public void setOnclickListener(int viewId, View.OnClickListener listener) {
//内部调用DialogViewHelper操作具体的view 给view设置点击事件
mViewHelper.setOnclickListener(viewId, listener);
}
//给静态内部类使用的方法
private AlertDialog getDialog() {
return mDialog;
}
//给静态内部类使用的方法
private Window getWindow() {
return mWindow;
}
//静态内部类 可以脱离AlertController存在 用于存储dialog的各种变量
static class AlertParams {
public CharSequence mTitle;//存储title
public int mThemeResId;//存储theme
public boolean mCancelable = true;//默认可以点击空白收起dialog
public Context mContext;
public CharSequence mMessage;//存储Message
//存储各种listener
public DialogInterface.OnDismissListener mOnDismissListener;
public DialogInterface.OnKeyListener mOnKeyListener;
public DialogInterface.OnCancelListener mOnCancelListener;
//布局和布局id二选一
public View mView;
public int mViewLayoutResId;
//存放int和Object的键值对 比hash map更高效
//存放各个部分的文字 是id和CharSequence的键值对
public SparseArray<CharSequence> mTextArray = new SparseArray<>();
//存储各个控件的点击事件 是viewId和OnClickListener的键值对
public SparseArray<View.OnClickListener> mClickArray = new SparseArray<>();
//存储宽度
public int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
//存储动画的资源id
public int mAnimations = 0;
//存储显示位置
public int mGravity = Gravity.CENTER;
//存储高度
public int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
public AlertParams(Context context, int themeResId) {
this.mContext = context;
this.mThemeResId = themeResId;
}
//将属性直接应用到Dialog或者 通过viewHelper操作Dialog的view
public void apply(AlertController mAlert) {
DialogViewHelper viewHelper = null;
//设置dialog的资源id
if (mViewLayoutResId != 0) {
viewHelper = new DialogViewHelper(mContext, mViewLayoutResId);
}
//设置dialog的资源id的另一种方式
if (mView != null) {
viewHelper = new DialogViewHelper();
viewHelper.setContentView(mView);
}
if (viewHelper == null) {
throw new IllegalArgumentException("请设置布局view或布局id");
}
//真正设置dialog的地方
mAlert.getDialog().setContentView(viewHelper.getContentView());
//感觉没什么用 除非外部想要直接操作AlertController类 此前还要提供getViewHelper方法
mAlert.setViewHelper(viewHelper);
int textArraySize = mTextArray.size();
for (int i = 0; i < textArraySize; i++) {
//调用DialogViewHelper操作具体的view setText
mAlert.setText(mTextArray.keyAt(i), mTextArray.valueAt(i));
}
int clickArraySize = mClickArray.size();
for (int i = 0; i < clickArraySize; i++) {
//调用DialogViewHelper操作具体的view setOnclickListener
mAlert.setOnclickListener(mClickArray.keyAt(i), mClickArray.valueAt(i));
}
Window window = mAlert.getWindow();
// 设置位置
window.setGravity(mGravity);
//设置动画
if (mAnimations != 0) {
window.setWindowAnimations(mAnimations);
}
//设置宽高
WindowManager.LayoutParams params = window.getAttributes();
params.width = mWidth;
params.height = mHeight;
window.setAttributes(params);
}
}
}
class DialogViewHelper {
private static final String TAG = "DialogViewHelper";
//dialog的view
private View mContentView = null;
//存储了dialog上的各个view viewId+View的键值对 目的是减少findViewById的调用次数
//为防止内存泄漏 使用WeakReference
private final SparseArray<WeakReference<View>> mViews = new SparseArray<>();
public DialogViewHelper(Context context, int layoutResId) {
mContentView = LayoutInflater.from(context).inflate(layoutResId, null);
}
public DialogViewHelper() {
}
//根据view id操作该view的Text
public void setText(int viewId, CharSequence text) {
// 每次都 findViewById 减少findViewById的次数
View view = getView(viewId);
if (view != null) {
try {
//利用反射使得所有带有setText(CharSequence)的方法的控件都可以调用setText的方法
Class clazz = view.getClass();
Method setTextMethod = clazz.getMethod("setText", CharSequence.class);
setTextMethod.invoke(view, text);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
Log.e(TAG, "setText:"+e.getMessage());
e.printStackTrace();
}
}
}
//获取view
public <T extends View> T getView(int viewId) {
WeakReference<View> viewReference = mViews.get(viewId);
//利用弱引用防止内存泄漏
View view = null;
if (viewReference != null) {
view = viewReference.get();
}
if (view == null) {
view = mContentView.findViewById(viewId);
if (view != null) {
mViews.put(viewId, new WeakReference<>(view));
}
}
return (T) view;
}
//操作view setOnClickListener
public void setOnclickListener(int viewId, View.OnClickListener listener) {
View view = getView(viewId);
if (view != null) {
view.setOnClickListener(listener);
}
}
//存储dialog的view
public void setContentView(View contentView) {
this.mContentView = contentView;
}
//获取dialog的view
public View getContentView() {
return mContentView;
}
}
使用的方式:
com.example.dialog.AlertDialog myDialog = new com.example.dialog.AlertDialog.Builder(this)
.setContentView(R.layout.detail_comment_dialog)
// .setText(R.id.share_label, "评论哈哈")
.fullWidth().show();
// dialog去操作点击事件
final EditText commentEt = myDialog.getView(R.id.comment_editor);
myDialog.setOnclickListener(R.id.submit_btn, v12 -> Toast.makeText(MainActivity.this,
commentEt.getText().toString().trim(), Toast.LENGTH_LONG).show());
myDialog.setText(R.id.share_label, "评论哈哈1111");
显示效果
代码流程分析
AlertDialog AlertController DialogViewHelper一一对应,每一次创建一个AlertDialog 必然有一个唯一AlertController 一个唯一DialogViewHelper被创建
1.new com.example.dialog.AlertDialog.Builder调用时AlertDialog.Builder创建
2.在builder的构造方法中AlertDialog.Builder创建
3.set方法被调用 这些参数被存储在Builder中AlertParams的变量中
4.AlertDialog show方法调用 内部调用create方法 AlertDialog创建
5.AlertDialog构造方法中AlertController创建
6.apply方法调用DialogViewHelper创建 各种参数被应用在dialog上
对比上面Android原生的AlertDialog 可以看到步骤基本一致,或者说是抄的Android源码更合适,哈哈,不过我们现在可以自己更容易决定想要显示成什么样子
再次总结创建顺序和作用
1.AlertDialog.Builder创建 该对象用于规范dialog的显示式样 有哪些API可以调用
2.AlertController.AlertParams创建 该对象用于存储dialog各种参数
3.AlertDialog创建 实际显示的dialog
4.AlertController创建 控制dialog
5.DialogViewHelper创建 直接操作view的辅助类
DialogViewHelper通过AlertController来控制AlertDialog中的view的显示文本以及事件监听等
如何控制Dialog的显示样式?
这里有两种控制方式
一种直接通过AlertController.AlertParams的参数 apply给dialog
比如formBottom
public Builder formBottom(boolean isAnimation){
if(isAnimation){
P.mAnimations = R.style.dialog_from_bottom_anim;
}
P.mGravity = Gravity.BOTTOM;
return this;
}
上面只是设置参数
在apply函数中有如下调用
Window window = mAlert.getWindow();
// 设置位置
window.setGravity(mGravity);
//设置动画
if (mAnimations != 0) {
window.setWindowAnimations(mAnimations);
}
另一种 如果涉及到view相关的,则需要通过DialogViewHelper控制
比如setText
public void setText(int viewId, CharSequence text) {
mAlert.setText(viewId,text);
}
调用AlertController的方法
public void setText(int viewId, CharSequence text) {
//调用DialogViewHelper操作具体的view setText
mViewHelper.setText(viewId, text);
}
最后调用DialogViewHelper的方法 这里无非是找到view 然后判断它有没有setText(CharSequence)的方法 如果有 则调用该方法
public void setText(int viewId, CharSequence text) {
// 每次都 findViewById 减少findViewById的次数
View view = getView(viewId);
if (view != null) {
try {
//利用反射使得所有带有setText(CharSequence)的方法的控件都可以调用setText的方法
Class clazz = view.getClass();
Method setTextMethod = clazz.getMethod("setText", CharSequence.class);
setTextMethod.invoke(view, text);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
Log.e(TAG, "setText:"+e.getMessage());
e.printStackTrace();
}
}
}
那么如果我们想扩展这个dialog 则看我们的扩展是否会直接操作view,如果不会,则可以模仿formBottom的方式进行扩展
如果会影响view 则可以通过模仿setText的方式进行扩展
完整代码
https://github.com/caihuijian/learn_darren_eassy_joke/tree/main/baseLibrary/src/main/java/com/example/dialog