介紹:
- Android 中實現彈窗的一種方式。
- 分為 v4 包下的和android.app 包下的,我們使用 v4 包下的, android.app 包下的 DialogFragment 在 Android28 版本上已經被標記為棄用了。
- 繼承與 Fragment ,擁有 Fragment 所有的特性。DialogFragment 裡面內嵌了一個 Dialog。
和 Dialog 的區別:
- 相比較 Dialog 來說,DialogFragment 其內嵌了一個 Dialog ,並對它進行一些靈活的管理,並且在 Activity 被異常銷燬後重建的時候,DialogFragment 也會跟著重建,但是單獨使用 Dialog 並不會。而且我們可以在 DialogFragment 的 onSaveInstanceState 方法中儲存一些我們的資料,DialogFragment 跟著 Activity 重建的時候,從 onRestoreInstanceState 中取出資料,恢復頁面顯示。
- Dialog 不適合複雜UI,而且不適合彈窗中有網路請求的邏輯開發。而 DialogFragment 可以當做一個 Fragment 來使用,比較適合做一些複雜的邏輯,網路請求。
基本使用方式
-
建立方式:
- 重寫
onCreateView
方法,自定義佈局。適用於複雜UI場景。 - 重寫
onCreateDialog
方法,自定義Dialog。適用於簡單、傳統彈窗UI。
- 重寫
-
重寫 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);
}
}
複製程式碼
- 重寫 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 包下的類。
- 在 Activity 中的顯示出來:
CustomDialogFragment dialog = CustomDialogFragment.newInstance("我是內容") ;
dialog.show(getSupportFragmentManager(),"dialog");
複製程式碼
- 其他
- 關閉彈窗
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);
}
複製程式碼
-
傳遞資料給宿主 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("點選確認"); } } }); 複製程式碼
-
傳遞資料給宿主 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_FRAME
和 STYLE_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 也都是在這個時候生效的。
所以可以得出的結論是:
- setStyle 要在 onCreateView 之前呼叫。一般是在 onCreate 中呼叫。
- getDialog() 獲取 Dialog ,這個方法在 onCreateView 之前呼叫都是為 null 的。我們可以在 onCreateView 方法中獲取 Dialog 例項。
- 如果是重寫 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() 函式