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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容