封装实践——仿微信底部Tab栏

可怕的用户习惯

目前市面上很多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恤改成底裤穿的感觉总是怪怪的,所以那要不然,我们还是自己造个轮子吧。


2.jpg

化整为零

基于以上需求和分析,可以开工编码了。我们还是以微信为例吧,假设底部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) {

    }
}
3.jpg

添加点击事件

但是我们还没有加点击事件,重点来了,我又不想去做一大堆判断,除了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();
    }
}

device-2016-04-08-002202.gif

滑动切换

我们只用两行代码就完成了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;
        }
        }
    }
device-2016-04-11-095800.gif

自此一个模仿微信的底部Tab栏的封装基本实现了。没找到比较好的gif录制软件,所以看起来怪怪的。

本文首发:CSDN
次发:简书
有需要代码的点这里:GitHub

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容