美文网首页安卓UI程序员
谈谈Fragment的用法之Fragment实现Tab切换中的那

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

作者: 笨小孩丶 | 来源:发表于2017-01-18 18:02 被阅读4571次

    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页面重叠的分析和解决办法(其实就是数据恢复造成的)

    相关文章

      网友评论

      • biginsect:来回切换tab的时候 fragment的onResume()方法不会重新运行?我对fragment的理解不太深刻,帮忙解答一下~
      • 魏魏魏魏:大佬,你好!我查看你推荐的BaseLib库但是并没有发现如何使用的demo或者文档,请问能介绍一下吗?https://gitee.com/jaaksi/BaseLib/tree/master
        笨小孩丶: @魏魏魏魏 这是我的个人收集的一些常用的工具类之类的

      本文标题:谈谈Fragment的用法之Fragment实现Tab切换中的那

      本文链接:https://www.haomeiwen.com/subject/azambttx.html