一步一步使用 DialogFragment 封裝鏈式調用 Dialog

前言

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


2018-01-31更新

最後封裝好的 BaseDialogFragment 已經添加到個人快速開發 lib 包中。android

能夠經過:implementation cn.smartsean:lib:0.0.7 快速引入,git

也能夠去 AndroidCode 查看示例源碼。github


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

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

若是每一個都要單獨寫,就顯得有點浪費了,通常狀況下,咱們都須要進行封裝,便於使用和閱讀。app

那爲何要使用 DialogFragment 呢?ide

使用 DialogFragment 來管理對話框,當旋轉屏幕和按下後退鍵時能夠更好的管理其生命週期,它和 Fragment 有着基本一致的生命週期。動畫

而且 DialogFragment 也容許開發者把 Dialog 做爲內嵌的組件進行重用,相似 Fragment (能夠在大屏幕和小屏幕顯示出不一樣的效果)ui

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

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

效果圖

1、構建 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 ,而後在裏面新建兩個文件: 一、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>
複製代碼

二、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;
        }
    }
}
複製代碼

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

在一般的確認、取消選擇框中,咱們須要傳入的值有什麼呢?

來看下具體的展現:

  • 標題
  • 內容
  • 取消的提示文字
  • 肯定的提示文字

這裏咱們定義四個 靜態字符換常量:

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,不用寫那麼多的重複代碼,省下的時間可讓咱們作不少事情。

若是你有更好的想法,歡迎提出來~~~

相關文章
相關標籤/搜索