> For the complete documentation index, see [llms.txt](https://ryan-8.gitbook.io/android-architecture-journey/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ryan-8.gitbook.io/android-architecture-journey/widgets/tab.md).

# 标签导航

标签导航是常见的几种APP导航模式之一. 我们会经常用到这种模式. 使用标签导航的APP比较著名的有微信, 如下图所示:

![微信](/files/-LsLm89nfYoXWPKe6mK_)

通常的做法是在底部写几个RadioButton, 或者是ImageView加TextView来模拟单选. 然后在Activity中使用OnClickListener来控制每个标签的状态, 同时还需要控制每个Fragment的创建与销毁, 显示与隐藏. 代码量不少, 如果每个项目都这么写会浪费很多精力与成本. 因此我们需要一个控件, 来代替OnClickListener去维护标签的状态, 以及Fragment的状态. 我们需要做的只是告诉这个控件, 每个标签长什么样, 标签对应的Fragment的类是什么, 需不需要参数等.

## 需求

下面我们试着实现以下UI效果:

![UI](/files/-LsLm89pkm9PH3QStbFB)

上述效果图在严格意义上来说不算是标签导航, 但是从开发来看, 实现的方式类似. 以下是具体需求:

> 有四个标签, 分别是心理测试, 心情圈, 健步走以及个人中心, 每个标签分别在当前页面内切换标签页. 点击中间的加号按钮则需要跳转到另一个页面, 当前页面则继续显示之前的标签页.

首先我们来分析一下重点:

1. 中间加号的UI效果实现
2. 点击加号跳转新的Activity, 并保持当前的标签页

如果没有中间的加号, 使用FragmentTabHost就能实现, 具体用法可以参考[Android常用控件之FragmentTabHost的使用](http://blog.csdn.net/deng0zhaotai/article/details/11264643). 但是有了中间的加号FragmentTabHost就力不从心了, 因为FragmentTabHost的每一个标签都要对应一个Fragment, 不能对应Activity, 也无法对点击事件做拦截. 通过查看源码发现OnTabChangeListener也是在标签切换完成之后才会回调. 因此FragmentTabHost无法完成上述需求. 怎么办呢? 引入InterceptedFragmentTabHost.

## InterceptedFragmentTabHost

InterceptedFragmentTabHost是CoreLibs中对FragmentTabHost的扩展, 实现了tab切换拦截功能. 一旦多了拦截功能, 就能实现上述需求了. 比如FragmentTabHost中实际上是有5个标签, 包含了中间的加号. 我可以通过设置拦截监听器, 检测一旦用户点击了第三个标签, 就不进行切换操作, 而是跳转至一个新的Activity.

由于通过继承FragmentTabHost没法很好的实现拦截功能, 并且FragmentTabHost内部只是引用了几个Android内部的id资源, 没有其他内部资源, 因此我们完全可以将FragmentTabHost的源码复制出来放到自己的项目中, 而不会出现编译不通过的后果. InterceptedFragmentTabHost就是通过这种方式对FragmentTabHost扩展.

接下来看看如何使用InterceptedFragmentTabHost. InterceptedFragmentTabHost中提供了如下方法来设置拦截监听器:

```
public void setTabChangeInterceptor(TabChangeInterceptor interceptor);
```

以下是TabChangeInterceptor接口的代码:

```
/**
 * 用于{@link InterceptedFragmentTabHost}权限控制
 */
public interface TabChangeInterceptor {
    /**
     * 是否能切换到标签为tabId的Fragment
     * @param tabId 目标Fragment的标签
     * @return 是否能切换
     */
    boolean canTab(String tabId);

    /**
     * 当切换被拦截时调用
     * @param tabId 被拦截的Fragment的标签
     */
    void onTabIntercepted(String tabId);
}
```

InterceptedFragmentTabHost的用法与FragmentTabHost基本完全一致, 唯一不同的就是多了setTabChangeInterceptor方法. 由于FragmentTabHost需要我们提供每个标签对应的tab, 因此我们可以声明一个String数组:

```
String[] tabTags = new String[] { getString(R.string.tab_test),
                getString(R.string.tab_mood), getString(R.string.tab_add_mood),
                getString(R.string.tab_pm), getString(R.string.tab_me) };
```

有了tabTags, 我们就可以跟据TabChangeInterceptor中传来的tabId来判断用户点击的是哪个标签了:

```
interceptedFragmentTabHost.setTabChangeInterceptor(new TabChangeInterceptor() {
            @Override 
            public boolean canTab(String tabId) {
                return !tabId.equals(tabTags[2]); // 判断点击的是否是第三个标签
            }

            @Override 
            public void onTabIntercepted(String tabId) {
                toAddMood(); // 跳转Activity
            }
        });
```

TabChangeInterceptor的用法很简单, 每当用户点击一个非当前标签的标签时, InterceptedFragmentTabHost都会根据canTab方法的返回值来判断是否能做切换操作. 如果返回true, 意味着能切换, 返回false则意味不能切换. `!tabId.equals(tabTags[2])`这行代码意味着只要点击的不是第三个标签, canTab都会返回true, 如果是第三个, 则返回false. 一旦canTab返回false, InterceptedFragmentTabHost会紧接着调用onTabIntercepted方法, 我们可以在此方法中做一些其他的事情, 如跳转Activity.

## TabNavigator

使用FragmentTabHost虽然不需要控制点击事件以及Fragment的切换, 但是还是需要做一些Tab页设置等工作. 这些逻辑也可以抽出一个公共类 - TabNavigator. TabNavigator只需实现一个接口, 加上一行代码, 就可以配置好FragmentTabHost:

```
    /**
     * Fragment tab页切换需实现此接口, 来获取tab页的必要信息
     */
    public interface TabNavigatorContent {
        /**
         * 根据position获取每个tab标签视图
         * @param position tab标签位置
         * @return tab标签视图
         */
        View getTabView(int position);

        /**
         * 根据position获取切换至目标Fragment要传递的数据Bundle
         * @param position 目标Fragment位置
         * @return 数据Bundle
         */
        Bundle getArgs(int position);

        /**
         * 获取Fragment的类对象数组
         * @return Fragment的类对象数组
         */
        Class[] getFragmentClasses();

        /**
         * 获取每个Fragment的tag
         * @return Fragment的tag数组
         */
        String[] getTabTags();
    }
```

```
public void setup(Context context, InterceptedFragmentTabHost tabHost, TabNavigatorContent content,
                      FragmentManager manager, int containerId)
```

只要我们的Activity实现了TabNavigatorContent接口, 就可以通过下面的代码来配置FragmentTabHost:

```
private TabNavigator navigator = new TabNavigator();
navigator.setup(context, tabHost, content, getSupportFragmentManager(), R.id.real_tab_content);
```

## 实现

下面我们来看看具体如何使用TabNavigator加InterceptedFragmentTabHost来实现前面提到的UI效果图.

### Activity布局

首先标签导航一般是位于主页, 因此我们来看看MainActivity的布局:

```
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/real_tab_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="@dimen/tab_content_height" />


    <com.corelibs.views.tab.InterceptedFragmentTabHost
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_alignParentBottom="true"
        android:layout_height="@dimen/tab_height" />

</RelativeLayout>
```

代码中只有两个控件, 一个FrameLayout, 以及一个InterceptedFragmentTabHost, 两个控件被RelativeLayout包裹. 接着看看dimen里的度量单位:

```
<dimen name="tab_height">75dp</dimen>
<dimen name="tab_content_height">55dp</dimen>
```

为什么是75和55dp呢? 可以看看使用钛合金狗眼测量的图:

![测量](/files/-LsLm89sM16Tp77FfRQY)

InterceptedFragmentTabHost的高度就是整个底部标签栏的高度, tab\_content\_height则是每个标签内容的具体高度. 两个高度不一致是由于加号按钮比标签要高. 又因为Fragment里的内容需要显示在标签内容上, 加号按钮下, 所以才使用RelativeLayout包裹, 并且FrameLayout的layout\_marginBottom为tab\_content\_height. FrameLayout就是承载Fragment的容器.

### 标签布局

除了加号按钮, 其他四个布局都类似, 因此我们可以使用同一个布局:

```
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="@dimen/tab_height"
    android:gravity="bottom">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/tab_content_height"
        android:orientation="vertical"
        android:gravity="center"
        android:background="@color/tab_bg">

        <ImageView
            android:id="@+id/iv_tab_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/tab_test_icon"
            android:contentDescription="@null"
            android:layout_marginBottom="3dp"/>

        <TextView
            android:id="@+id/tv_tab_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textColor="@drawable/tab_text_color"
            android:text="@string/tab_test"/>

    </LinearLayout>

</LinearLayout>
```

整个根布局实际上也是75dp, 只是实际上的标签内容是55dp, 并且显示在下方. 效果图:

![效果图](/files/-LsLm89um2lrgGORDSQB)

接着来画加号按钮:

```
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="@dimen/tab_height">

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/tab_content_height"
        android:background="@color/tab_bg"
        android:layout_alignParentBottom="true"/>

    <ImageView
        android:layout_width="@dimen/tab_height"
        android:layout_height="@dimen/tab_height"
        android:src="@mipmap/tab_add_mood"
        android:contentDescription="@null"
        android:padding="6dp"
        android:background="@drawable/tab_add_bg"
        android:layout_centerInParent="true"/>

</RelativeLayout>
```

与标签布局类似, 根布局的高度也是75dp, 底部有个55dp的view, 背景色与标签布局的背景色一样, 这样绿色背景就能无缝连接起来. ImageView则是居中显示, 宽高都是75dp, 但是留有6dp的padding, 用于显示绿色的圆环. 圆环则通过drawable背景来实现:

```
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/tab_bg" />
</shape>
```

![效果图](/files/-LsLm89wM0W4jYgdjAJb)

### Java代码

接下来看看MainActivity的代码. 有了布局, 我们还需要一个包含标签tag的String数组, 一个包含Fragment的Class的数组, 以及标签内icon的资源id的int数组:

```
String[] tabTags = new String[] { getString(R.string.tab_test),
                getString(R.string.tab_mood), getString(R.string.tab_add_mood),
                getString(R.string.tab_pm), getString(R.string.tab_me) };

Class[] classes = new Class[] { TestFragment.class,
                MoodFragment.class, AddMoodFragment.class,
                PedometerFragment.class, MeFragment.class }

int[] imageResIds = new int[] { R.drawable.tab_test_icon, R.drawable.tab_mood_icon, 0,
            R.drawable.tab_pm_icon, R.drawable.tab_me_icon };
```

每个imageResId都是使用的Selector. 因为每个标签被选中之后, 所有的View都会被设置成selected状态:

```
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true" android:drawable="@mipmap/tab_test_selected"/>
    <item android:drawable="@mipmap/tab_test"/>
</selector>
```

有了上述这些信息, 我们就可以去实现TabNavigator的TabNavigatorContent接口了:

```
    public static final int ADD_MOOD_POSITION = 2;

    @Override
    public View getTabView(int position) {
        View view;
        if (position == ADD_MOOD_POSITION) {
            // 加号按钮布局
            view = getLayoutInflater().inflate(R.layout.view_tab_add, null);
            return view;
        } else {
            // 普通标签布局
            view = getLayoutInflater().inflate(R.layout.view_tab_content, null);
        }
        ImageView iv = (ImageView) view.findViewById(R.id.iv_tab_icon);
        TextView tv = (TextView) view.findViewById(R.id.tv_tab_text);

        iv.setImageResource(imageResIds[position]); // 设置每个标签的icon
        tv.setText(tabTags[position]); // 设置每个标签的文字
        return view;
    }

    @Override
    public Bundle getArgs(int position) {
        return null; // 因为每个Fragment切换无需传递数据, 因此此处为null
    }

    @Override
    public Class[] getFragmentClasses() {
        return classes;
    }

    @Override
    public String[] getTabTags() {
        return tabTags;
    }
```

接着看看与TabNavigator与InterceptedFragmentTabHost有关的代码. InterceptedFragmentTabHost我们通过ButterKnife bind出来, 而TabNavigator我们可以直接new出来:

```
 @Bind(android.R.id.tabhost) InterceptedFragmentTabHost tabHost;
 private TabNavigator navigator = new TabNavigator();

 protected void init(Bundle savedInstanceState) {
    navigator.setup(this, tabHost, this, getSupportFragmentManager(), R.id.real_tab_content);
    navigator.setTabChangeInterceptor(new TabChangeInterceptor() {
        @Override 
        public boolean canTab(String tabId) {
            return !tabId.equals(tabTags[ADD_MOOD_POSITION]);
        }

        @Override 
        public void onTabIntercepted(String tabId) {
            toAddMood();
        }
    });
}
```

由于MainActivity实现了TabNavigatorContent接口, 所以TabNavigator的setup方法的第三个参数传来this. 到此, 整个标签导航就完成了. 至于每个标签页的具体内容, 则分别交给TestFragment.class, MoodFragment.class, AddMoodFragment.class, PedometerFragment.class, MeFragment.class这几个类去处理. 此处还需要注意的是, 如果一旦tabTags, classes等几个数组的长度不一样, TabNavigator则会根据classes的长度来生成标签.

看看完整的MainActivity代码:

```
public class MainActivity extends BaseActivity implements TabNavigator.TabNavigatorContent {

    public static final int ADD_MOOD_POSITION = 2;

    @Bind(android.R.id.tabhost) InterceptedFragmentTabHost tabHost;

    private TabNavigator navigator = new TabNavigator();
    private String[] tabTags;
    private int[] imageResIds = new int[] { R.drawable.tab_test_icon, R.drawable.tab_mood_icon, 0,
            R.drawable.tab_pm_icon, R.drawable.tab_me_icon };

    public static Intent getLaunchIntent(Context context) {
        return new Intent(context, MainActivity.class);
    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    protected void init(Bundle savedInstanceState) {
        tabTags = new String[] { getString(R.string.tab_test),
                getString(R.string.tab_mood), getString(R.string.tab_add_mood),
                getString(R.string.tab_pm), getString(R.string.tab_me) };

        navigator.setup(this, tabHost, this, getSupportFragmentManager(), R.id.real_tab_content);
        navigator.setTabChangeInterceptor(new TabChangeInterceptor() {
            @Override 
            public boolean canTab(String tabId) {
                return !tabId.equals(tabTags[ADD_MOOD_POSITION]);
            }

            @Override 
            public void onTabIntercepted(String tabId) {
                toAddMood();
            }
        });
    }

    @Override
    protected BasePresenter createPresenter() {
        return null;
    }

    @Override
    public View getTabView(int position) {
        View view;
        if (position == ADD_MOOD_POSITION) {
            view = getLayoutInflater().inflate(R.layout.view_tab_add, null);
            return view;
        } else {
            view = getLayoutInflater().inflate(R.layout.view_tab_content, null);
        }
        ImageView iv = (ImageView) view.findViewById(R.id.iv_tab_icon);
        TextView tv = (TextView) view.findViewById(R.id.tv_tab_text);

        iv.setImageResource(imageResIds[position]);
        tv.setText(tabTags[position]);
        return view;
    }

    @Override
    public Bundle getArgs(int position) {
        return null;
    }

    @Override
    public Class[] getFragmentClasses() {
        return new Class[] { TestFragment.class,
                MoodFragment.class, AddMoodFragment.class,
                PedometerFragment.class, MeFragment.class };
    }

    @Override
    public String[] getTabTags() {
        return tabTags;
    }

    private void toAddMood() {
        startActivity(AddMoodActivity.getLaunchIntent(MainActivity.this));
    }
}
```

最终效果:

![最终效果](/files/-LsLm89yfPc5Tvkg8Pf6)

## 关键代码

### InterceptedFragmentTabHost

```
    @Override
    public void onTabChanged(String tabId) {
        if (mAttached) {
            // 判断canTab的返回值
            if (mInterceptor != null && !mInterceptor.canTab(tabId)) {
                // 拦截
                // 将CurrentTab重置成上一次的值
                setCurrentTabByTag(mLastTab.tag);
                mInterceptor.onTabIntercepted(tabId);
            } else {
                // 切换
                FragmentTransaction ft = doTabChanged(tabId, null);
                if (ft != null) {
                    ft.commitAllowingStateLoss();
                }
                if (mOnTabChangeListener != null) {
                    mOnTabChangeListener.onTabChanged(tabId);
                }
            }
        }
    }
```

### TabNavigator

```
private TabNavigatorContent content;
private InterceptedFragmentTabHost host;

private void init() {
    Class[] fragmentClasses = content.getFragmentClasses();
    String[] tabTags = content.getTabTags();

    int tabCount = fragmentClasses.length;

    for (int i = 0; i < tabCount; i++) {
        host.addTab(host.newTabSpec(tabTags[i]).setIndicator(content.getTabView(i)),
                fragmentClasses[i], content.getArgs(i));
    }
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryan-8.gitbook.io/android-architecture-journey/widgets/tab.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
