一步一步使用 DialogFragment 封裝鏈式呼叫 Dialog

smartsean發表於2018-01-30

前言

日常開發中,Dialog 是一個每個 app 所必備的。


2018-01-31更新

最後封裝好的 BaseDialogFragment 已經新增到我的快速開發 lib 包中。

可以通過:implementation cn.smartsean:lib:0.0.7 快速引入,

也可以去 AndroidCode 檢視示例原始碼。


通常來說,每個 app 的Dialog 的樣式一般都是統一風格的,比如說有:

  • 確認、取消的 Dialog
  • 提示性的 Dialog
  • 列表選擇的 Dialog
  • 版本更新的 Dialog
  • 帶輸入框的 Dialog

如果每個都要單獨寫,就顯得有點浪費了,一般情況下,我們都需要進行封裝,便於使用和閱讀。

那為什麼要使用 DialogFragment 呢?

使用 DialogFragment 來管理對話方塊,當旋轉螢幕和按下後退鍵時可以更好的管理其生命週期,它和 Fragment 有著基本一致的生命週期。

並且 DialogFragment 也允許開發者把 Dialog 作為內嵌的元件進行重用,類似 Fragment (可以在大螢幕和小螢幕顯示出不同的效果)

那麼接下來我們就一步一步的來封裝出一個便於我們使用的 DialogFragment。

還是先看下效果圖吧,可能有點不是很好看,畢竟沒有 ui,哈哈

效果圖

一、構建 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 ,然後在裡面新建兩個檔案: 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>
複製程式碼

我們需要的基本屬性已經好了,接下來就是如何通過構建者模式來賦值了。

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;
        }
    }
}
複製程式碼

二、如何方便的構建 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 正常顯示需要的值

在通常的確認、取消選擇框中,我們需要傳入的值有什麼呢?

來看下具體的展示:

一步一步使用 DialogFragment 封裝鏈式呼叫 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,不用寫那麼多的重複程式碼,省下的時間可以讓我們做很多事情。

如果你有更好的想法,歡迎提出來~~~

相關文章