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

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

上述效果图在严格意义上来说不算是标签导航, 但是从开发来看, 实现的方式类似. 以下是具体需求:
有四个标签, 分别是心理测试, 心情圈, 健步走以及个人中心, 每个标签分别在当前页面内切换标签页. 点击中间的加号按钮则需要跳转到另一个页面, 当前页面则继续显示之前的标签页.
首先我们来分析一下重点:
中间加号的UI效果实现
点击加号跳转新的Activity, 并保持当前的标签页
如果没有中间的加号, 使用FragmentTabHost就能实现, 具体用法可以参考Android常用控件之FragmentTabHost的使用. 但是有了中间的加号FragmentTabHost就力不从心了, 因为FragmentTabHost的每一个标签都要对应一个Fragment, 不能对应Activity, 也无法对点击事件做拦截. 通过查看源码发现OnTabChangeListener也是在标签切换完成之后才会回调. 因此FragmentTabHost无法完成上述需求. 怎么办呢? 引入InterceptedFragmentTabHost.
InterceptedFragmentTabHost
InterceptedFragmentTabHost是CoreLibs中对FragmentTabHost的扩展, 实现了tab切换拦截功能. 一旦多了拦截功能, 就能实现上述需求了. 比如FragmentTabHost中实际上是有5个标签, 包含了中间的加号. 我可以通过设置拦截监听器, 检测一旦用户点击了第三个标签, 就不进行切换操作, 而是跳转至一个新的Activity.
由于通过继承FragmentTabHost没法很好的实现拦截功能, 并且FragmentTabHost内部只是引用了几个Android内部的id资源, 没有其他内部资源, 因此我们完全可以将FragmentTabHost的源码复制出来放到自己的项目中, 而不会出现编译不通过的后果. InterceptedFragmentTabHost就是通过这种方式对FragmentTabHost扩展.
接下来看看如何使用InterceptedFragmentTabHost. InterceptedFragmentTabHost中提供了如下方法来设置拦截监听器:
以下是TabChangeInterceptor接口的代码:
InterceptedFragmentTabHost的用法与FragmentTabHost基本完全一致, 唯一不同的就是多了setTabChangeInterceptor方法. 由于FragmentTabHost需要我们提供每个标签对应的tab, 因此我们可以声明一个String数组:
有了tabTags, 我们就可以跟据TabChangeInterceptor中传来的tabId来判断用户点击的是哪个标签了:
TabChangeInterceptor的用法很简单, 每当用户点击一个非当前标签的标签时, InterceptedFragmentTabHost都会根据canTab方法的返回值来判断是否能做切换操作. 如果返回true, 意味着能切换, 返回false则意味不能切换. !tabId.equals(tabTags[2])这行代码意味着只要点击的不是第三个标签, canTab都会返回true, 如果是第三个, 则返回false. 一旦canTab返回false, InterceptedFragmentTabHost会紧接着调用onTabIntercepted方法, 我们可以在此方法中做一些其他的事情, 如跳转Activity.
TabNavigator
使用FragmentTabHost虽然不需要控制点击事件以及Fragment的切换, 但是还是需要做一些Tab页设置等工作. 这些逻辑也可以抽出一个公共类 - TabNavigator. TabNavigator只需实现一个接口, 加上一行代码, 就可以配置好FragmentTabHost:
只要我们的Activity实现了TabNavigatorContent接口, 就可以通过下面的代码来配置FragmentTabHost:
实现
下面我们来看看具体如何使用TabNavigator加InterceptedFragmentTabHost来实现前面提到的UI效果图.
Activity布局
首先标签导航一般是位于主页, 因此我们来看看MainActivity的布局:
代码中只有两个控件, 一个FrameLayout, 以及一个InterceptedFragmentTabHost, 两个控件被RelativeLayout包裹. 接着看看dimen里的度量单位:
为什么是75和55dp呢? 可以看看使用钛合金狗眼测量的图:

InterceptedFragmentTabHost的高度就是整个底部标签栏的高度, tab_content_height则是每个标签内容的具体高度. 两个高度不一致是由于加号按钮比标签要高. 又因为Fragment里的内容需要显示在标签内容上, 加号按钮下, 所以才使用RelativeLayout包裹, 并且FrameLayout的layout_marginBottom为tab_content_height. FrameLayout就是承载Fragment的容器.
标签布局
除了加号按钮, 其他四个布局都类似, 因此我们可以使用同一个布局:
整个根布局实际上也是75dp, 只是实际上的标签内容是55dp, 并且显示在下方. 效果图:

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

Java代码
接下来看看MainActivity的代码. 有了布局, 我们还需要一个包含标签tag的String数组, 一个包含Fragment的Class的数组, 以及标签内icon的资源id的int数组:
每个imageResId都是使用的Selector. 因为每个标签被选中之后, 所有的View都会被设置成selected状态:
有了上述这些信息, 我们就可以去实现TabNavigator的TabNavigatorContent接口了:
接着看看与TabNavigator与InterceptedFragmentTabHost有关的代码. InterceptedFragmentTabHost我们通过ButterKnife bind出来, 而TabNavigator我们可以直接new出来:
由于MainActivity实现了TabNavigatorContent接口, 所以TabNavigator的setup方法的第三个参数传来this. 到此, 整个标签导航就完成了. 至于每个标签页的具体内容, 则分别交给TestFragment.class, MoodFragment.class, AddMoodFragment.class, PedometerFragment.class, MeFragment.class这几个类去处理. 此处还需要注意的是, 如果一旦tabTags, classes等几个数组的长度不一样, TabNavigator则会根据classes的长度来生成标签.
看看完整的MainActivity代码:
最终效果:

关键代码
InterceptedFragmentTabHost
TabNavigator
Last updated
Was this helpful?