Dialogment詳解

StarkSong發表於2019-03-26

介紹:

  1. Android 中實現彈窗的一種方式。
  2. 分為 v4 包下的和android.app 包下的,我們使用 v4 包下的, android.app 包下的 DialogFragment 在 Android28 版本上已經被標記為棄用了。
  3. 繼承與 Fragment ,擁有 Fragment 所有的特性。DialogFragment 裡面內嵌了一個 Dialog。

和 Dialog 的區別:

  • 相比較 Dialog 來說,DialogFragment 其內嵌了一個 Dialog ,並對它進行一些靈活的管理,並且在 Activity 被異常銷燬後重建的時候,DialogFragment 也會跟著重建,但是單獨使用 Dialog 並不會。而且我們可以在 DialogFragment 的 onSaveInstanceState 方法中儲存一些我們的資料,DialogFragment 跟著 Activity 重建的時候,從 onRestoreInstanceState 中取出資料,恢復頁面顯示。
  • Dialog 不適合複雜UI,而且不適合彈窗中有網路請求的邏輯開發。而 DialogFragment 可以當做一個 Fragment 來使用,比較適合做一些複雜的邏輯,網路請求。

基本使用方式

  1. 建立方式:

    • 重寫 onCreateView 方法,自定義佈局。適用於複雜UI場景。
    • 重寫 onCreateDialog 方法,自定義Dialog。適用於簡單、傳統彈窗UI。
  2. 重寫 onCreateView 方法:

public class CustomDialogFragment extends DialogFragment {
    private static final String TAG = "CustomDialogFragment";
    private TextView mTvDialogTitle;
    private TextView mTvDialogContent;
    private Button mBtnCancel;
    private Button mBtnConfirm;
    private String content;
    
    public CustomDialogFragment() {
        /*每一個繼承了 Fragment 的類都必須有一個空參的構造方法,這樣當 Activity 被恢復狀態時 Fragment 能夠被例項化。
        Google強烈建議我們不要使用構造方法進行傳參,因為 Fragment 被例項化的時候,這些帶參建構函式不會被呼叫。如果要
        要傳遞引數,可以使用 setArguments(bundle) 方式來傳參。*/
    }
    
    static CustomDialogFragment newInstance(String content) {
        CustomDialogFragment customDialogFragment = new CustomDialogFragment();
        Bundle bundle = new Bundle();
        bundle.putString("content", content);
        customDialogFragment.setArguments(bundle);
        return customDialogFragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle bundle = getArguments();
        if (bundle != null) {
            content = bundle.getString("content");
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    //載入佈局
        View view = inflater.inflate(R.layout.dialog_coustom, container);
        initView(view);
        return view;
    }
    //初始化View
    private void initView(View view) {
        mTvDialogTitle = view.findViewById(R.id.tv_dialogTitle);
        mTvDialogContent = view.findViewById(R.id.tv_dialogContent);
        mBtnCancel = view.findViewById(R.id.btn_cancel);
        mBtnConfirm = view.findViewById(R.id.btn_confirm);
        mBtnCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        mBtnConfirm.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        mTvDialogContent.setText(content);
    }
}

複製程式碼
  1. 重寫 onCreateDialog 方法:
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
   AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
   builder.setTitle("我是標題");
   builder.setMessage(content);
   builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            //處理點選事件
        }
    });
    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            //處理點選事件
        }
    });
    return builder.create();
}
複製程式碼

注意: AlertDialog 分為 v7 包下的和 android.app 包下的, android.app 包下的在 Android 5.0 以前版本顯示為老樣式,5.0 以後顯示新的 MD 新風格,為了相容老版本統一顯示最新樣式,使用 v7 包下的類。

  1. 在 Activity 中的顯示出來:
CustomDialogFragment  dialog = CustomDialogFragment.newInstance("我是內容") ;
dialog.show(getSupportFragmentManager(),"dialog");
複製程式碼
  1. 其他
  • 關閉彈窗 customDialogFragment.dismiss();
  • 去掉標題 getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE)

自定義寬高樣式

自定義寬高

我們在使用 onCreateView 方式建立 DialogFragment 的時候,發現我們在 xml 根佈局中設定的寬高並不起作用。這個時候我們可以自己設定 Dialog 所在 Window 的寬高來設定彈窗寬高大小。

具體兩種方法:1. 直接指定 window 的寬高。2.在 xml 中設定具體寬高(需要在根佈局中再巢狀一層佈局)。

  • 直接指定 window 的寬高
//CustomDialogFragment 類
@Override
public void onStart() {
    super.onStart();
    Window window = getDialog().getWindow();
    if (window != null) {
        //設定 window 的背景色為透明色.
        //如果通過 window 設定寬高時,想要設定寬為屏寬,就必須呼叫下面這行程式碼。
        window.setBackgroundDrawableResource(R.color.transparent);
        WindowManager.LayoutParams attributes = window.getAttributes();
        //在這裡我們可以設定 DialogFragment 彈窗的位置
        attributes.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
        //我們可以在這裡指定 window的寬高
        attributes.width = 1000;
        attributes.height = 1000;
        //設定 DialogFragment 的進出動畫
        attributes.windowAnimations = R.style.DialogAnimation;
        window.setAttributes(attributes);
    }
}
複製程式碼

注: 如果通過 window 設定彈窗寬高,要注意 attributes.width = ViewGroup.LayoutParams.MATCH_PARENT 來設定寬為屏寬時,則必須設定 window.setBackgroundDrawableResource(R.color.transparent)

  • 在 xml 中設定寬高
@Override
    public void onStart() {
        super.onStart();
        Window window = getDialog().getWindow();
        if (window != null) {
            //設定 window 的背景色為透明色.
            window.setBackgroundDrawableResource(R.color.transparent);
            WindowManager.LayoutParams attributes = window.getAttributes();
            //在這裡我們可以設定 DialogFragment 彈窗的位置
            attributes.gravity = Gravity.BOTTOM;
            
            /*為什麼這裡還要設定 window 的寬高呢?
            因為如果 xml 裡面的寬高為 match_parent 的時候,window 的寬高也必須是 MATCH_PARENT,否則無法生效!*/
            attributes.width = ViewGroup.LayoutParams.MATCH_PARENT;
            attributes.height = ViewGroup.LayoutParams.WRAP_CONTENT;
            
            //設定 DialogFragment 的進出動畫
            attributes.windowAnimations = R.style.DialogAnimation;
            window.setAttributes(attributes);

        }
    }
複製程式碼
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!--通過 xml 指定寬高的時候,要巢狀一層佈局-->
    <!--我們在這裡設定寬高為 match_parent 屬性的時候, 
    也必須把 window 的寬高設定為 MATCH_PARENT ,否則無法生效!-->
    <android.support.constraint.ConstraintLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary">

        <TextView
            android:id="@+id/tv_dialogTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:text="我是標題"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_dialogContent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:background="@color/colorPrimary"
            android:text="我是內容"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_dialogTitle" />

        <Button
            android:id="@+id/btn_cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:layout_marginBottom="100dp"
            android:text="取消"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/btn_confirm"
            app:layout_constraintTop_toBottomOf="@+id/tv_dialogContent" />

        <Button
            android:id="@+id/btn_confirm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="確定"
            app:layout_constraintBottom_toBottomOf="@id/btn_cancel"
            app:layout_constraintLeft_toRightOf="@+id/btn_cancel"
            app:layout_constraintRight_toRightOf="parent" />
    </android.support.constraint.ConstraintLayout>
</FrameLayout>
複製程式碼

設定樣式

我們通常通過 style(int style,int theme) 方法來設定Dialog的樣式,其中 theme 需要在 styles.xml 檔案中自定義一個樣式,如果不設定樣式,直接傳 0。這裡我們主要說 style 。style 型別總共有四種。

STYLE_NORMAL 基本的*普通對話方塊。預設型別。

STYLE_NO_TITLE 對話方塊無標題。

STYLE_NO_FRAME 對話方塊無邊框,無標題。

STYLE_NO_INPUT 禁用對話方塊的所有輸入,使用者無法觸控它,其視窗將不會接收輸入焦點。

注意:1、我們在呼叫 style(int style,int theme) 的時候,需要注意的是,這個方法必須在 onCreateView 之前呼叫,否則是無效的。我們一般在 onCreate 中呼叫。 2、 如果我們是通過重寫 OnCreateDialog 方法建立 DialogFragment,我們設定的 theme 主題是不會生效的,需要在 onCreateDialog 方法中重新給 Dialog 設定。 setStyle() 呼叫時機-原始碼分析

和頁面之間傳遞資料

在我們展示彈窗的時候,可以使用 setArguments(bundle) 方法進行傳遞引數,也可以使用 FragmentManager 根據 tag 獲取 DialogFragment 例項實現通訊。getFragmentManager().findFragmentByTag(tag)

這裡如何將 DialogFragment 的資料回傳呢?這裡一般分為兩種情況:1. DialogFragment 傳遞資料給 Activity 2. DialogFragment 傳遞資料給 Fragment 。

首先定義一個介面

public interface OnDialogClickListener {
    void cancel(String msg);
    void confirm(String msg);
}
複製程式碼
  1. 傳遞資料給宿主 Activity

    Activity 實現 OnDialogClickListener 介面

    public class MainActivity extends BaseActivity implements OnDialogClickListener {
    
        private static final String TAG = "MainActivity---";
    
       @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
            findViewById(R.id.btn_showDialog).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //彈出彈窗
                    CustomDialogFragment dialog = CustomDialogFragment.newInstance("我是內容");
                    dialog.show(getSupportFragmentManager(),"dialog");
                }
            });
        }
    
        @Override
        public void cancel(String msg) {
            Log.i(TAG, "cancel: " + msg);
        }
    
        @Override
        public void confirm(String msg) {
            Log.i(TAG, "confirm: " + msg);
        }
    }
    
    複製程式碼

    然後在 DialogFragment 中進行回撥。

    mBtnCancel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (getActivity() instanceof OnDialogClickListener) {
                //傳遞訊息給 Activity
                ((OnDialogClickListener) getActivity()).cancel("點選取消");
            }
        }
    });
    mBtnConfirm.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (getActivity() instanceof OnDialogClickListener) {
                //傳遞訊息給 Activity 
                ((OnDialogClickListener) getActivity()).confirm("點選確認");
            }
        }
    });
    複製程式碼
  2. 傳遞資料給宿主 Fragment

    Activity 實現 OnDialogClickListener 介面

    public class MyFragment extends Fragment implements OnDialogClickListener {
        private String TAG = "Fragment=== ";
    
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_my, container, false);
            view.findViewById(R.id.btn_showDialog).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //彈出彈窗
                    CustomDialogFragment dialog = CustomDialogFragment.newInstance("我是內容");
                    dialog.show(getChildFragmentManager(),"dialog");
                }
            });
            return view;
        }
    
        @Override
        public void cancel(String msg) {
            Log.i(TAG, "cancel: " + msg);
        }
    
        @Override
        public void confirm(String msg) {
            Log.i(TAG, "confirm: " + msg);
        }
    }
    複製程式碼

    然後在 DialogFragment 中進行回撥。

    mBtnCancel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (getParentFragment() instanceof OnDialogClickListener) {
                //傳遞訊息給 Fragment 
                ((OnDialogClickListener) getParentFragment()).cancel("點選取消");
            }
        }
            });
    mBtnConfirm.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (getParentFragment() instanceof OnDialogClickListener) {
                //傳遞訊息給 Fragment 
                ((OnDialogClickListener) getParentFragment()).cancel("點選確認");
            }
        }
    });
    複製程式碼

    也可以使用 FragmentManager 通過 tag 獲取其他 Fragment 的例項,來和其他 Fragment 進行通訊。

    如果要進行其他複雜場景的資料傳遞,可以使用 廣播、EventBus 等進行傳遞。

原始碼分析

style、theme 的生效時機。

DialogFragment 中 setStyle 的原始碼如下

//DialogFragment 類:
public void setStyle(@DialogStyle int style, @StyleRes int theme) {
    mStyle = style;
    if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
        mTheme = android.R.style.Theme_Panel;
    }
    if (theme != 0) {
        mTheme = theme;
    }
}

複製程式碼

可以看出在 style 為 STYLE_NO_FRAMESTYLE_NO_INPUT 的時候, 如果 mTheme 為 0,就設定 mTheme 為 android.R.style.Theme_Panel;

我們在 DialogFragment 中找到 mStyle 起作用的地方。

@Override
public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
    if (!mShowsDialog) {
        return super.onGetLayoutInflater(savedInstanceState);
    }
    // 在這裡呼叫了 onCreateDialog 方法,建立一個 Dialog.
    mDialog = onCreateDialog (savedInstanceState);

    if (mDialog != null) {
        //在這裡呼叫了 mStyle
        setupDialog(mDialog, mStyle);

        return (LayoutInflater) mDialog.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }
    return (LayoutInflater) mHost.getContext().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
}
//----------------------------------------------------
/*從下面的方法可以看出,STYLE_NO_INPUT、STYLE_NO_FRAME、STYLE_NO_TITLE 
這三種型別的 Style 都去掉了 Dialog 的標題。*/
/** @hide */
@RestrictTo(LIBRARY_GROUP)
public void setupDialog(Dialog dialog, int style) {
    switch (style) {
        case STYLE_NO_INPUT:
            dialog.getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                            WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            // fall through...
        case STYLE_NO_FRAME:
        case STYLE_NO_TITLE:
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    }
}
//----------------------------------------------------------
/*DialogFragment 本身在建立 dialog 的時候,
呼叫了 getTheme 方法獲取了當前設定的 mTheme,設定給了 Dialog 。
所以如果我們重寫覆蓋了父類的 onCreateDialog 方法,mTheme 需要我們重新手動設定給 Dialog */
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new Dialog(getActivity(), getTheme());
}
複製程式碼

我們找到 onGetLayoutInflater 方法的呼叫地方

//Fragment 類
@NonNull
LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) {
    //這裡呼叫了 onGetLayoutInflater 方法
    LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState);
    mLayoutInflater = layoutInflater;
    return mLayoutInflater;
}
複製程式碼

然後,繼續找到 performGetLayoutInflater 方法的呼叫地方,發現在 FragmentManager 中有這麼一行程式碼:

//這裡呼叫了 onGetLayoutInflater 方法,這裡的 f 是指具體的 Fragment 例項
f.mView = f.performCreateView(f.performGetLayoutInflater(
                    f.mSavedFragmentState), null, f.mSavedFragmentState);
複製程式碼

那麼,f.performCreateView 做了什麼呢?

//Fragment 類
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
    }
    mPerformedCreateView = true;
    //開始呼叫 Fragment 的 onCreateView 方法.
    return onCreateView(inflater, container, savedInstanceState);
}
複製程式碼

看到上面,就完全清晰明瞭了,DialogFragment 中的 onGetLayoutInflater 方法是在 準備呼叫 onCreateView 方法的時候呼叫的。 DialogFragment 中的 Dialog 是在執行 onGetLayoutInflater 方法中建立的。並且,mStyle、mTheme 也都是在這個時候生效的。

所以可以得出的結論是:

  1. setStyle 要在 onCreateView 之前呼叫。一般是在 onCreate 中呼叫。
  2. getDialog() 獲取 Dialog ,這個方法在 onCreateView 之前呼叫都是為 null 的。我們可以在 onCreateView 方法中獲取 Dialog 例項。
  3. 如果是重寫 onCreateDialog 方法建立 DialogFragment ,設定的 mTheme 是不起作用的,需要我們在 onCreateDialog 方法中手動設定給 Dialog 。

setCancelable 不起作用-原因分析

setCancelable 點選彈窗外部消失,並且遮蔽返回鍵。

setCanceledOnTouchOutside 點選彈窗外部不消失,不遮蔽返回鍵。

一般我們在 DialogFragment 中呼叫這兩個方法的時候,會在 onCreateView 或 onCreateDialog 中呼叫:

dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
複製程式碼

但是,測試的時候你會發現實際結果並未達到期望。 其實,DialogFragment 本身也有一個 setCancelable 方法,如果想實現點選外部不消失、遮蔽返回按鈕效果,我們要在 onCreateView 和 onCreateDialog 中呼叫 CustomDialogFragment.this.setCancelable (false) 方法。而不是 Dialog 的 setCancelable 方法。下面是具體分析:

首先來看 Dialog 的 setCancelable 和 setCanceledOnTouchOutside 方法。

Dialog 類

public void setCancelable(boolean flag) {
    mCancelable = flag;
    updateWindowForCancelable();
}
---------------------------------------------------
public void setCanceledOnTouchOutside(boolean cancel) {
    /*如果設定了點選彈窗外部可消失( cancel 為 true ),首先會檢視是否設定了 setCancelable(false),
    如果設定了,就取消這個設定。*/
    if (cancel && !mCancelable) {
        mCancelable = true;
        updateWindowForCancelable();
    }
    mWindow.setCloseOnTouchOutside(cancel);
}
複製程式碼

然後是 DialogFragment 中 setCancelable 的原始碼:

DialogFragment 類
// mCancelable 的預設值為ture。
boolean mCancelable = true;

public void setCancelable(boolean cancelable) {
    //將是否能夠取消通過 mCancelable 標記起來
    mCancelable = cancelable;
    //如果 mDialog 已經建立了,就直接設定設定給 mDialog 。
    if (mDialog != null) mDialog.setCancelable(cancelable);
}
複製程式碼

這個時候可能有疑問了,DialogFragment 的 setCancelable 方法內部也是呼叫了 Dialog 的 setCancelable 方法,為什麼這個方法就可以起作用了呢?原因就在於 mCancelable = cancelable; 這行程式碼。

我們通過尋找 mCancelable 呼叫地方,發下真正的原因所在:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    //-----程式碼省略----
    //真正原因就在這裡,在 onActivityCreated 方法中又呼叫了一次 mDialog.setCancelable 方法
    mDialog.setCancelable(mCancelable);
    mDialog.setOnCancelListener(this);
    mDialog.setOnDismissListener(this);
    //-----程式碼省略----
}
複製程式碼

因為 mCancelable 這個值預設是 true ,我們在 onCreateView 和 onCreateDialog 中設定 dialog.setCancelable(false);後,並沒有將 mCancelable 的值改變為false, DialogFragment 在走到 onActivityCreated 生命週期時,又呼叫了 mDialog.setCancelable(mCancelable); 覆蓋了我們之前的設定,所以我們之前的設定沒有起作用。而 DialogFragment 本身的 setCancelable 方法內部改變了 mCancelable 值,所以達到了我們的效果。

另,推薦一個好文選擇正確的 Fragment#commitXXX() 函式

參考