DialogFragment源碼分析
時間 2020-12-27
標籤
DialogFragment
Dialog
Fragment
目錄介紹
- 1.最簡單的使用方法
- 1.1 官方建議
- 1.2 最簡單的使用方法
- 1.3 DialogFragment做屏幕適配
- 2.源碼分析
- 2.1 DialogFragment繼承Fragment
- 2.2 onCreate(@Nullable Bundle savedInstanceState)源碼分析
- 2.3 setStyle(@DialogStyle int style, @StyleRes int theme)
- 2.4 onActivityCreated(Bundle savedInstanceState)源碼分析
- 2.5 onCreateDialog(Bundle savedInstanceState)源碼分析
- 2.6 重點分析彈窗展示和銷燬源碼
- 3.經典總結
- 4.DialogFragment封裝庫介紹
- 5.常見問題總結
- 5.1 使用中show()方法遇到的IllegalStateException
好消息
1.最簡單的使用方法
1.1 官方建議
- Android比較推薦採用DialogFragment實現對話框,它完全能夠實現Dialog的所有需求,並且還能複用Fragment的生命週期管理,被後臺殺死後,可以恢復重建。
1.2 最簡單的使用方法
- 如下所示:
public class CustomDialogFragment extends DialogFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設置樣式
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.view_fragment_dialog, container, false);
}
public static void showDialog(FragmentActivity activity){
CustomDialogFragment customDialogFragment = new CustomDialogFragment();
customDialogFragment.show(activity.getSupportFragmentManager(),"yc");
}
}
//然後一行代碼調用
CustomDialogFragment.showDialog(this);
- 1.2.1 創建theme主題樣式,並且進行設置
- 設置樣式,以DialogFragment爲例,只需要在onCreate中setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog)即可。
- 注意,CenterDialog中可以設置彈窗的動畫效果。
- 注意一下style常量,這裏只是展示常用的。
STYLE_NORMAL:會顯示一個普通的dialog
STYLE_NO_TITLE:不帶標題的dialog
STYLE_NO_FRAME:無框的dialog
STYLE_NO_INPUT:無法輸入內容的dialog,即不接收輸入的焦點,而且觸摸無效。
- 1.2.2 重寫onCreateView方法創建彈窗
- 1.2.3 創建類的對象,然後調用show(FragmentManager manager, String tag)方法即可創建出彈窗
- 1.2.4 如何去掉標題欄,也許你會問,爲什麼第二種要在super.onActivityCreated(savedInstanceState)之前設置呢。這個是因爲,看了源碼之後才知道onActivityCreated這個方法中,有mDialog.setContentView(view)這一步,說到setContentView是不是很熟悉。沒錯,後面再深度解析這塊源碼思路……
//第一種
//設置樣式時,使用STYLE_NO_TITLE
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog);
//第二種
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Window window = getDialog().getWindow();
if(window!=null){
window.requestFeature(Window.FEATURE_NO_TITLE);
}
super.onActivityCreated(savedInstanceState);
}
2.源碼分析
2.1 DialogFragment繼承Fragment
- DialogFragment是繼承Fragment,具有Fragment的生命週期,本質上說就是Fragment,只是其內部還有一個dialog而已。你既可以當它是Dialog使用,也可以把它作爲Fragment使用。
2.2 onCreate(@Nullable Bundle savedInstanceState)源碼分析
- onCreate這個方法主要是保存一些屬性狀態,比如style樣式,theme注意,是否可以取消,後退棧的ID等等。
- 重點看一下mShowsDialog這個參數,這個參數是Boolean值,mShowsDialog = mContainerId == 0;所以,默認情況下,mContainerId就是0,所以mShowsDialog就是true;而當你在把它當成Fragment使用時,會爲其指定xml佈局中位置,那麼mContainerId也會不爲0,所以mShowsDialog就是false。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mShowsDialog = mContainerId == 0;
if (savedInstanceState != null) {
mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);
mTheme = savedInstanceState.getInt(SAVED_THEME, 0);
mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);
mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);
}
}
- mShowsDialog這個參數的作用
- 然後直接搜索,可以看到這個參數,可以看到mShowsDialog是false,如果不是Dialog,則調用Fragment自身的方法;否則,就先創建一個dialog,然後,根據之前設置的style,通過setupDialog(mDialog, mStyle),對dialog賦值。所以,setStyle這個方法調用,一定要在onCreateView之前。一般來講,都會放到onCreate中調用。
2.3 setStyle(@DialogStyle int style, @StyleRes int theme)源碼分析
2.4 onActivityCreated(Bundle savedInstanceState)源碼分析
- 該方法的作用主要是:當DialogFragment依附的Activity被創建的時候調用,此時fragment的活動窗體被初始化
- 可以看到這個方法,如果是彈窗已經show出來的話,則直接return。然後通過setContentView方法將view創建出來。同時還設置了彈窗是否可以被取消,以及點擊事件等等。
2.5 onCreateDialog(Bundle savedInstanceState)源碼分析
2.6 重點分析彈窗展示和銷燬源碼
2.6.1 show方法
- 第一種:顯示對話框,將片段添加到給定的FragmentManager中。這對於顯式創建事務、使用給定的標記將片段添加到事務並提交它是很方便的。這樣做可以將事務添加到後臺堆棧。當片段被取消時,將執行一個新的事務來從活動中刪除它。
- 第二種:顯示對話框,使用現有事務添加片段,然後提交事務。
- 共同點:這兩種顯示方式都是通過tag的方式將DialogFragment以事務的形式提交,不同的是第二種方式是採用已經創建過的transaction,並且他返回了一個int類型的數值mBackStackId,mBackStackId是幹什麼用的呢?
- mBackStackId:是做爲將DialogFragment壓入回退棧的編號,初始值是-1,如果DialogFragment是用第二種方式show的話,他將被transaction默認壓入回退棧,mBackStackId=transaction.commit(),此時她的回退棧編號大於0,她的具體使用在dismissInternal方法中後面會具體介紹
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
public int show(FragmentTransaction transaction, String tag) {
mDismissed = false;
mShownByMe = true;
transaction.add(this, tag);
mViewDestroyed = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
2.6.2 dismiss()銷燬方法
- 在源碼中可以看到這兩個方法都調用了dismissInternal(boolean)方法,不同的是傳入的boolean值一個爲false一個爲true,那麼究竟這個boolean起到什麼作用呢?
- 在dismissInternal這個方法中,主要操作了:如果對話框已經不可見就跳出方法體;設置對話框消失,然後將對話框屬性設置不可見;如果DialogFragment中的Dialog對象不爲空,就讓其內的對話框消失;然後銷燬View;對於回退棧編號mBackStackId,在前面show方法源碼分析時提到這個呢!主要是用show(FragmentTransaction transaction, String tag)這個方法來壓棧的,所以要取消對話框需要在這裏面判斷,已壓棧的要彈出回退棧,這個回退棧是由Activity來管理的,如果show(FragmentManager manager, String tag)方式的話則不需要彈棧,只需要在FragmentTransaction中將其remove掉即可。
- 簡單總結就是:調用dialog的dismiss方法後,如果自己在後退棧中,就將自己從後退棧中移除掉;如果自己不在後退棧中,就將自己從FragmentManager中移除掉。
2.6.3 dialog顯示與隱藏
3.經典總結
- DialogFragment是繼承Fragment,具有Fragment的生命週期,本質上說就是Fragment,只是其內部還有一個dialog而已。你既可以當它是Dialog使用,也可以把它作爲Fragment使用。
- onCreateView可以加載客戶化更高的對話框,onCreateDialog加載系統AlertDialog類型對話框比較合適。
- DialogFragmnet對話框橫屏時對話框不會關閉,因爲DailogFragment有Fragment屬性,會在屏幕發生變化時重新創建DialogFragment。
- setStyle的調用點,要放在onCreateView前,一般是放在onCreat方法中執行,否則,設置的style和theme將不起作用!setStyle中,style的參數是不可以相互一起使用的,只能用一個,如果還不滿足你使用,可以通過設置theme來滿足。
4.DialogFragment封裝庫介紹
- 自定義對話框,其中包括:自定義Toast,採用builder模式,支持設置吐司多個屬性;自定義dialog控件,仿IOS底部彈窗;自定義DialogFragment彈窗,支持自定義佈局,也支持填充recyclerView佈局;自定義PopupWindow彈窗,輕量級,還有自定義Snackbar等等;還有自定義loading加載窗,簡單便用。這裏只是展示dialogFragment用法!
- 第一種:鏈式編程,如下所示
BottomDialogFragment.create(getSupportFragmentManager())
.setViewListener(new BottomDialogFragment.ViewListener() {
@Override
public void bindView(View v) {
}
})
.setLayoutRes(R.layout.dialog_bottom_layout_list)
.setDimAmount(0.5f)
.setTag("BottomDialog")
.setCancelOutside(true)
.setHeight(getScreenHeight() / 2)
.show();
- 第二種:直接繼承,可以高度定製自己想要的彈窗
public class ADialog extends BaseDialogFragment {
@Override
protected boolean isCancel() {
return false;
}
@Override
public int getLayoutRes() {
return 0;
}
@Override
public void bindView(View v) {
}
}
5.常見問題總結
5.1 使用中show()方法遇到的IllegalStateException
- 報錯日誌如下:
lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1493)
- 出現該問題的原因
- Activity 調用了onSaveInstanceState()以後有觸發了dialog的顯示,dialog.show()方法裏邊用的是commit()而不是commitAllowingStateLoss()
- 追蹤報錯日誌的來源
- 於是,我挺好奇,show方法中只有兩個參數,決定從getSupportFragmentManager()方法分析.FragmentManager是抽象類,我這裏主要是看FragmentManagerImpl實現類代碼
//第一步:
public FragmentManager getSupportFragmentManager() {
return mFragments.getSupportFragmentManager();
}
//第二步:
public FragmentManager getSupportFragmentManager() {
return mHost.getFragmentManagerImpl();
}
//第三步:
FragmentManagerImpl getFragmentManagerImpl() {
return mFragmentManager;
}
//第四步:看beginTransaction()方法
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
//第五步:看BackStackRecord類中看commit方法
@Override
public int commit() {
return commitInternal(false);
}
@Override
public int commitAllowingStateLoss() {
return commitInternal(true);
}
//第六步:可以看到這倆函數的區別就是commitInternal()方法中參數一個爲true,一個爲false
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
//第七步:再追蹤到enqueueAction(this,allowStateLoss)
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
//第八步:checkStateLoss()方法,這裏可以看到拋出的錯誤日誌呢
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
關於其他內容介紹
01.關於博客彙總鏈接
02.關於我的博客