谈谈Fragment的用法之Fragment实现Tab切换中的那些事

Fragment在Android开发中占据着不可替代的作用。
举一些常见的应用场景:

  • 各种tab切换页面
  • 解耦Activity
  • 业务复用

今天我们就来谈谈Fragment在Tab切换中的状态变化等。
这里我们就拿QQ来分析
QQ主页包含3个模块:消息、联系人、动态。消息模块又包含了两个子模块:消息和电话。
这种使用Fragment来实现是再好不过的了。

首先底部的我们使用FragmentTabHost即可,这里我们对系统的这个控件做了简单的修改。系统的这个控件在切换tab的时候是会detach 当前的Fragment, 也就是销毁当前Fragment的视图。这样就会导致每次切换tab的时候都会重新走onCreateView,重新创建Fragment view。这样我们之前的状态就会丢失,这当然不是我们所想要的。

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相关的代码改为hide和show。这样Fragment加载一次后就不会再重新加载了,我们的状态也不会丢失。
这里还封装了一个BaseTabActivity基类
http://git.oschina.net/jaaksi/BaseLib/blob/master/src/main/java/org/an/ku/base/BaseTabsActivity.java
当然,如果你不想用FragmentTabHost,我推荐你使用另外一个强大的Tab库。
https://github.com/H07000223/FlycoTabLayout/blob/master/README_CN.md
它的强大我这里就罗嗦了,感兴趣的可以看看这个库。这位大神还有另外一个强大的库RoundView.

使用FragmentTabHost,我们就可以很简单的实现底部的3个tab。再去分析消息模块的子模块。这里我们就可以使用上面提到的FlycoTabLayout库来实现(它在切换的时候也是使用的hide, show的方式),当然你也可以手动去实现。我是个不喜欢重复造轮子的人。

1.这里要用到Fragment嵌套子Fragment。要注意在Fragment中嵌套Fragment要使用getChildFragmentManager()来获取FragmentManager。这里TabLayout库就不能用了。我改了一下它的setTabData()方法,直接将FragmentManager传过去,这样就不用考虑是否是子Fragment了。
2.还有一点,FragmentTabHost(我们修改的)只会加载一个Fragment,当切到指定tab时,才会去加载其他的,而把之前的hide。而FlycoTabLayout库则一开始会把所有的Fragment都加载进来,然后hide所有,然后再show指定tab的。如果你不想要这样的效果,你可以很简单的去修改这个库。

总之,实现这样的功能很简单,这个并不是我们今天要说的重点。我们要分析的是在tab切换时,对应Fragment的状态变化。

  • 第一次创建主页Activity时处于联系人Fragment,然后当我们切换到消息Fragment,msg开始创建,这个过程消息Fragment和它的两个子Fragment都经历了什么?
  • 切换消息的两个子Fragment,他们的状态又是如何变化的?
  • onResume又会对这些Fragment有什么影响?

事实上QQ并不是在初始化的时候只加载一个Fragment,在切换时才会去加载其他Fragment,这里我们只是拿QQ来描述我们的使用场景。

为了更直接的分析上面的几个问题,我们来分析几个方法:

  • isResume()
  • isHidden()
  • isVisible()
  • onResume()
  • onHiddenChanged()

我们今天主要也就是搞清楚在切换tab及onResume时Fragment的这些回调及状态的变化。下面先来简单解释一下这些方法。

  • onResume()不用多说,和Activity的onResume是对应的。
  • isResume()也很简单,就是Fragment是否处于Resume状态,即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发生改变的回调的方法。这个回调的时机是我们主动调用hide(), show()方法。

需要说明的是Fragment在初始化的时候并不会回调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()回调来监听Fragment 这个状态的变化。这个回调的参数其实就是当前的hidden state.
    默认情况下,add之后的Fragment是处于shown状态的。
/**
 * 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是否对用户可见。事实上这么说是不完全正确的。
    我们分析一下,这个方法的实现,isAdd()是否添加,!isHidden()是否隐藏,后面的表示Fragment依附的容器view是否visible,该view是否依附在window中。
    对于普通的Fragment而言,这么理解是对的。但是对于嵌套在Fragment中的子Fragment,就不对了。
    如果当前嵌套中的子Fragment isVisible()=true,此时调用父Fragment的hide()方法,那么对父Fragment而言,isHidden()返会ture,isVisible()返回false。而对于子Fragment 并没有调用hide(),show()方法,父Fragment的hide,show对它并没有任何影响,isVisible()依然是true的。但事实上,因为父Fragment是不可见的了,所以自然而然子Fragment也是不可见的了。

所以我们可以这么改造一下这个方法。真正意义上的可见。

/**
 * 是否真正的对用户可见
 * @return
 */
public boolean isRealVisible() {
  if (getParentFragment() == null) {
    return isVisible();
  } else {
    return isVisible() && getParentFragment().isVisible();
  }
}

解释完这些方法,接下来,我们来分析一个完整的流程中Fragment的状态变化。还拿QQ来描述。


我们分析一这样个场景:
进入主页(默认初始化联系人Fragment,尚且认为其他不会被初始化),然后切换到消息模块,将这个过程定义为过程A。然后再切换到联系人模块,这个过程定义为B。
为了简单的描述,我们记消息模块Fragment 为 MsgF,子消息Fragment为MsgSubF,联系人模块为ContactF。
下面就分析一下A和B过程中都发生了什么:

A过程,切换到消息模块时,MsgF开始创建,子Fragment MsgSubF开始创建。MsgF onResume(),而后MsgSubF onResume().整个过程就是一个简单的初始化过程。
B过程,切回联系人模块,MsgF被hide,回调onHiddenChanged()方法,isVisible()=false。但正如前面说到的,子Fragment MsgSubF并不会回调onHiddenChanged(),isVisible()依然是true,但是父Fragment不可见了,子Fragment也就不可见了。

分析完上面的场景,我们来分析一个开发中的应用。

假设我们要在很多Activity页面做某个操作后回到消息列表时需要刷新子Fragment MsgSubF页面。

首先多个Activity,如果我们采用startActivityForResult就不是很方便了。两个原因,子Fragment是不能接收到onActivityResult回调的(非嵌套可以)。第二个原因,即使是非嵌套,可以接收到onActivityResult回调,也不推荐使用。因为如果有很多跳转时,各种requestCode,resultCode,就会显得比较乱,难以维护。对于这种统一行为的操作,建议使用EventBus。在触发的地方发送一个事件,在MsgSubF(需要处理的地方)中处理。
然而eventbus发送之后立刻就会收到,我们是希望,在MsgSubF页面对用户可见时才去刷新。那么该怎么处理呢?实际上我们可以在接收到event的时候,设置一个flag,用于标识是否需要刷新。在Fragment可见的时候再去做刷新操作。

秉着这个思路,我们去分析。这里要考虑回到主页时是否处于消息模块(确切的说子Fragment是否是真的对用户可见的)。回到主页时,Fragment和子Fragment都会回调onResume().所以如果处于消息模块,就很简单了,直接在MsgSubF中的onResume方法中,根据flag判断是否需要刷新,如果需要,就去执行,刷新之后重置flag。

我们来重点分析一下,另外一种情况。

回到主页时,并未处于MsgF,而MsgSubF isVisible()是true的。当回到主页时,回调onResume,但是MsgSubF是不可见的,所以此时不应该去处理刷新。应该在切换到消息模块,MsgSubF可见的时候,再去执行刷新。然而不幸的是,切换tab时,只会回调父Fragment的onHiddenChanged()方法,子Fragment并不会回调。这就比较尴尬了。我们是没有办法直接通过系统的回调方法来处理了。
既然父Fragment会回调,而父Fragment又可以持有子Fragment的引用。那么我就可以在父Fragment的回调中去主动调用子Fragment的onHiddenChanged方法。

@Override public void onHiddenChanged(boolean hidden) {
  super.onHiddenChanged(hidden);
  // fixme 由于该方法只会在hide,show的时候回调,导致切换父fragment tab时,子Fragment不会回调此方法,如果需要子Fragment也回调,就手动调用
  for (int i = 0; i < mFragmentList.size(); i++) {
    Fragment fragment = mFragmentList.get(i);
    fragment.onHiddenChanged(fragment.isHidden());
  }
}

你也可以定义一个接口,让你的子Fragment实现这个接口,然后在父onHiddenChanged()回调中去回调这个接口。

public interface OnSupperHiddenChangedListener {
  void onSupperHiddenChanged(boolean hidden);
}

这么一来,我们就可以实现在子Fragment真正可见的时候去刷新了。
好吧今天的主题到这里就结束了。

其实Fragment还是有不少坑在的,比如getActivity()==null,页面重叠等。之后会分享一篇关于Fragment页面重叠的分析和解决办法(其实就是数据恢复造成的)

你可能感兴趣的:(谈谈Fragment的用法之Fragment实现Tab切换中的那些事)