Builder設計模式構建通用型Dialog

目錄

寫在前面

一、什麼是Builder模式

二、AlertDialog源碼分析

2.1、源碼閱讀

2.2、Builder模式工作流程

三、代碼實戰——Builder模式構建通用型Dialog

3.1、基本框架搭建

3.2、完善Builder

3.3、完善真正的構建器

3.4、自定義參數配置

四、使用Dialog


寫在前面

最近在看輝哥的視頻,看到視頻裏他寫Dialog的方式,又想到了自己寫的,瞬間覺得尷尬無比,自己的代碼真的就像一坨XIANG!不說了,這是輝哥的簡書地址:https://www.jianshu.com/u/35083fcb7747,有興趣的可以加波關注,反正我是對他佩服的五體投地,不只是寫代碼哦,各方面都是!

再回到本篇中來,不知不覺又快到五一了,天氣好自然而然的心情也變好了,這麼好的天氣不擼代碼真的可惜了,於是乎就把輝哥視頻中講的內容自己跟着搞了一把,就有了今天這一篇——建造者模式構建萬能Dialog了!看過我博客的小夥伴們應該都還記得關於Dialog的使用,我之前也是寫過的,而且還寫了兩篇,有興趣的可以看一下:

  1. Android自定義通用的Dialog:https://blog.csdn.net/JArchie520/article/details/79157471
  2. Android自定義View之通用Dialog:https://blog.csdn.net/JArchie520/article/details/103454231

對於Dialog的使用沒有任何的難度,但是如何讓你寫的Dialog功能上更加通用,架構設計上更加簡潔並且具有可擴展性,這就需要你認真的思考了,大家看我之前寫的兩篇就能夠看出代碼層次上也是一次比一次清晰,易用性一次比一次要好,雖然很low,但是也說明了咱也是在不斷進步的對吧,那爲了更加易用,程序的可移植性更好,我決定繼續對它改造升級和優化,結合設計模式來封裝一個通用型的Dialog,之所以選擇Builder設計模式,是因爲系統的AlertDialog也是採用這種模式來實現的,可以鏈式調用非常方便。

效果展示:

一、什麼是Builder模式

定義:將一個複雜對象的構建與它的表示分離,使得不同的構建過程可以創建不同的顯示,但其根本還是不變。

使用場景:

  • ①、相同的方法,不同的執行順序,產生不同的事件結果時;
  • ②、多個部件或零件都可以裝配到一個對象中,但是產生的運行結果又不相同時;
  • ③、產品類非常複雜,或者產品類中的調用順序不同產生了不同的效能時;

二、AlertDialog源碼分析

2.1、源碼閱讀

既然我們要採用構建者模式來實現一個Dialog,又因爲Android系統的AlertDialog就是採用構建者模式來實現的,那這一部分就先來看下谷歌的大神們是如何實現的?下面這行代碼是我們在應用層最簡單的一個api調用了,接下來我們就按照這行代碼來依次去到Android系統的源碼中查看一下底層是如何實現的:

new AlertDialog.Builder(this).setTitle("測試").setIcon(R.mipmap.ic_launcher).create().show();

以下源碼基於Android6.0源碼分析:文件目錄:frameworks/base/core/java/android/app/AlertDialog.java(不要找錯了)

首先進入AlertDialog類中,可以看到它的構造方法是protected類型的,這也就意味着你不能直接去new一個AlertDialog對象:

protected AlertDialog(Context context) {
    this(context, 0);
}

它是通過一個內部類Builder在構造方法中new了一個對象P,它是AlertController類的一個靜態內部類對象AlertParams:

public Builder(Context context, int themeResId) {
    private final AlertController.AlertParams P;
    P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
}

然後我們再來看一下setTitle()、setIcon()這些方法又是做了什麼?

public Builder setTitle(@StringRes int titleId) {
    P.mTitle = P.mContext.getText(titleId);
    return this;
}
public Builder setIcon(Drawable icon) {
    P.mIcon = icon;
    return this;
}

從這個代碼中可以看出,這些set方法其實就是給P對象內部去放置一些參數,然後返回Builder自身,也就是這裏的this。

然後接着看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);
    ...省略部分代碼        
}

這個方法中首先就是new了一個Dialog,然後通過P調用了一個apply方法,到這一步就是設置參數了,把Dialog中的參數都從P裏面拿,現在跟到這個apply方法中看一下:

public void apply(AlertController dialog) {
            if (mCustomTitleView != null) {
                dialog.setCustomTitle(mCustomTitleView);
            } else {
                if (mTitle != null) {
                    dialog.setTitle(mTitle);
                }
                if (mIcon != null) {
                    dialog.setIcon(mIcon);
                }
                if (mIconId != 0) {
                    dialog.setIcon(mIconId);
                }
                if (mIconAttrId != 0) {
                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                }
            }
            ...省略一大串代碼
}

可以看到在這裏就是開始組裝P內部的一系列參數,有什麼就拼裝什麼,裏面有一系列的if判斷。

最後是調用了Dialog的show()方法去展示,這個show注意是在Dialog中的,因爲AlertDialog是繼承自Dialog的,這個源碼就不再貼了,裏面的實現還是比較複雜的,涉及到了Window對象的一些概念,有興趣的可以研究研究,因爲不是本篇的重點,所以就不多說了。

2.2、Builder模式工作流程

添加參數(P)--->組裝參數(添加多少就組裝多少)--->顯示

主要涉及的對象:

  • AlertDialog:整體的彈出框對象
  • AlertDialog.Builder:規範一系列的組裝過程
  • AlertController:具體的構建器
  • AlertController.AlertParams:存放參數以及一部分設置參數的功能

三、代碼實戰——Builder模式構建通用型Dialog

3.1、基本框架搭建

從這裏開始我們就來封裝這個通用型Dialog了,首先仿照源碼把基本框架搭建起來,我們先來創建一個類CommonDialog讓它繼承自Dialog,以及它的內部類Builder,這些代碼都是仿照源碼來寫的,其中有些代碼是直接從源碼中拷貝過來然後修改的:

/**
 * 作者: 喬布奇
 * 日期: 2020-04-26 22:42
 * 郵箱: [email protected]
 * 描述: 自定義通用型Dialog
 */
public class CommonDialog extends Dialog {

    private CommonController mController;

    public CommonDialog(@NonNull Context context, int themeResId) {
        super(context, themeResId);
        mController = new CommonController(this,getWindow());
    }

    //創建內部類構建器
    public static class Builder{
        private final CommonController.CommonParams P;

        public Builder(Context context){
            this(context, R.style.dialog);
        }

        public Builder(Context context,int themeId){
            P = new CommonController.CommonParams(context,themeId);
        }

        public CommonDialog create(){
            // Context has already been wrapped with the appropriate theme.
            final CommonDialog dialog = new CommonDialog(P.mContext,P.mThemeResId);
            P.apply(dialog.mController);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

        public CommonDialog show(){
            final CommonDialog dialog = create();
            dialog.show();
            return dialog;
        }
    }
}

這裏給它一個默認的style,也就是在Builder的構造方法中設置的這個R.style.dialog,主題的代碼如下:

    <style name="dialog" parent="@android:style/Theme.Dialog">
        <!--邊框-->
        <item name="android:windowFrame">@null</item>
        <!--是否浮現在Activity之上-->
        <item name="android:windowIsFloating">true</item>
        <!--背景透明-->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!--模糊-->
        <item name="android:backgroundDimEnabled">true</item>
        <!--無標題-->
        <item name="android:windowNoTitle">true</item>
    </style>

然後接着創建Builder中的Dialog的構建器類CommonController以及它的構建參數的內部類CommonParams

/**
 * 作者: 喬布奇
 * 日期: 2020-04-26 22:43
 * 郵箱: [email protected]
 * 描述: 通用型Dialog構建器
 */
class CommonController {
    private CommonDialog mDialog;
    private Window mWindow;

    public CommonController(CommonDialog dialog, Window window) {
        this.mDialog = dialog;
        this.mWindow = window;
    }

    //獲取Dialog
    public CommonDialog getDialog(){
        return mDialog;
    }

    //獲取Dialog的Window對象
    public Window getWindow() {
        return mWindow;
    }

    public static class CommonParams {
        public Context mContext;
        public int mThemeResId;
        //點擊空白是否能夠取消
        public boolean mCancelable = false;
        //dialog Cancel監聽
        public DialogInterface.OnCancelListener mOnCancelListener;
        //dialog Dismiss監聽
        public DialogInterface.OnDismissListener mOnDismissListener;
        //dialog Key監聽
        public DialogInterface.OnKeyListener mOnKeyListener;

        public CommonParams(Context context, int themeResId) {
            this.mContext = context;
            this.mThemeResId = themeResId;
        }

        /**
         * 綁定和設置參數
         * @param mController
         */
        public void apply(CommonController mController) {

        }
    }
}

這裏面的變量及事件監聽都是仿照源碼來的,拷貝修改就OK了,這樣最基本的架子就先搭建起來了!

3.2、完善Builder

這一部分我們來給Builder類的內部添加一系列的setXXX()方法,比如系統Dialog中的設置標題圖標這些東西,這些設置的方法都是一些套路代碼,定義一個方法,返回值類型爲Builder,方法內部進行設置操作,最後返回this即可。

首先我們需要在CommonController中的CommonParams類中添加幾個需要用到的變量:

//佈局View
public View mView;
//佈局Layout ID
public int mViewLayoutResId;
//存放文本的修改,文本可能有多個,需要使用Map存儲,這裏選擇SparseArray因爲它更加高效
public SparseArray<CharSequence> mTextArray = new SparseArray<>();
//存放點擊事件
public SparseArray<View.OnClickListener> mClickArray = new SparseArray<>();

這裏需要注意的點我也在代碼註釋中寫了,因爲我們的Dialog可能是各式各樣的,所以對於文本和點擊事件的設置都是不可控的,無法確定數量上有多少,位置上在哪裏點擊,所以這裏需要採用Map集合去存儲這種多個的情況,又因爲我們都是通過控件id去操作文本及點擊事件的,它符合HashMap<Integer,Object>這種int--->Object的格式,所以這裏選擇使用SparseArray<T>去存儲,它比Map在性能上更加高效。

然後就是Builder中的一系列設置操作了:

        //設置佈局View
        public Builder setContentView(View view){
            P.mView = view;
            P.mViewLayoutResId = 0;
            return this;
        }

        //設置佈局內容LayoutId
        public Builder setContentView(int layoutId){
            P.mView = null;
            P.mViewLayoutResId = layoutId;
            return this;
        }

        //設置文本
        public Builder setText(int viewId,CharSequence text){
            P.mTextArray.put(viewId,text);
            return this;
        }

        //設置點擊事件
        public Builder setOnClickListener(int viewId,View.OnClickListener listener){
            P.mClickArray.put(viewId,listener);
            return this;
        }

        //設置是否可以取消
        public Builder setCancelable(boolean cancelable) {
            P.mCancelable = cancelable;
            return this;
        }

        //設置Cancel監聽
        public Builder setOnCancelListener(OnCancelListener onCancelListener) {
            P.mOnCancelListener = onCancelListener;
            return this;
        }

        //設置Dismiss監聽
        public Builder setOnDismissListener(OnDismissListener onDismissListener) {
            P.mOnDismissListener = onDismissListener;
            return this;
        }

        //設置key監聽
        public Builder setOnKeyListener(OnKeyListener onKeyListener) {
            P.mOnKeyListener = onKeyListener;
            return this;
        }

3.3、完善真正的構建器

在上面分析源碼的時候我們說過,真正的設置參數的操作是P對象調用apply()方法實現的:P.apply(dialog.mAlert),那接下來就來寫我們自己的這個apply()方法,在寫之前我們先定義一個DialogViewHelper類用於View的輔助處理:

/**
 * 作者: 喬布奇
 * 日期: 2020-04-26 22:44
 * 郵箱: [email protected]
 * 描述: Dialog View的輔助處理類
 */
class DialogViewHelper {

    private View mContentView = null;
    //WeakReference防止內存泄漏
    private SparseArray<WeakReference<View>> mViews;

    public DialogViewHelper(Context context, int layoutResId) {
        this();
        mContentView = LayoutInflater.from(context).inflate(layoutResId, null);
    }

    public DialogViewHelper() {
        mViews = new SparseArray<>();
    }

    //設置佈局
    public void setContentView(View contentView) {
        this.mContentView = contentView;
    }

    //設置文本
    public void setText(int viewId, CharSequence text) {
        TextView textView = getView(viewId);
        if (textView != null) {
            textView.setText(text);
        }
    }

    //設置點擊事件
    public void setOnclickListener(int viewId, View.OnClickListener listener) {
        View view = getView(viewId);
        if (view != null) {
            view.setOnClickListener(listener);
        }
    }

    //獲取ContentView
    public View getContentView() {
        return mContentView;
    }

    //通用fv獲取控件
    private <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;
    }
}

這裏需要注意的點是我們定義了一個通用的防止重複綁定控件的方法,已經綁定過的不用再次通過findViewById獲取了,直接從弱引用中拿就行,定義好了這個類,我們就可以先來填充apply方法了,先讓我們的Dialog顯示出來:

        /**
         * 綁定和設置參數
         *
         * @param mController
         */
        public void apply(CommonController mController) {
            DialogViewHelper viewHelper = null;
            //設置Dialog的佈局
            if (mViewLayoutResId != 0) {
                viewHelper = new DialogViewHelper(mContext, mViewLayoutResId);
            }

            if (mView != null) {
                viewHelper = new DialogViewHelper();
                viewHelper.setContentView(mView);
            }

            if (viewHelper == null){
                throw new IllegalArgumentException("請設置佈局setContentView()");
            }
            //給Dialog設置佈局
            mController.getDialog().setContentView(viewHelper.getContentView());

            //設置文本
            int textArraySize = mTextArray.size();
            for (int i=0;i<textArraySize;i++){
                viewHelper.setText(mTextArray.keyAt(i),mTextArray.valueAt(i));
            }

            //設置點擊事件
            int clickArraySize = mClickArray.size();
            for (int i=0;i<textArraySize;i++){
                viewHelper.setOnclickListener(mClickArray.keyAt(i),mClickArray.valueAt(i));
            }
        }

OK,到這裏其實我們的Dialog就已經能夠顯示出來了,只不過還有很多細節需要處理,來繼續往下看吧!

3.4、自定義參數配置

首先我們在CommonController的內部類CommonParams中添加我們需要的自定義參數:

//寬度
public int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
//動畫
public int mAnimations = 0;
//位置
public int mGravity = Gravity.CENTER;
//高度
public int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;

然後在Builder中對添加的這些變量同樣的去賦值也即是set操作,這裏的dialog_scale_anim就是一個簡單的縮放動畫:

        //配置一些通用參數
        public Builder fullWidth(){
            P.mWidth = ViewGroup.LayoutParams.MATCH_PARENT;
            return this;
        }

        //從底部彈出,是否有動畫
        public Builder fromBottom(boolean isAnimation){
            if (isAnimation){
                P.mAnimations = R.style.dialog_from_bottom_anim;
            }
            P.mGravity = Gravity.BOTTOM;
            return this;
        }

        //設置寬高
        public Builder setWidthAndHeight(int width,int height){
            P.mWidth = width;
            P.mHeight = height;
            return this;
        }

        //添加默認動畫
        public Builder addDefaultAnimation(){
            P.mAnimations = R.style.dialog_scale_anim;
            return this;
        }

        //自行設置動畫
        public Builder setAnimations(int styleAnimation){
            P.mAnimations = styleAnimation;
            return this;
        }

最後在apply()方法中進行配置,這裏就要用到我們的Window對象了:

//配置自定義效果:全屏,從底部彈出,動畫等
Window window = mController.getWindow();
//設置位置
window.setGravity(mGravity);
//設置動畫
if (mAnimations != 0) {
    window.setWindowAnimations(mAnimations);
}
//設置寬高
WindowManager.LayoutParams params = window.getAttributes();
params.width = mWidth;
params.height = mHeight;
window.setAttributes(params);

OK,到這裏我們的dialog基本上就已經搞定了,後續你如果需要進行相關的拓展,直接添加你需要的屬性就OK了。好,寫了這麼多,你會覺得這不是更加複雜了嗎?是嗎?造輪子的過程是比較複雜,但是用輪子的時候可不復雜哦,甚至你還會偷着樂,下面就讓我們一起來看一下調用的時候是不是變得相當簡單了呢?

四、使用Dialog

我們先來看一個最簡單的情況,比如我們有些場景下會彈出一些⚠️警告提示之類的Dialog,這種直接給用戶看的,無需操作:

先來自定義一個Dialog的佈局dialog_test_1.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:background="@color/color_white"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher_round"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"/>

    <TextView
        android:id="@+id/mContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:lineSpacingExtra="3dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:layout_marginBottom="20dp"
        android:text="我是彈出內容"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp" />

</LinearLayout>

一個圖片一個文本,這是很簡單的一個場景了,這種調用就很爽了,一直往下點就行了:

new CommonDialog.Builder(MainActivity.this)
    .setContentView(R.layout.dialog_test_1)
    .setWidthAndHeight(DensityUtil.dp2px(300), LinearLayout.LayoutParams.WRAP_CONTENT)
    .addDefaultAnimation()
    .create()
    .show();

就是這麼直接,就是這麼簡單,一行(如果你的屏幕足夠寬)下來搞定!

再來看一個有用戶交互的場景,帶有確認取消按鈕的,同樣的我們創建一個dialog_test_0.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:background="@color/color_white"
    android:orientation="vertical">

    <TextView
        android:id="@+id/mTitle"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:gravity="center"
        android:text="我是標題"
        android:textColor="@color/colorAccent"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/mContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:lineSpacingExtra="3dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:text="Android自定義View,Android JetPack,常用第三方庫源碼,Android Framework源碼,C/C++/JNI/NDK,MVC/MVP/MVVM/模塊化/組件化/插件化,熱更新熱修復,設計模式,線程間通信,進程間通信"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="10dp"
        android:background="@color/colorE9" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@color/colorE9"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/mCancel"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginRight="1dp"
            android:layout_weight="1"
            android:background="@color/color_white"
            android:gravity="center"
            android:text="取消"
            android:textColor="@color/color_666"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/mConfirm"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="1dp"
            android:layout_weight="1"
            android:background="@color/color_white"
            android:gravity="center"
            android:text="確認"
            android:textColor="@color/colorAccent"
            android:textSize="18sp" />
    </LinearLayout>
</LinearLayout>

然後在Java代碼中調用,這裏考慮到某些場景下需要拿到相關的數據,所以將點擊事件在Dialog中也提供了一份,這樣方便我們進行一些數據處理的操作,所以最後的調用成了這個樣子:

CommonDialog dialog = new CommonDialog.Builder(MainActivity.this)
    .setContentView(R.layout.dialog_test_0)
    .setCancelable(true)
    .fromBottom(true)
    .fullWidth()
    .setText(R.id.mTitle,"Android高級進階")
    .create();
dialog.setOnclickListener(R.id.mConfirm, v -> {
    Toast.makeText(MainActivity.this,"點擊確定了",Toast.LENGTH_SHORT).show();
    dialog.dismiss();
});
dialog.setOnclickListener(R.id.mCancel, v -> {
    Toast.makeText(MainActivity.this,"點擊取消了",Toast.LENGTH_SHORT).show();
    dialog.dismiss();
});
dialog.show();

其實跟上面的也是差不多的,沒有太大的區別,代碼稍作改動即可,最後我會把完整的代碼貼出來供大家參考!

寫到這裏基本上就要和大家說再見了,如有問題,歡迎留言或者私信我進行探討!因爲我本人比較懶,所以沒有新建項目,我直接在把代碼封裝了一個lib_common放到了我之前寫的《Android架構設計之MVC/MVP/MVVM淺析》的源碼中了,有需要的可以下載或者clone!

源碼:https://github.com/JArchie/MVXProject

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章