Exchange Fragment in FragmentTabHost/替换FragmentTabHost中的Fragment

因为Android App经常要做成iOS的样子,FragmentTabHost也算是比较常用的控件了(不考虑Google打脸的新TabHost控件)。在使用FragmentTabHost时,我们可能会遇到这种需求,就是将某个Tab进行替换,比如“我”这个Tab,如果是未登录状态就需要切换成未登录状态的页面。

本文就讲一下如何实现FragmentTabHost中对Fragment进行替换。先看下效果:

preview]

FragmentTabHost继承自TabHost,使用TabHost的切换逻辑,又通过FragmentManager封装了对Fragment切换的操作。FragmentTabHost在实现时通过使用TabInfo 缓存Fragment,所以我们在替换Fragment的时候,要考虑到复用的问题。

思路不难,清空FragmentTabHost,重建Tabs。因为有缓存,所以清空的时候麻烦一点。核心代码如下:

public void resetTabs(String oldFragmentTag) {
    if (mTabHost != null) {
        int currentTab = mTabHost.getCurrentTab();
        mTabHost.clearAllTabs();

        //noinspection TryWithIdenticalCatches
        try {
            Field mTabs = mTabHost.getClass().getDeclaredField("mTabs");
            mTabs.setAccessible(true);
            Object o = mTabs.get(mTabHost);
            if (o instanceof List) {
                ((List) o).clear();
            }

            Field mLastTab = mTabHost.getClass().getDeclaredField("mLastTab");
            mLastTab.setAccessible(true);
            mLastTab.set(mTabHost, null);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        if (!TextUtils.isEmpty(oldFragmentTag)) {
            try {
                Fragment f = getSupportFragmentManager().findFragmentByTag(oldFragmentTag);
                if (f != null) {
                    getSupportFragmentManager().beginTransaction().remove(f).commitAllowingStateLoss();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        createTabs();
        mTabHost.setCurrentTab(currentTab);
    }
}
  1. 使用mTabHost.clearAllTabs()方法清空Tabs。
  2. 使用反射方法将FragmentTabHost的mTabs字段和mLastTab字段置空。
  3. 使用FragmentManager移除掉需要被替换的Fragment。
  4. 注意:FragmentTabHost中的Tag一定要是唯一的。
    如果只进行第三个操作,然后调用createTabs()重建Tabs,在切换成原始Fragment的时候就会出现页面空白的Bug。原因是,FragmentManager操作Fragment的FragmentTransaction操作是异步的,调用FragmentTransaction的commit方法时,是将一系列操作添加到了队列中,并没有立刻执行。

而此时如果执行FragmentTabHost的addTab方法,FragmentTabHost会使用tag参数在FragmentManager中查找Fragment,如果存在并且没有被Detach,就会将其detach掉。FragmentTabHost源码:

public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
tabSpec.setContent(new DummyTabFactory(mContext));
String tag = tabSpec.getTag();

    TabInfo info = new TabInfo(tag, clss, args);

    if (mAttached) {
        // If we are already attached to the window, then check to make
        // sure this tab's fragment is inactive if it exists.  This shouldn't
        // normally happen.
        info.fragment = mFragmentManager.findFragmentByTag(tag);
        if (info.fragment != null &amp;&amp; !info.fragment.isDetached()) {
            FragmentTransaction ft = mFragmentManager.beginTransaction();
            ft.detach(info.fragment);
            ft.commit();
        }
    }

    mTabs.add(info);
    addTab(tabSpec);
}</pre>

回到上面说的,FragmentTransition是异步的,所以Fragment还没有被Detach,一个Fragment踩到两只FragmentTransition的船上,这船说翻就翻啊。其实是Fragment的状态和Fragment实际的状态会出现不一致的情况,导致Fragment无法Detach,也无法attach。

那不执行第三个操作会怎样?会导致Fragment没有被移除,就出现Fragment重叠的情况了,代码注释掉感受下吧。

对于,项目源码在Github上可以找到。