Fragment在Android開發(fā)中占據(jù)著不可替代的作用晌缘。
舉一些常見的應(yīng)用場(chǎng)景:
- 各種tab切換頁面
- 解耦A(yù)ctivity
- 業(yè)務(wù)復(fù)用
今天我們就來談?wù)凢ragment在Tab切換中的狀態(tài)變化等桃漾。
這里我們就拿QQ來分析
QQ主頁包含3個(gè)模塊:消息凡伊、聯(lián)系人罕模、動(dòng)態(tài)。消息模塊又包含了兩個(gè)子模塊:消息和電話寺董。
這種使用Fragment來實(shí)現(xiàn)是再好不過的了。
首先底部的我們使用FragmentTabHost即可,這里我們對(duì)系統(tǒng)的這個(gè)控件做了簡(jiǎn)單的修改碌奉。系統(tǒng)的這個(gè)控件在切換tab的時(shí)候是會(huì)detach 當(dāng)前的Fragment, 也就是銷毀當(dāng)前Fragment的視圖短曾。這樣就會(huì)導(dǎo)致每次切換tab的時(shí)候都會(huì)重新走onCreateView寒砖,重新創(chuàng)建Fragment view。這樣我們之前的狀態(tài)就會(huì)丟失嫉拐,這當(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
這里我們只做了很少的改動(dòng)。主要是把detach和attach相關(guān)的代碼改為hide和show婉徘。這樣Fragment加載一次后就不會(huì)再重新加載了漠嵌,我們的狀態(tài)也不會(huì)丟失。
這里還封裝了一個(gè)BaseTabActivity基類
http://git.oschina.net/jaaksi/BaseLib/blob/master/src/main/java/org/an/ku/base/BaseTabsActivity.java
當(dāng)然盖呼,如果你不想用FragmentTabHost儒鹿,我推薦你使用另外一個(gè)強(qiáng)大的Tab庫。
https://github.com/H07000223/FlycoTabLayout/blob/master/README_CN.md
它的強(qiáng)大我這里就羅嗦了几晤,感興趣的可以看看這個(gè)庫约炎。這位大神還有另外一個(gè)強(qiáng)大的庫RoundView.
使用FragmentTabHost,我們就可以很簡(jiǎn)單的實(shí)現(xiàn)底部的3個(gè)tab蟹瘾。再去分析消息模塊的子模塊圾浅。這里我們就可以使用上面提到的FlycoTabLayout庫來實(shí)現(xiàn)(它在切換的時(shí)候也是使用的hide, show的方式),當(dāng)然你也可以手動(dòng)去實(shí)現(xiàn)憾朴。我是個(gè)不喜歡重復(fù)造輪子的人狸捕。
1.這里要用到Fragment嵌套子Fragment。要注意在Fragment中嵌套Fragment要使用getChildFragmentManager()來獲取FragmentManager众雷。這里TabLayout庫就不能用了灸拍。我改了一下它的setTabData()方法做祝,直接將FragmentManager傳過去,這樣就不用考慮是否是子Fragment了鸡岗。
2.還有一點(diǎn)剖淀,F(xiàn)ragmentTabHost(我們修改的)只會(huì)加載一個(gè)Fragment,當(dāng)切到指定tab時(shí)纤房,才會(huì)去加載其他的纵隔,而把之前的hide。而FlycoTabLayout庫則一開始會(huì)把所有的Fragment都加載進(jìn)來炮姨,然后hide所有捌刮,然后再show指定tab的。如果你不想要這樣的效果舒岸,你可以很簡(jiǎn)單的去修改這個(gè)庫绅作。
總之,實(shí)現(xiàn)這樣的功能很簡(jiǎn)單蛾派,這個(gè)并不是我們今天要說的重點(diǎn)俄认。我們要分析的是在tab切換時(shí),對(duì)應(yīng)Fragment的狀態(tài)變化洪乍。
- 第一次創(chuàng)建主頁Activity時(shí)處于聯(lián)系人Fragment眯杏,然后當(dāng)我們切換到消息Fragment,msg開始創(chuàng)建壳澳,這個(gè)過程消息Fragment和它的兩個(gè)子Fragment都經(jīng)歷了什么岂贩?
- 切換消息的兩個(gè)子Fragment,他們的狀態(tài)又是如何變化的巷波?
- onResume又會(huì)對(duì)這些Fragment有什么影響萎津?
事實(shí)上QQ并不是在初始化的時(shí)候只加載一個(gè)Fragment,在切換時(shí)才會(huì)去加載其他Fragment抹镊,這里我們只是拿QQ來描述我們的使用場(chǎng)景锉屈。
為了更直接的分析上面的幾個(gè)問題,我們來分析幾個(gè)方法:
- isResume()
- isHidden()
- isVisible()
- onResume()
- onHiddenChanged()
我們今天主要也就是搞清楚在切換tab及onResume時(shí)Fragment的這些回調(diào)及狀態(tài)的變化垮耳。下面先來簡(jiǎn)單解釋一下這些方法颈渊。
- onResume()不用多說,和Activity的onResume是對(duì)應(yīng)的氨菇。
- isResume()也很簡(jiǎn)單儡炼,就是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)的方法豌研。這個(gè)回調(diào)的時(shí)機(jī)是我們主動(dòng)調(diào)用hide(), show()方法妹田。
需要說明的是Fragment在初始化的時(shí)候并不會(huì)回調(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 這個(gè)狀態(tài)的變化鬼佣。這個(gè)回調(diào)的參數(shù)其實(shí)就是當(dāng)前的hidden state.
默認(rèn)情況下驶拱,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()這個(gè)方法晶衷。
Return true if the fragment is currently visible to the user蓝纲。
看官方注釋,很多人理解為這個(gè)返回值就是指Fragment是否對(duì)用戶可見晌纫。事實(shí)上這么說是不完全正確的税迷。
我們分析一下,這個(gè)方法的實(shí)現(xiàn)锹漱,isAdd()是否添加箭养,!isHidden()是否隱藏,后面的表示Fragment依附的容器view是否visible哥牍,該view是否依附在window中毕泌。
對(duì)于普通的Fragment而言,這么理解是對(duì)的嗅辣。但是對(duì)于嵌套在Fragment中的子Fragment撼泛,就不對(duì)了。
如果當(dāng)前嵌套中的子Fragment isVisible()=true辩诞,此時(shí)調(diào)用父Fragment的hide()方法坎弯,那么對(duì)父Fragment而言,isHidden()返會(huì)ture译暂,isVisible()返回false。而對(duì)于子Fragment 并沒有調(diào)用hide()撩炊,show()方法外永,父Fragment的hide,show對(duì)它并沒有任何影響拧咳,isVisible()依然是true的伯顶。但事實(shí)上,因?yàn)楦窮ragment是不可見的了骆膝,所以自然而然子Fragment也是不可見的了祭衩。
所以我們可以這么改造一下這個(gè)方法。真正意義上的可見阅签。
/**
* 是否真正的對(duì)用戶可見
* @return
*/
public boolean isRealVisible() {
if (getParentFragment() == null) {
return isVisible();
} else {
return isVisible() && getParentFragment().isVisible();
}
}
解釋完這些方法掐暮,接下來,我們來分析一個(gè)完整的流程中Fragment的狀態(tài)變化政钟。還拿QQ來描述路克。
我們分析一這樣個(gè)場(chǎng)景:
進(jìn)入主頁(默認(rèn)初始化聯(lián)系人Fragment樟结,尚且認(rèn)為其他不會(huì)被初始化),然后切換到消息模塊精算,將這個(gè)過程定義為過程A瓢宦。然后再切換到聯(lián)系人模塊,這個(gè)過程定義為B灰羽。
為了簡(jiǎn)單的描述驮履,我們記消息模塊Fragment 為 MsgF,子消息Fragment為MsgSubF,聯(lián)系人模塊為ContactF廉嚼。
下面就分析一下A和B過程中都發(fā)生了什么:
A過程疲吸,切換到消息模塊時(shí),MsgF開始創(chuàng)建前鹅,子Fragment MsgSubF開始創(chuàng)建摘悴。MsgF onResume(),而后MsgSubF onResume().整個(gè)過程就是一個(gè)簡(jiǎn)單的初始化過程。
B過程舰绘,切回聯(lián)系人模塊蹂喻,MsgF被hide,回調(diào)onHiddenChanged()方法捂寿,isVisible()=false口四。但正如前面說到的,子Fragment MsgSubF并不會(huì)回調(diào)onHiddenChanged()秦陋,isVisible()依然是true蔓彩,但是父Fragment不可見了,子Fragment也就不可見了驳概。
分析完上面的場(chǎng)景赤嚼,我們來分析一個(gè)開發(fā)中的應(yīng)用。
假設(shè)我們要在很多Activity頁面做某個(gè)操作后回到消息列表時(shí)需要刷新子Fragment MsgSubF頁面顺又。
首先多個(gè)Activity更卒,如果我們采用startActivityForResult就不是很方便了。兩個(gè)原因稚照,子Fragment是不能接收到onActivityResult回調(diào)的(非嵌套可以)蹂空。第二個(gè)原因,即使是非嵌套果录,可以接收到onActivityResult回調(diào)上枕,也不推薦使用。因?yàn)槿绻泻芏嗵D(zhuǎn)時(shí)弱恒,各種requestCode辨萍,resultCode,就會(huì)顯得比較亂斤彼,難以維護(hù)分瘦。對(duì)于這種統(tǒng)一行為的操作蘸泻,建議使用EventBus。在觸發(fā)的地方發(fā)送一個(gè)事件嘲玫,在MsgSubF(需要處理的地方)中處理悦施。
然而eventbus發(fā)送之后立刻就會(huì)收到,我們是希望去团,在MsgSubF頁面對(duì)用戶可見時(shí)才去刷新抡诞。那么該怎么處理呢?實(shí)際上我們可以在接收到event的時(shí)候土陪,設(shè)置一個(gè)flag昼汗,用于標(biāo)識(shí)是否需要刷新。在Fragment可見的時(shí)候再去做刷新操作鬼雀。
秉著這個(gè)思路顷窒,我們?nèi)シ治觥_@里要考慮回到主頁時(shí)是否處于消息模塊(確切的說子Fragment是否是真的對(duì)用戶可見的)源哩⌒回到主頁時(shí),F(xiàn)ragment和子Fragment都會(huì)回調(diào)onResume().所以如果處于消息模塊励烦,就很簡(jiǎn)單了谓着,直接在MsgSubF中的onResume方法中,根據(jù)flag判斷是否需要刷新坛掠,如果需要赊锚,就去執(zhí)行,刷新之后重置flag屉栓。
我們來重點(diǎn)分析一下舷蒲,另外一種情況。
回到主頁時(shí)系瓢,并未處于MsgF阿纤,而MsgSubF isVisible()是true的。當(dāng)回到主頁時(shí)夷陋,回調(diào)onResume,但是MsgSubF是不可見的,所以此時(shí)不應(yīng)該去處理刷新胰锌。應(yīng)該在切換到消息模塊骗绕,MsgSubF可見的時(shí)候,再去執(zhí)行刷新资昧。然而不幸的是酬土,切換tab時(shí),只會(huì)回調(diào)父Fragment的onHiddenChanged()方法格带,子Fragment并不會(huì)回調(diào)撤缴。這就比較尷尬了刹枉。我們是沒有辦法直接通過系統(tǒng)的回調(diào)方法來處理了。
既然父Fragment會(huì)回調(diào)屈呕,而父Fragment又可以持有子Fragment的引用微宝。那么我就可以在父Fragment的回調(diào)中去主動(dòng)調(diào)用子Fragment的onHiddenChanged方法。
@Override public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
// fixme 由于該方法只會(huì)在hide,show的時(shí)候回調(diào)虎眨,導(dǎo)致切換父fragment tab時(shí)蟋软,子Fragment不會(huì)回調(diào)此方法,如果需要子Fragment也回調(diào)嗽桩,就手動(dòng)調(diào)用
for (int i = 0; i < mFragmentList.size(); i++) {
Fragment fragment = mFragmentList.get(i);
fragment.onHiddenChanged(fragment.isHidden());
}
}
你也可以定義一個(gè)接口,讓你的子Fragment實(shí)現(xiàn)這個(gè)接口岳守,然后在父onHiddenChanged()回調(diào)中去回調(diào)這個(gè)接口。
public interface OnSupperHiddenChangedListener {
void onSupperHiddenChanged(boolean hidden);
}
這么一來碌冶,我們就可以實(shí)現(xiàn)在子Fragment真正可見的時(shí)候去刷新了湿痢。
好吧今天的主題到這里就結(jié)束了。
其實(shí)Fragment還是有不少坑在的扑庞,比如getActivity()==null譬重,頁面重疊等。之后會(huì)分享一篇關(guān)于Fragment頁面重疊的分析和解決辦法(其實(shí)就是數(shù)據(jù)恢復(fù)造成的)