FragmentTabHost使用介绍及填坑指南

FragmentTabHost是什么,TabActivity应该大家比较熟悉。
不过,TabActivity已经早被淘汰,而FragmentTabHost就是TabActivity的替代品(类)。
它主要用于实现常用的底部菜单栏,常用于实现底部菜单栏大概有以下几种:

  • TabWidget + FragmentTabHost
  • 隐藏TabWidget,使用RadioGroup和RadioButton + FragmentTabHost
  • TabLayout
  • Bottom navigation

今天主要讲解FragmentTabHost实现自定义菜单栏的简易流程及填坑指南


FragmentTabHost 介绍

FragmentTabHost类官方介绍

  • 允许你给每个标签(菜单)设置一个fragment。
  • 在将视图放置在视图层次结构中时,必须调用FragmentTabHost.setup(Context, FragmentManager, int)实现视图初始化绑定。

FragmentTabHost 使用示例

以下是官方提供的两个示例,分别在activity、fragmnet场景下的。
主要使用方法有两个:

  • setup(Context, FragmentManager, int); //给出FragmentManager将添加的fargment添加到int对应的视图
  • addTab (TabHost.TabSpec tabSpec,Class<?> clss,Bundle args) //用于添加每个标签的fragment
    可以简单看下以下示例,后续讲解使用中遇到的坑

在FragmentActivity 操作FragmentTabHost

  • android.support.v4.app.FragmentTabHost对应的xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v4.app.FragmentTabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TabWidget
    android:id="@android:id/tabs"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_weight="0"/>

    <FrameLayout
    android:id="@android:id/tabcontent"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_weight="0"/>

    <FrameLayout
    android:id="@+id/realtabcontent"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"/>

    </LinearLayout>
    </android.support.v4.app.FragmentTabHost>
  • FragmentTabs类绑定操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import com.example.android.supportv4.R;

    import android.os.Bundle;
    import android.support.v4.app.FragmentActivity;
    import android.support.v4.app.FragmentTabHost;

    /**
    * This demonstrates how you can implement switching between the tabs of a
    * TabHost through fragments, using FragmentTabHost.
    */
    public class FragmentTabs extends FragmentActivity {
    private FragmentTabHost mTabHost;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_tabs);
    mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);
    mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);
    mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
    FragmentStackSupport.CountingFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
    LoaderCursorSupport.CursorLoaderListFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
    LoaderCustomSupport.AppListFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
    LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
    }
    }

Fragment 操作FragmentTabHost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.example.android.supportv4.R;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTabHost;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class FragmentTabsFragmentSupport extends Fragment {
private FragmentTabHost mTabHost;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mTabHost = new FragmentTabHost(getActivity());
mTabHost.setup(getActivity(), getChildFragmentManager(), R.id.fragment1);
mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
FragmentStackSupport.CountingFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
LoaderCursorSupport.CursorLoaderListFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
LoaderCustomSupport.AppListFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
return mTabHost;
}

@Override
public void onDestroyView() {
super.onDestroyView();
mTabHost = null;
}
}

填坑指南

通过FragmentManager可能获取不到指定tag的fragment。

实现上,FragmentTabHost内部通过addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args)添加
Tab,内部内创建以下对象,并存放在集合里面。

1
2
3
4
5
6
 static final class TabInfo {
final @NonNull String tag;
final @NonNull Class<?> clss;
final @Nullable Bundle args;
Fragment fragment;//对,這就是我们显示需要的frgament。
}
  • TabInfo对象追踪介绍

    • tag 即 我们传递进去的tabSpec.getTag(),通过newTabSpec(String tag)赋值的。
    • clss 即, Class<?> clss,用于生成fragment的类
    • args 即,fragment创建传递过去的Bundle参数
    • fragment 即,我们填充到界面所不能缺少的。(坑在这,默认是空,那它是在什么时候创建的呢?)
  • 请看创建fragment的关键源码
    FragmentTabHost的doTabChanged方法,该方法调用时机:

    • onAttachedToWindow()会调用一次,即第一次创建,也就是说第一次创建只有创建默认选择的那个fragment,默认就是第一添加的那个class。所以其他fragment通过getSupportFragmentManager().findFragmentByTag(String tag);拿不到。
    • onTabChanged(String tabId)会调用,即。你切换到其他tag对于的fragment才创建fragmetn。如果没切换到其他fragmetn,则永远拿到的fragment都是空。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
       @Nullable
      private FragmentTransaction doTabChanged(@Nullable String tag,
      @Nullable FragmentTransaction ft) {
      final TabInfo newTab = getTabInfoForTag(tag);
      if (mLastTab != newTab) {//切换了tab
      if (ft == null) {
      ft = mFragmentManager.beginTransaction();
      }

      if (mLastTab != null) {
      if (mLastTab.fragment != null) {
      ft.detach(mLastTab.fragment);//取消关联fragment
      }
      }

      if (newTab != null) {//!!! 重点看这 (看到这了没?)
      if (newTab.fragment == null) {//!!! 就下面这代码,fragment创建的代码。意思是fragment是可以为null的,默认为null。
      newTab.fragment = Fragment.instantiate(mContext,
      newTab.clss.getName(), newTab.args);//这里调用了fragment内部通过反射创建fragment的方法,赋值
      ft.add(mContainerId, newTab.fragment, newTab.tag);//然后这才将该fragment添加到FragmentManager,
      // 也就是说只有这个时候getSupportFragmentManager().findFragmentByTag(String tag)才能拿到该tag对应的fragment
      } else {
      ft.attach(newTab.fragment);//关联fragment
      }
      }

      mLastTab = newTab;
      }
      return ft;
      }
      如果需要在activity操作fragment。
  • 解决方案

    • 方案1不需要即时调用,fragment的代码,作下延时获取
      • 1.在获取fragment的时候加判断,为null的时候获取一次,然后再判断下
      • 2.也可以在创建的时候将每个tab都选中一次,然后延时一下可获取所有frgament。模拟切换到每个tag对应的fragment。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
          if (mHomeFragment == null) {
        mHomeFragment = (HomeFragment) mNewTuHuTabActivity.getSupportFragmentManager().findFragmentByTag(mFragmentTags[0]);
        }
        if(mHomeFragment!=null) {
        mHomeFragment.onSelectArea();
        }else{
        //说明该fragment还未创建,如果非要调用该fragment的onSelectArea();可在这里做标记。tabhost.setOnTabChangedListener执行onTabChanged后为当前fragment后再获取然后调用。
        }
        tabhost.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
        @Override
        public void onTabChanged(String tabId) {

        }
        });
    • 方案2 重写FragmentTabHost。
      • 1.在addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args)的时候就创建fragment。
      • 2.在需要调用获取该fragment的时候,判断该fragment是否存在,如果不存在,然后调用创建该fragment并添加到FragmentManager。(主要是TabHost没提供公开的方法)

每次切换tab,fragment会重建视图

FragmentTabHost 在每次切换tab的代码中,会先调用detach,然后再调用attach。
于是:

  • 在切换过程中会先调用之前选中的fragment的onDestroyView方法
  • 然后调用当前可见fragment的onCreateView方法

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TabFragment extends Fragment {    
private View rootView;//将当前View缓存在内存中,再次调用onCreateView方法时,不再直接返回该View,而不需要重新创建。
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (rootView == null) {
rootView = inflater.inflate(R.layout.fragmetnview, container, false);
initView();
}else{
container.removeView(rootView);
}
return rootView;
}
}

其他

目前就遇到这两个坑比较大,如果小伙伴们还遇到什么坑,欢迎提出,大家一同解决,避免踩坑。