談?wù)凢ragment的用法之Fragment實(shí)現(xiàn)Tab切換中的那些事

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ù)造成的)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嫩挤,一起剝皮案震驚了整個(gè)濱河市害幅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岂昭,老刑警劉巖以现,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異约啊,居然都是意外死亡邑遏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門恰矩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來记盒,“玉大人,你說我怎么就攤上這事外傅〖退保” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵萎胰,是天一觀的道長(zhǎng)碾盟。 經(jīng)常有香客問我,道長(zhǎng)技竟,這世上最難降的妖魔是什么冰肴? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上熙尉,老公的妹妹穿的比我還像新娘联逻。我一直安慰自己,他們只是感情好检痰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布包归。 她就那樣靜靜地躺著,像睡著了一般攀细。 火紅的嫁衣襯著肌膚如雪箫踩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天谭贪,我揣著相機(jī)與錄音境钟,去河邊找鬼。 笑死俭识,一個(gè)胖子當(dāng)著我的面吹牛慨削,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播套媚,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缚态,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了堤瘤?” 一聲冷哼從身側(cè)響起玫芦,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎本辐,沒想到半個(gè)月后桥帆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慎皱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年老虫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茫多。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祈匙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出天揖,到底是詐尸還是另有隱情夺欲,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布今膊,位于F島的核電站洁闰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏万细。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赖钞。 院中可真熱鬧腰素,春花似錦、人聲如沸雪营。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽献起。三九已至洋访,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谴餐,已是汗流浹背姻政。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岂嗓,地道東北人汁展。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像厌殉,于是被迫代替她去往敵國(guó)和親食绿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容