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

作者: 尹star | 来源:发表于2016-04-13 12:41 被阅读5064次

可怕的用户习惯

目前市面上很多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

相关文章

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

    可怕的用户习惯 目前市面上很多App都采用底部一个Tab栏,管理四到五个Tab,然后选择切换页面的方式的设计,这虽...

  • Google官方的底部Tab栏设计规范

    上一篇《仿微信底部Tab栏》中粗略的讲了下底部Tab栏的封装,不少同学在实际运用中发现了一些问题,比如我demo中...

  • Android 仿微信底部渐变Tab(2)

    之前写过一篇仿微信底部渐变Tab的文章:Android 仿微信底部渐变Tab 是根据ImageView的tint属...

  • 微信小程序初学之底部导航栏

    在App.json中进行全局配置 在微信小程序开发中底部导航栏有专门的控件TabBar来进行显示底部导航栏 Tab...

  • Android 仿微信底部渐变Tab

    先来看一下效果图 除了第三个的发现Tab有所差别外,其他的基本还原了微信的底部Tab渐变效果 每个Tab都是一个自...

  • 小程序tab组件封装

    微信小程序tab组件封装 最近在做微信小程序的项目,下面就微信小程序中tab的tab功能封装成一个组件,在项目项需...

  • Android 底部导航栏(底部Tab)最佳实践

    当开始一个新项目的时候,有一个很重要的步骤就是确定我们的APP首页框架,也就是用户从桌面点击APP 图标,进入AP...

  • 在APP中,Tab bar是否要固定

    Tab bar即我们手机app中的一级导航栏,打开你的微信首页,便可以看到底部那条每天被你点击无数次的导航栏,那么...

  • Android 仿微信底部渐变颜色导航栏

    运行效果图 完整项目地址 Github:仿微信底部渐变导航栏 顶部标题栏地址 简书:顶部标题栏 自定义View 自...

  • 小程序底部导航栏

    创建微信小程序的底部导航栏,需要用到“tabBar”tabBar 是一个数组,只能配置最少2个、最多5个 tab,...

网友评论

  • 柯原哀:博主我想问下,adapter那部分返回的是BasePageFragment,报错说是需要Fragment,是我哪里搞错了吗?
  • FrankDaddy:这样是不是一开始fragment没有初始化的呀,MVP结构会报P层不存在
  • 松小白:您好,如果用tablayout实现底部导航,请问如何实现activity和fargment传递数据呢?
    尹star: @松小白 接口callback,eventbus都可以
  • HappyPieBinLiu:正好要写这一块。3Q
  • fendo:赞一个!!
  • 程序员徐公:学习了另外一种思路实现
  • 捡淑:马克
  • 陆地蛟龙:封装的不错。么么哒。
  • hackware:GifCam比较好用
  • hackware:试试用我的MagicIndicator来做这个,😊,分分钟搞定
  • ElonYanJ:为什么 JDK已经出到1.8 不用关心反射了,1.8有什么重大优化吗,还是这些版本以来反射的性能已经提升到不用担心的地步了
    尹star:@JohnnyYanE 语言本身也在进步,性能损耗可以忽略不计
  • Karma1026:怎么样才可以做到思路如此清晰?
  • davidtps:棒棒哒
  • gzfgeh:官方TabLayout 更好一点

本文标题:封装实践——仿微信底部Tab栏

本文链接:https://www.haomeiwen.com/subject/auqulttx.html