可怕的用户习惯
目前市面上很多App都采用底部一个Tab栏,管理四到五个Tab,然后选择切换页面的方式的设计,这虽然不太符合material design,但却是一个不容易出错而又符合国人使用习惯的设计方式。用户习惯是个可怕的东西,早在4.0之前,Android几乎无UI设计可言,于是乎各种仿IOS设计大行其道,久而久之用户也就习惯于斯。而Android真正推出material design时,用户反而不习惯。今天要封装的这种底部Tab栏的展现方式,微信,支付宝,网易新闻,简书等都采用这种设计。而所谓封装一定是基于某种确定的业务需求,所以针对上述这种常见的设计方式,我们可以做一个比较通用的封装。
为什么要做封装
你可能会觉得,这就是一个选择切换嘛,我只要做些if else判断就好了。但是Tab栏一般用在首页,纷繁芜杂的业务逻辑和庞大代码量就不用说了,如果这时候不想被各种if else , swich case 搞得心力交瘁,那么我们少写些冗余代码又有何妨。毕竟代码不止眼前的苟且,还有设计改版和需求变更,某天产品经理更你说要改版,修改完xml布局,再去修改if else判断,然后再去修改click事件。。。想想也是醉了。所以这里要说的封装当然不会是,一个LinearLayout塞几个布局,然后做swich case去切换fragment,我希望布局里只需要include一个view,代码里也不需要N多findviewbyId,更不想添加各种if else 判断,就能实现上述需求。
官方的TabLayout
官方也有一个TabLayout,在android.widget包里。既然官方都有了,为什么还要重复造轮子呢。仔细看看官方源码和使用说明,这个TabLayout建议使用在顶部,配合Viewpager使用,甚至还可以左右滑动。就像当初这版不太被用户接受的微信一样(如下图),tab栏放在顶部。当然官方这个TabLayout非要放在底部,重写下样式布局,自己改造下也能满足底部Tab栏的需求,但是T恤改成底裤穿的感觉总是怪怪的,所以那要不然,我们还是自己造个轮子吧。

化整为零
基于以上需求和分析,可以开工编码了。我们还是以微信为例吧,假设底部Tab栏共有四个按钮,上面icon,下面文本。那么我们先把这一样式的xml写出来,我这里先用merge标签,原因不说了。
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/tab_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tab_lable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</merge>
上面是四个Tab按钮的通用布局,上面一个icon,下面是文字,非常简单。我们还需要写个TabView来解析这个布局。
public class TabView extends LinearLayout implements View.OnClickListener{
private ImageView mTabImage;
private TextView mTabLable;
public TabView(Context context) {
super(context);
initView(context);
}
public TabView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public TabView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
private void initView(Context context){
setOrientation(VERTICAL);
setGravity(Gravity.CENTER);
LayoutInflater.from(context).inflate(R.layout.tab_view,this,true);
mTabImage=(ImageView)findViewById(R.id.tab_image);
mTabLable=(TextView)findViewById(R.id.tab_lable);
}
public void initData(TabItem tabItem){
mTabImage.setImageResource(tabItem.imageResId);
mTabLable.setText(tabItem.lableResId);
}
@Override
public void onClick(View v) {
}
}
化零为整
到这里我们已经完成了单个TabView按钮的解析,但是我们现在有四个按钮,要在xml里include四次嘛,要在代码里findviewById四次嘛,对于这样的hard code我是拒绝的,我希望在xml里只include一个view,代码里只findviewById一次,所以我们还需要给TabView再包一层,给四个Tab按钮一个父容器TabLayout,我们只需要include一个父容器,就能达到现在一片顶过去五片,一口气上五楼,不费劲的效果。我们把一个TabView看做是一个对象,需要几个就new几个,然后add到TabLayout里。所以首先我需要一个TabView的对象TabItem。
/**
* Created by yx on 16/4/3.
*/
public class TabItem {
/**
* icon
*/
public int imageResId;
/**
* 文本
*/
public int lableResId;
public TabItem(int imageResId, int lableResId) {
this.imageResId = imageResId;
this.lableResId = lableResId;
}
}
然后再写个父容器TabLayout,我们姑且也叫TabLayout吧。
public class TabLayout extends LinearLayout implements View.OnClickListener{
private ArrayList<TabItem> tabs;
private OnTabClickListener listener;
public TabLayout(Context context) {
super(context);
initView();
}
public TabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TabLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView(){
setOrientation(HORIZONTAL);
}
public void initData(ArrayList<TabItem>tabs,OnTabClickListener listener){
this.tabs=tabs;
this.listener=listener;
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
params.weight=1;
if(tabs!=null&&tabs.size()>0){
TabView mTabView=null;
for(int i=0;i< tabs.size();i++){
mTabView=new TabView(getContext());
mTabView.setTag(tabs.get(i));
mTabView.initData(tabs.get(i));
mTabView.setOnClickListener(this);
addView(mTabView,params);
}
}else{
throw new IllegalArgumentException("tabs can not be empty");
}
}
@Override
public void onClick(View v) {
listener.onTabClick((TabItem)v.getTag());
}
public interface OnTabClickListener{
void onTabClick(TabItem tabItem);
}
}
以上都是小学五年级水平的代码,所以我就不写注释了,也不需要做过多讲解,直接看代码。到这里我们基本完成了底部TabLayout代码的编写,那我们写个activity测试下效果先。
先把TabLayout include到布局中
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/tab_layout"
/>
<star.yx.tabview.TabLayout
android:id="@+id/tab_layout"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="50dp"
/>
</RelativeLayout>
是你代码写的丑,而不是产品狗故意让你下班不能走
这里TabLayout实际上是一个容器,底部需要几个Tab按钮,就在MainActiviy里new几个然后add到TabLayout即可。所以有一天产品经理跟你说需要增加一个按钮,只需要再new一个add进去就好,又有一天boss说把底部Tab栏顺序调整下呗,就只要调整下new出的TabView顺序即可。这种兵来将挡水来土掩的感觉真好,再也不怕需求改来改去了,下班时间好像可以提前了呢。
public class MainActivity extends ActionBarActivity implements TabLayout.OnTabClickListener{
private TabLayout mTabLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView(){
mTabLayout=(TabLayout)findViewById(R.id.tab_layout);
}
private void initData(){
ArrayList<TabItem>tabs=new ArrayList<TabItem>();
tabs.add(new TabItem(R.drawable.selector_tab_msg,R.string.wechat));
tabs.add(new TabItem(R.drawable.selector_tab_contact,R.string.contacts));
tabs.add(new TabItem(R.drawable.selector_tab_moments,R.string.discover));
tabs.add(new TabItem(R.drawable.selector_tab_profile,R.string.me));
mTabLayout.initData(tabs, this);
}
@Override
public void onTabClick(TabItem tabItem) {
}
}

添加点击事件
但是我们还没有加点击事件,重点来了,我又不想去做一大堆判断,除了if else还有其他办法吗嘛?当然有啊!switch case啊!这不等于没说嘛!我可不可以在点击的时候动态的获取当前Fragment,这样就可以避免一大堆的判断了,所以我们可以考虑用反射,JDK已经出到1.8了,我们这里就不要在计较反射的性能问题了。那么我们先在TabItem中增加一个Fragment变量继承自BaseFragment,这个BaseFragment就是我在ViewPager+Fragment LazyLoad最优解中使用的BaseFragment。
public Class<? extends BaseFragment>tagFragmentClz;
然后构造函数里也加一个参数,先偷个懒姑且写在构造函数里。
public TabItem(int imageResId, int lableResId, Class<? extends BaseFragment> tagFragmentClz) {
this.imageResId = imageResId;
this.lableResId = lableResId;
this.tagFragmentClz = tagFragmentClz;
}
相应的MainActivity里的引用也要修改下,第三个参数就传入相应的Fragment。
ArrayList<TabItem>tabs=new ArrayList<TabItem>();
tabs.add(new TabItem(R.drawable.selector_tab_msg, R.string.wechat, WechatFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_contact, R.string.contacts, ContactsFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_moments, R.string.discover, DiscoverFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_profile, R.string.me, ProfileFragment.class));
然后点击事件的方法如下:
@Override
public void onTabClick(TabItem tabItem) {
try {
BaseFragment fragment= tabItem.tagFragmentClz.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.fragment,fragment).commitAllowingStateLoss();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

滑动切换
我们只用两行代码就完成了Fragment的切换,这里我先用replace()做切换,以后有机会再探讨replace()和Add(),hide()的区别,然后我们还需要再处理下按钮的选中状态。一个模仿微信的底部导航栏就初见雏形了,但是微信是可以滑动切换的,我们这个还不能滑动切换,所以我们还要对以上代码做些调整,毫无疑问这个时候viewpager要出场了。我们把MainActivity中之前的Framelayout替换成Viewpager。
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_above="@id/tab_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
还要写一个viewPager的适配器,这个时候我们选择把adapter写为内部类,这样会更方便一点动态获取Fragment。然后之前onTabClick()中通过反射获取Fragment的方法挪到adapter中的getItem()方法中,代码如下。
public class FragAdapter extends FragmentPagerAdapter {
public FragAdapter(FragmentManager fm) {
super(fm);
// TODO Auto-generated constructor stub
}
@Override
public Fragment getItem(int arg0) {
// TODO Auto-generated method stub
try {
return tabs.get(arg0).tagFragmentClz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return fragment;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return tabs.size();
}
}
切换状态改变
适配好viewpager后,滑动的时候我们还需要对title栏和底部Tab栏做相应的状态改变。这里viewPager只需要实现OnPageChangeListener接口,在onPageSelected(int position)方法中做相应的处理。我这里的title用了actionbar。
@Override
public void onPageSelected(int position) {
mTabLayout.setCurrentTab(position);
actionBar.setTitle(tabs.get(position).lableResId);
}
滑动的时候要改变状态,那相应的点击tab栏也要做类似操作。
@Override
public void onTabClick(TabItem tabItem) {
actionBar.setTitle(tabItem.lableResId);
mViewPager.setCurrentItem(tabs.indexOf(tabItem));
}
其中tabLayout中的setCurrentTab(int i)方法如下。我们声明两个变量,tabCount用来记录底部tabView的个数,selectView用来标识被选中的View。
public void setCurrentTab(int i) {
if (i < tabCount && i >= 0) {
View view = getChildAt(i);
if (selectView != view) {
view.setSelected(true);
if (selectView != null) {
selectView.setSelected(false);
}
selectView = view;
}
}
}

自此一个模仿微信的底部Tab栏的封装基本实现了。没找到比较好的gif录制软件,所以看起来怪怪的。
本文首发:CSDN
次发:简书
有需要代码的点这里:GitHub。
网友评论