一. DialogFragment源碼分析。
因為是Fragment旭寿,我們先從onCreate生命周期入手躯保。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 一般這樣設(shè)置樣式
setStyle(.....);
}
先來DialogFragment中有個style方法
public void setStyle(int style, @StyleRes int theme) {
this.mStyle = style;
if (this.mStyle == 2 || this.mStyle == 3) {
this.mTheme = 16973913;
}
if (theme != 0) {
this.mTheme = theme;
}
}
可以看出這里并沒有做什么操做,只是把傳進來的style和theme存到全局變量菌瘫。
因為fragment不會無緣無故去走他的生命周期方法,所以入口方法就是show()方法布卡。
public void show(FragmentManager manager, String tag) {
this.mDismissed = false;
this.mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
可以看出這里先設(shè)置了兩個屬性mDismissed和mShownByMe 雨让。很可惜的是源碼中并沒有給這兩個屬性添加注釋,那就只能猜了忿等,從命名上猜和編程習(xí)慣猜mDismissed是記錄這個Dialog是否dismiss栖忠,mShownByMe 是記錄是否是用戶調(diào)起的show方法。
然后用manager.beginTransaction()拿到FragmentTransaction贸街,抽象理解就是當(dāng)前這個外層頁面的FragmentManager的事物庵寞,然后把當(dāng)前fragment添加到外層界面的FragmentManager,commit就是提交匾浪。
加了會怎么樣皇帮?那還用說,我們先看看Activity動態(tài)展示Fragment的代碼蛋辈,我隨便去網(wǎng)上copy一段代碼
getSupportFragmentManager()
.beginTransaction()
.add(布局的ID , fragment)
.commit();
就都是這樣的操作属拾,所以你說添加了會怎樣,當(dāng)然是走Fragment的生命周期啊冷溶。按照Fragment的生命周期鉤子來走渐白,按順序看看DialogFragment有重寫哪些生命周期方法。
public void onAttach(Context context) {
super.onAttach(context);
if (!this.mShownByMe) {
this.mDismissed = false;
}
}
這里做了一個判斷逞频,應(yīng)該是為了安全性考慮纯衍,如果這個Dialog不是由我們調(diào)用show方法展示的話,還記得在show方法中有設(shè)置this.mDismissed = false;嗎 苗胀, 如果不是調(diào)用show方法襟诸,而恰巧這個Fragment的生命周期又被調(diào)用了。所以這里為了安全考慮補上this.mDismissed = false基协。
然后調(diào)用onCreate
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mShowsDialog = this.mContainerId == 0;
if (savedInstanceState != null) {
this.mStyle = savedInstanceState.getInt("android:style", 0);
this.mTheme = savedInstanceState.getInt("android:theme", 0);
this.mCancelable = savedInstanceState.getBoolean("android:cancelable", true);
this.mShowsDialog = savedInstanceState.getBoolean("android:showsDialog", this.mShowsDialog);
this.mBackStackId = savedInstanceState.getInt("android:backStackId", -1);
}
}
this.mShowsDialog = this.mContainerId == 0這個容器ID mContainerId 我也不太清楚是什么歌亲,先跳過。
下面 if (savedInstanceState != null) {......} 是就恢復(fù)數(shù)據(jù)的操作澜驮。可以看到官網(wǎng)在onCreate中的恢復(fù)數(shù)據(jù)的寫法是怎么寫的陷揪,十分建議學(xué)會使用這種做法,能讓代碼更為安全。
相應(yīng)的可以先來直接看看保存數(shù)據(jù)的做法
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (this.mDialog != null) {
Bundle dialogState = this.mDialog.onSaveInstanceState();
if (dialogState != null) {
outState.putBundle("android:savedDialogState", dialogState);
}
}
if (this.mStyle != 0) {
outState.putInt("android:style", this.mStyle);
}
if (this.mTheme != 0) {
outState.putInt("android:theme", this.mTheme);
}
if (!this.mCancelable) {
outState.putBoolean("android:cancelable", this.mCancelable);
}
if (!this.mShowsDialog) {
outState.putBoolean("android:showsDialog", this.mShowsDialog);
}
if (this.mBackStackId != -1) {
outState.putInt("android:backStackId", this.mBackStackId);
}
}
可以看到保存fragment的數(shù)據(jù)之前悍缠,先保存dialog的數(shù)據(jù)卦绣。
我們繼續(xù)來看生命周期onActivityCreated
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (this.mShowsDialog) {
View view = this.getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException("DialogFragment can not be attached to a container view");
}
this.mDialog.setContentView(view);
}
Activity activity = this.getActivity();
if (activity != null) {
this.mDialog.setOwnerActivity(activity);
}
this.mDialog.setCancelable(this.mCancelable);
this.mDialog.setOnCancelListener(this);
this.mDialog.setOnDismissListener(this);
if (savedInstanceState != null) {
Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
if (dialogState != null) {
this.mDialog.onRestoreInstanceState(dialogState);
}
}
}
}
可以看出這里就是把view設(shè)置給Dialog,而這個view就是我們在onCreateView方法中所返回的view飞蚓。所以先前需要判斷view.getParent()滤港,因為一個子view不能同時擁有兩個父view。
this.mDialog.setOwnerActivity(activity);這個好像是把activity傳給Dialog玷坠,因為Dialog里面肯定要用到activity的地方蜗搔。后面的代碼就是設(shè)置能關(guān)閉,設(shè)置Cancel和Dismiss時的監(jiān)聽八堡,還有獲取savedInstanceState保存的數(shù)據(jù)。
這些操作寫在這里聘芜,我估計是因為此時activity的創(chuàng)建才剛走完兄渺。
然后是onStart
public void onStart() {
super.onStart();
if (this.mDialog != null) {
this.mViewDestroyed = false;
this.mDialog.show();
}
}
記錄mViewDestroyed 為false , 然后展示Dialog汰现。
可以看出在onStart生命周期中才展示Dialog挂谍,此時頁面已經(jīng)展示出來。
看看彈框頁面消失的操作
public void onStop() {
super.onStop();
if (this.mDialog != null) {
this.mDialog.hide();
}
}
Fragment隱藏時把Dialog也隱藏瞎饲,相當(dāng)于把他兩的狀態(tài)都綁在一起口叙。
public void onDestroyView() {
super.onDestroyView();
if (this.mDialog != null) {
this.mViewDestroyed = true;
this.mDialog.dismiss();
this.mDialog = null;
}
}
mViewDestroyed = true, 記錄當(dāng)前頁面已經(jīng)關(guān)閉嗅战,此時dialog也跟著dismiss妄田,并且this.mDialog = null;釋放掉內(nèi)存(GC過后再不到這個引用,會來釋放掉)驮捍。這里雖然沒什么難理解的疟呐,但是這4行代碼寫得非常好,值得學(xué)習(xí)东且。
最后
public void onDetach() {
super.onDetach();
if (!this.mShownByMe && !this.mDismissed) {
this.mDismissed = true;
}
}
這里和dismissInternal方法我感覺是有一種是做了多線程的感覺启具,所以加了雙向判斷,看起來感覺有點繞珊泳。dismissInternal方法是Dialog關(guān)閉時調(diào)用的鲁冯。
void dismissInternal(boolean allowStateLoss) {
if (!this.mDismissed) {
this.mDismissed = true;
this.mShownByMe = false;
if (this.mDialog != null) {
this.mDialog.dismiss();
}
this.mViewDestroyed = true;
if (this.mBackStackId >= 0) {
this.getFragmentManager().popBackStack(this.mBackStackId, 1);
this.mBackStackId = -1;
} else {
FragmentTransaction ft = this.getFragmentManager().beginTransaction();
ft.remove(this);
if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
}
}
}
}
如果生命周期onDetach先執(zhí)行,mShownByMe 還是為true色查,所以onDetach中的判斷不會走薯演,之后還會走dismissInternal。如果dismissInternal先執(zhí)行综慎,mDismissed為false涣仿,走判斷里的方法,
mDismissed = true
his.mShownByMe = false
if (this.mDialog != null) {
this.mDialog.dismiss();
}
表示沒走onDestroyView方法,所以這里再走一次this.mDialog.dismiss();
mViewDestroyed = true
mBackStackId 回退狀態(tài)好港,一般流程會等于-1愉镰,這時讓外層的FragmentManager移除當(dāng)前Fragment。
我覺得這里就是處理一個多線程的結(jié)果钧汹,調(diào)用的順序可以是以下幾種情況
(1)onDestroyView - > onDetach -> dismissInternal
(2)onDestroyView - > dismissInternal-> onDetach
(3)dismissInternal- > onDestroyView -> onDetach
所以可以看出丈探,最主要的方法是dismissInternal,它一定會調(diào)用拔莱,哪怕是在Fragment銷毀之后碗降。所以這里我有個問題:fragment銷毀了,那this就有可能被釋放為空吧塘秦,那 ft.remove(this);這個操作不是有可能報空指針嗎讼渊?這個要看FragmentManager的源碼之后才知道,也許它在里面有判空操作尊剔。
他的這個多線程的邏輯應(yīng)該是挺穩(wěn)定的爪幻,就是看著會很繞,如果先調(diào)生命周期再調(diào)dismissInternal基本是沒問題须误,如果先調(diào)dismissInternal再調(diào)onDestroyView 的話挨稿,onDestroyView里面的this.mDialog.dismiss();還是會走一遍,只不過mViewDestroyed已經(jīng)為true京痢,不會再走dismissInternal里面的邏輯奶甘。
而且Dialog內(nèi)部的dismiss方法里的邏輯也有判斷,防止多次調(diào)用祭椰。
所以可以看出java的多線程是一個很麻煩的家伙臭家,為了保證調(diào)用順序沒問題,需要加一大堆判斷吭产,而且久了可能連自己也看得懵侣监,不好意思扯遠了。
最后還有一個方法沒講到—— onCreateDialog
他是在調(diào)用fragment的onGetLayoutInflater方法時調(diào)用的臣淤,onGetLayoutInflater方法是在fragment中的getLayoutInflater()方法調(diào)用后調(diào)用的橄霉,而這個getLayoutInflater()我暫時也找不到在哪里調(diào)用,但是我們可以通過打印的方式來判斷onCreateDialog再哪個生命周期之間調(diào)用邑蒋。
可以看到姓蜂,是在調(diào)onCreateView之前調(diào)用的。
所以說DialogFragment里面的Dialog在onCreate之后創(chuàng)建医吊,在onStart中展示钱慢,在onDestroyView中關(guān)閉。
也能看出卿堂,F(xiàn)ragment中其實并沒有做什么復(fù)雜的邏輯操作束莫,都是在處理生命周期懒棉、保存數(shù)據(jù)這些操作,可以看出這很符合谷歌說的建議用DialogFragment代替Dialog的概念览绿,確實是加了一層用于管理的Fragment策严。所以最核心的功能還是Dialog的功能,最核心的代碼還是Dialog的代碼饿敲。
二.Dialog源碼簡單分析
相比DialogFragment妻导,這里我不會像上面一樣那么詳細的分析,我只會分析某些方法怀各。
從DialogFragment可以在知道在onCreate之后創(chuàng)建Dialog
@NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new Dialog(this.getActivity(), this.getTheme());
}
我們進去看看構(gòu)造方法做了什么
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
首先如果判斷是否在創(chuàng)建時有傳theme
這就是我們之前說的傳0表示沒樣式倔韭。
可以額外說說這個默認的樣式(這章暫時用不上),可以在themes.xml中找到
<!-- Dialog attributes -->
<item name="dialogTheme">@style/Theme.Dialog</item>
<item name="dialogTitleIconsDecorLayout">@layout/dialog_title_icons</item>
<item name="dialogCustomTitleDecorLayout">@layout/dialog_custom_title</item>
<item name="dialogTitleDecorLayout">@layout/dialog_title</item>
<item name="dialogPreferredPadding">@dimen/dialog_padding</item>
<item name="dialogCornerRadius">0dp</item>
mContext = new ContextThemeWrapper(context, themeResId);
就是把themeResId給保存到ContextThemeWrapper里面瓢对。
之后創(chuàng)建一個Window 寿酌,final Window w = new PhoneWindow(mContext);并且設(shè)置一些監(jiān)聽的事件,并且設(shè)置Gravity居中(所以默認的Dialog都是居中顯示)沥曹。
其實可以模仿他們這里的獲取windowManger的方法
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
這樣就創(chuàng)建好了Dialog份名,Dialog就是一個window。
然后設(shè)置布局給Dialog
可以看出在onActivityView中把布局設(shè)置給Dialog妓美,跳進去看源碼
public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
mWindow.setContentView(view, params);
}
也就是給這個window設(shè)置View
之后看看Dialog的展示
在onStart中展示Dialog,跳進去看源碼
public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
mCanceled = false;
if (!mCreated) {
dispatchOnCreate(null);
} else {
// Fill the DecorView in on any configuration changes that
// may have occured while it was removed from the WindowManager.
final Configuration config = mContext.getResources().getConfiguration();
mWindow.getDecorView().dispatchConfigurationChanged(config);
}
onStart();
mDecor = mWindow.getDecorView();
if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new WindowDecorActionBar(this);
}
WindowManager.LayoutParams l = mWindow.getAttributes();
boolean restoreSoftInputMode = false;
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
l.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
restoreSoftInputMode = true;
}
mWindowManager.addView(mDecor, l);
if (restoreSoftInputMode) {
l.softInputMode &=
~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
}
mShowing = true;
sendShowMessage();
}
mWindow.getDecorView()這個要詳細說又要扯到window鲤孵,一直扯其它的估計都講不完壶栋,所以這里先不講window。先把這行代碼理解成獲取window頂層的view普监。
然后設(shè)置ActionBar贵试,一般我們都沒有的。
之后這行就很熟悉凯正,就是設(shè)置window的屬性
WindowManager.LayoutParams l = mWindow.getAttributes();
之后就是處理軟鍵盤的操作毙玻。注意,這個show方法中最關(guān)鍵的方法就是顯示window的頁面廊散,也就是這句代碼(因為window的相關(guān)內(nèi)容不打算在這章講)桑滩,所以先了解。
mWindowManager.addView(mDecor, l);
最后在記錄當(dāng)前狀態(tài)為展示允睹。
我們再來額外先看看隱藏的方法
public void hide() {
if (mDecor != null) {
mDecor.setVisibility(View.GONE);
}
}
可以看出并沒有關(guān)閉window运准,只是對view做setVisibility隱藏操作。
最后看看關(guān)閉彈框
在onDestroyView中關(guān)閉Dialog缭受,跳進去看看源碼
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
也很簡單胁澳,就是移除窗口,然后再改變狀態(tài)米者。
mWindowManager.removeViewImmediate(mDecor);
可能有人會問韭畸,咦宇智,那為什么沒有看到在哪里設(shè)置樣式。
如果我們傳的是資源文件來設(shè)置樣式的話胰丁,資源文件會傳給context随橘,context會傳給window,樣式的設(shè)置就是在window內(nèi)部設(shè)置的隘马。如果我們動態(tài)設(shè)置樣式的話太防,一般都寫
getDialog().getWindow().XXXXXXX
這樣設(shè)置也是傳給window來設(shè)置。
三. 總結(jié)
從源碼我們可以看出酸员,DialogFragment實質(zhì)上還是操作Dialog蜒车,而Dialog實質(zhì)上是操作Window。所以我們是不是得出一個結(jié)論幔嗦,如果想測試某個屬性對Dialog有什么影響酿愧,基本上可以直接測這條屬性對Window有什么影響。
之后我總結(jié)的Dialog的一些屬性的分析邀泉,就可以寫到Window相關(guān)的地方嬉挡,關(guān)鍵的還是window,但是window的源碼就沒Dialog的這么簡單了汇恤,這個之后再講庞钢。