Fragment在Android開發(fā)中占據(jù)著不可替代的作用。
舉一些常見的應(yīng)用場景:
- 各種tab切換頁面
- 解耦A(yù)ctivity
- 業(yè)務(wù)復(fù)用
今天我們就來談?wù)凢ragment在Tab切換中的狀態(tài)變化等。
這里我們就拿QQ來分析
QQ主頁包含3個模塊:消息、聯(lián)系人、動態(tài)。消息模塊又包含了兩個子模塊:消息和電話。
這種使用Fragment來實現(xiàn)是再好不過的了。
首先底部的我們使用FragmentTabHost即可,這里我們對系統(tǒng)的這個控件做了簡單的修改。系統(tǒng)的這個控件在切換tab的時候是會detach 當(dāng)前的Fragment, 也就是銷毀當(dāng)前Fragment的視圖。這樣就會導(dǎo)致每次切換tab的時候都會重新走onCreateView,重新創(chuàng)建Fragment view。這樣我們之前的狀態(tài)就會丟失,這當(dāng)然不是我們所想要的。
private FragmentTransaction doTabChanged(String tabId, FragmentTransaction ft) {
FragmentTabHost.TabInfo newTab = null;
for (int i = 0; i < mTabs.size(); i++) {
FragmentTabHost.TabInfo tab = mTabs.get(i);
if (tab.tag.equals(tabId)) {
newTab = tab;
}
}
if (newTab == null) {
throw new IllegalStateException("No tab known for tag " + tabId);
}
if (mLastTab != newTab) {
if (ft == null) {
ft = mFragmentManager.beginTransaction();
}
if (mLastTab != null) {
if (mLastTab.fragment != null) {
ft.detach(mLastTab.fragment);
}
}
if (newTab != null) {
if (newTab.fragment == null) {
newTab.fragment = Fragment.instantiate(mContext, newTab.clss.getName(), newTab.args);
ft.add(mContainerId, newTab.fragment, newTab.tag);
} else {
ft.attach(newTab.fragment);
}
}
mLastTab = newTab;
}
return ft;
}
http://git.oschina.net/jaaksi/BaseLib/blob/master/src/main/java/org/an/ku/base/FragmentTabHost.java
這里我們只做了很少的改動。主要是把detach和attach相關(guān)的代碼改為hide和show。這樣Fragment加載一次后就不會再重新加載了,我們的狀態(tài)也不會丟失。
這里還封裝了一個BaseTabActivity基類
http://git.oschina.net/jaaksi/BaseLib/blob/master/src/main/java/org/an/ku/base/BaseTabsActivity.java
當(dāng)然,如果你不想用FragmentTabHost,我推薦你使用另外一個強大的Tab庫。
https://github.com/H07000223/FlycoTabLayout/blob/master/README_CN.md
它的強大我這里就羅嗦了,感興趣的可以看看這個庫。這位大神還有另外一個強大的庫RoundView.
使用FragmentTabHost,我們就可以很簡單的實現(xiàn)底部的3個tab。再去分析消息模塊的子模塊。這里我們就可以使用上面提到的FlycoTabLayout庫來實現(xiàn)(它在切換的時候也是使用的hide, show的方式),當(dāng)然你也可以手動去實現(xiàn)。我是個不喜歡重復(fù)造輪子的人。
1.這里要用到Fragment嵌套子Fragment。要注意在Fragment中嵌套Fragment要使用getChildFragmentManager()來獲取FragmentManager。這里TabLayout庫就不能用了。我改了一下它的setTabData()方法,直接將FragmentManager傳過去,這樣就不用考慮是否是子Fragment了。
2.還有一點,F(xiàn)ragmentTabHost(我們修改的)只會加載一個Fragment,當(dāng)切到指定tab時,才會去加載其他的,而把之前的hide。而FlycoTabLayout庫則一開始會把所有的Fragment都加載進來,然后hide所有,然后再show指定tab的。如果你不想要這樣的效果,你可以很簡單的去修改這個庫。
總之,實現(xiàn)這樣的功能很簡單,這個并不是我們今天要說的重點。我們要分析的是在tab切換時,對應(yīng)Fragment的狀態(tài)變化。
- 第一次創(chuàng)建主頁Activity時處于聯(lián)系人Fragment,然后當(dāng)我們切換到消息Fragment,msg開始創(chuàng)建,這個過程消息Fragment和它的兩個子Fragment都經(jīng)歷了什么?
- 切換消息的兩個子Fragment,他們的狀態(tài)又是如何變化的?
- onResume又會對這些Fragment有什么影響?
事實上QQ并不是在初始化的時候只加載一個Fragment,在切換時才會去加載其他Fragment,這里我們只是拿QQ來描述我們的使用場景。
為了更直接的分析上面的幾個問題,我們來分析幾個方法:
- isResume()
- isHidden()
- isVisible()
- onResume()
- onHiddenChanged()
我們今天主要也就是搞清楚在切換tab及onResume時Fragment的這些回調(diào)及狀態(tài)的變化。下面先來簡單解釋一下這些方法。
- onResume()不用多說,和Activity的onResume是對應(yīng)的。
- isResume()也很簡單,就是Fragment是否處于Resume狀態(tài),即onResume()之后就為ture,onPause()之后為false,這里不做多說。
/**
* Called when the hidden state (as returned by {@link #isHidden()} of
* the fragment has changed. Fragments start out not hidden; this will
* be called whenever the fragment changes state from that.
* @param hidden True if the fragment is now hidden, false otherwise.
*/
public void onHiddenChanged(boolean hidden) {
}
- onHiddenChanged 方法是在Fragment的hidden state發(fā)生改變的回調(diào)的方法。這個回調(diào)的時機是我們主動調(diào)用hide(), show()方法。
需要說明的是Fragment在初始化的時候并不會回調(diào)onHiddenChanged()方法。
/**
* Return true if the fragment has been hidden. By default fragments
* are shown. You can find out about changes to this state with
* {@link #onHiddenChanged}. Note that the hidden state is orthogonal
* to other states -- that is, to be visible to the user, a fragment
* must be both started and not hidden.
*/
final public boolean isHidden() {
return mHidden;
}
- isHidden()就是返回hidden state,我們可以通過onHiddenChanged()回調(diào)來監(jiān)聽Fragment 這個狀態(tài)的變化。這個回調(diào)的參數(shù)其實就是當(dāng)前的hidden state.
默認情況下,add之后的Fragment是處于shown狀態(tài)的。
/**
* Return true if the fragment is currently visible to the user. This means
* it: (1) has been added, (2) has its view attached to the window, and
* (3) is not hidden.
*/
final public boolean isVisible() {
return isAdded()
&& !isHidden()
&& mView != null
&& mView.getWindowToken() != null
&& mView.getVisibility() == View.VISIBLE;
}
- 這里著重說一下isVisible()這個方法。
Return true if the fragment is currently visible to the user。
看官方注釋,很多人理解為這個返回值就是指Fragment是否對用戶可見。事實上這么說是不完全正確的。
我們分析一下,這個方法的實現(xiàn),isAdd()是否添加,!isHidden()是否隱藏,后面的表示Fragment依附的容器view是否visible,該view是否依附在window中。
對于普通的Fragment而言,這么理解是對的。但是對于嵌套在Fragment中的子Fragment,就不對了。
如果當(dāng)前嵌套中的子Fragment isVisible()=true,此時調(diào)用父Fragment的hide()方法,那么對父Fragment而言,isHidden()返會ture,isVisible()返回false。而對于子Fragment 并沒有調(diào)用hide(),show()方法,父Fragment的hide,show對它并沒有任何影響,isVisible()依然是true的。但事實上,因為父Fragment是不可見的了,所以自然而然子Fragment也是不可見的了。
所以我們可以這么改造一下這個方法。真正意義上的可見。
/**
* 是否真正的對用戶可見
* @return
*/
public boolean isRealVisible() {
if (getParentFragment() == null) {
return isVisible();
} else {
return isVisible() && getParentFragment().isVisible();
}
}
解釋完這些方法,接下來,我們來分析一個完整的流程中Fragment的狀態(tài)變化。還拿QQ來描述。
我們分析一這樣個場景:
進入主頁(默認初始化聯(lián)系人Fragment,尚且認為其他不會被初始化),然后切換到消息模塊,將這個過程定義為過程A。然后再切換到聯(lián)系人模塊,這個過程定義為B。
為了簡單的描述,我們記消息模塊Fragment 為 MsgF,子消息Fragment為MsgSubF,聯(lián)系人模塊為ContactF。
下面就分析一下A和B過程中都發(fā)生了什么:
A過程,切換到消息模塊時,MsgF開始創(chuàng)建,子Fragment MsgSubF開始創(chuàng)建。MsgF onResume(),而后MsgSubF onResume().整個過程就是一個簡單的初始化過程。
B過程,切回聯(lián)系人模塊,MsgF被hide,回調(diào)onHiddenChanged()方法,isVisible()=false。但正如前面說到的,子Fragment MsgSubF并不會回調(diào)onHiddenChanged(),isVisible()依然是true,但是父Fragment不可見了,子Fragment也就不可見了。
分析完上面的場景,我們來分析一個開發(fā)中的應(yīng)用。
假設(shè)我們要在很多Activity頁面做某個操作后回到消息列表時需要刷新子Fragment MsgSubF頁面。
首先多個Activity,如果我們采用startActivityForResult就不是很方便了。兩個原因,子Fragment是不能接收到onActivityResult回調(diào)的(非嵌套可以)。第二個原因,即使是非嵌套,可以接收到onActivityResult回調(diào),也不推薦使用。因為如果有很多跳轉(zhuǎn)時,各種requestCode,resultCode,就會顯得比較亂,難以維護。對于這種統(tǒng)一行為的操作,建議使用EventBus。在觸發(fā)的地方發(fā)送一個事件,在MsgSubF(需要處理的地方)中處理。
然而eventbus發(fā)送之后立刻就會收到,我們是希望,在MsgSubF頁面對用戶可見時才去刷新。那么該怎么處理呢?實際上我們可以在接收到event的時候,設(shè)置一個flag,用于標識是否需要刷新。在Fragment可見的時候再去做刷新操作。
秉著這個思路,我們?nèi)シ治?。這里要考慮回到主頁時是否處于消息模塊(確切的說子Fragment是否是真的對用戶可見的)?;氐街黜摃r,F(xiàn)ragment和子Fragment都會回調(diào)onResume().所以如果處于消息模塊,就很簡單了,直接在MsgSubF中的onResume方法中,根據(jù)flag判斷是否需要刷新,如果需要,就去執(zhí)行,刷新之后重置flag。
我們來重點分析一下,另外一種情況。
回到主頁時,并未處于MsgF,而MsgSubF isVisible()是true的。當(dāng)回到主頁時,回調(diào)onResume,但是MsgSubF是不可見的,所以此時不應(yīng)該去處理刷新。應(yīng)該在切換到消息模塊,MsgSubF可見的時候,再去執(zhí)行刷新。然而不幸的是,切換tab時,只會回調(diào)父Fragment的onHiddenChanged()方法,子Fragment并不會回調(diào)。這就比較尷尬了。我們是沒有辦法直接通過系統(tǒng)的回調(diào)方法來處理了。
既然父Fragment會回調(diào),而父Fragment又可以持有子Fragment的引用。那么我就可以在父Fragment的回調(diào)中去主動調(diào)用子Fragment的onHiddenChanged方法。
@Override public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
// fixme 由于該方法只會在hide,show的時候回調(diào),導(dǎo)致切換父fragment tab時,子Fragment不會回調(diào)此方法,如果需要子Fragment也回調(diào),就手動調(diào)用
for (int i = 0; i < mFragmentList.size(); i++) {
Fragment fragment = mFragmentList.get(i);
fragment.onHiddenChanged(fragment.isHidden());
}
}
你也可以定義一個接口,讓你的子Fragment實現(xiàn)這個接口,然后在父onHiddenChanged()回調(diào)中去回調(diào)這個接口。
public interface OnSupperHiddenChangedListener {
void onSupperHiddenChanged(boolean hidden);
}
這么一來,我們就可以實現(xiàn)在子Fragment真正可見的時候去刷新了。
好吧今天的主題到這里就結(jié)束了。
其實Fragment還是有不少坑在的,比如getActivity()==null,頁面重疊等。之后會分享一篇關(guān)于Fragment頁面重疊的分析和解決辦法(其實就是數(shù)據(jù)恢復(fù)造成的)