2

1、介绍和准备

我们在使用手机App时不难会看到这样的页面上面是一组起导航作用的标签,点击标签就会切换到相应的页面;在不同的页面中滑动时,标签的样式(文字大小或者颜色)也会发生变化。这样你任何时候都能一眼看出自己停留在哪个页面。这个布局出镜率实在太高了,我甚至敢说每个学Android的人都写过这样的布局(下面就是知乎中的页面)。

好了,废话少说,我们照例先来分析一下这个布局的组成。标签下面的页面比较容易想到:整体是一个左右滑动的ViewPager,每一页则可以用Fragment填充,也就是ViewPager+Fragment。但上面的标签部分就有点头大了,之前我们都是使用第三方的项目(如PagerSlidingTabStrip),高手的话也可以自定义一个控件。但是这样并非长久之计,所以谷歌后来人性化地推出了自家的标签控件TabLayout(注意可不要跟TableLayout搞混了,后者是Android的基本布局之一,而前者是一个控件)。TabLayout顾名思义就是包含Tab的布局,它包含在Design support library库中,要使用它,你需要在先添加依赖库:
这里写图片描述

我导入的是最新的26.0.0版本:

compile 'com.android.support:design:26.0.0-alpha1'

准备完这些,我们可以开始写代码了。

2、初步实现

之前在知乎上看到有人对微信的设计改动:将使用频率高的朋友圈、消息提醒和公众号这三个功能独立出来放在首页。我很赞同这样的设计思路,所以今天就来弄一个简陋版的吧。大体效果如下:
这里写图片描述

2.1 页面布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.lindroid.tablayoutdemo.MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tayLayout"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        app:tabIndicatorColor="#49dd12"
        app:tabSelectedTextColor="#49dd12"
        app:tabTextColor="@android:color/darker_gray"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </android.support.v4.view.ViewPager>
</LinearLayout>

上面的TabLayout的高度固定为60dp,然后让ViewPager占据剩余的空间即可。现在我来介绍一下用到的TabLayout的属性:

  • app:tabIndicatorColor:标签下面移动的横线的颜色。
  • app:tabTextColor:标签文字的颜色
  • app:tabSelectedTextColor :标签被选中后的文字颜色

TabLayout还有很多其他的属性,比如你要是不想要下面的移动横线的话,可以调用属性app:tabIndicatorHeight ,将高度设置为0dp即可。关于TabLayout的其他属性,大家可以看看这篇博客,动手练习一下:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0731/3247.html

2.2 创建Viewpager页面(Fragment)

为了能够识别我们切换到的是ViewPager的哪个页面,我们在Fragment中创建一个带参数的构造函数,动态添加一个TextView,它的文本内容跟标签的一致就好。

public class TabFragment extends Fragment {
    private Context context;
    private String content; //Fragment的显示内容
    public TabFragment() {

    }

    public TabFragment(Context context,String content){
        this.context = context;
        this.content = content;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        TextView textView = new TextView(context);
        textView.setText(content);
        textView.setTextSize(30);
        textView.setGravity(Gravity.CENTER);
        return textView;
    }
}

2.3 MainActivity代码

先来看代码:

public class MainActivity extends AppCompatActivity {
    private ViewPager viewPager;
    private TabLayout tabLayout;
    private List<Fragment> fragments = new ArrayList<>();
    private List<String> tabs = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
    }

    private void initData() {
        tabs.add("新消息");
        tabs.add("朋友圈");
        tabs.add("公众号");
        fragments.add(new TabFragment(this,tabs.get(0)));
        fragments.add(new TabFragment(this,tabs.get(1)));
        fragments.add(new TabFragment(this,tabs.get(2)));
    }

    private void initView() {
        tabLayout = (TabLayout) findViewById(R.id.tayLayout);
        viewPager = (ViewPager) findViewById(R.id.viewPager);
        //设置TabLayout的模式
        tabLayout.setTabMode(TabLayout.MODE_FIXED);
        viewPager.setAdapter(new TabAdapter(getSupportFragmentManager()));
        //关联ViewPager和TabLayout
        tabLayout.setupWithViewPager(viewPager);
    }

    class TabAdapter extends FragmentPagerAdapter{
        public TabAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return fragments.get(position);
        }

        @Override
        public int getCount() {
            return fragments.size();
        }
        
        //显示标签上的文字
        @Override
        public CharSequence getPageTitle(int position) {
            return tabs.get(position);
        }
    }
}

代码不长,initData方法中添加数据,initView方法初始化控件,跟我们平时使用ViewPager的写法差别不大。要注意的是TabLayout需要设置模式(即setTabMode方法),一共有两种:

  • TabLayout.MODE_FIXED :当Tab较少,且占满整个屏幕时可以使用这种模式;
  • TabLayout.MODE_SCROLLABLE :当Tab数量较多,屏幕宽度不够时使用该模式,整个TabLayout是可以左右滑动的。

除此之外,我们需要让Tab显示文字,要重写FragmentPagerAdapter的getPageTitle方法,返回每一个Tab的文字内容。最后可别忘了最关键的一步:使用setupWithViewPager方法关联Viewpager和TabLayout,这样两者才会联动。

运行一下,就可以看到动态图中的效果了。

3、进阶

3.1 修改标签字体大小

默认的Tab字体大小有点小,看起来不太舒服,当我们去修改字体大小时却发现,TabLayout居然没有提供跟TextSize相关的属性。不过不用急,TabLayout其实提供了一个更灵活的属性app:tabTextAppearance ,它可以修改字体的样式,从而间接修改字体的大小。

我们在style.xml中自定义一个样式,继承于TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse ,在属性android:textSize中设置我们想要的字体大小,这里我设为20sp。

    <style name="TabTextSize" parent="TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse">
        <item name="android:textSize">20sp</item>
    </style>  

接下来在布局文件中使用就可以了。

运行一下,这下看起来清楚多了。

这里写图片描述

3.2 添加分割线

TabLayout的标签之间是默认没有分割线的,如果我们想添加分割线,让标签之间更有层次感的话,可以添加以下的代码:

        //设置分割线
        LinearLayout linearLayout = (LinearLayout) tabLayout.getChildAt(0);
        linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
        linearLayout.setDividerDrawable(ContextCompat.getDrawable(this,
                R.drawable.divider)); //设置分割线的样式
        linearLayout.setDividerPadding(dip2px(10)); //设置分割线间隔

自定义的分割线样式:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#80c0c0c0" />
    <size android:width="1dp" />
</shape>

setDividerPadding方法中输入的参数是单位是px,我们需要转换成像素:

    //像素单位转换
    public int dip2px(int dip) {
        float density = getResources().getDisplayMetrics().density;
        return (int) (dip * density + 0.5);
    }

3.2 显示信息数目

现在我们来尝试一下这样的效果:将tab的文字分为两行,第二行显示信息的数目,当然,我们并没有真的信息,所以直接输入一些假数据就可以。为了让文字变为两行,我们可以加入换行符。

        tabs.add("新消息"+"\n"+999);
        tabs.add("朋友圈"+"\n"+99);
        tabs.add("公众号"+"\n"+9);

运行,发现文字确实分成了两行。但是等等,怎么文字大小小了那么多?
这里写图片描述

如果你查一下TabLayout的源码,就会发现这一切早就命中注定了。源码中有这么一段:

               if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
                    // If the icon view is being displayed, we limit the text to 1 line
                    maxLines = 1;
                } else if (mTextView != null && mTextView.getLineCount() > 1) {
                    // Otherwise when we have text which wraps we reduce the text size
                    textSize = mTabTextMultiLineSize;
                }

这里是设置Tab的icon和字体大小的,在else if代码块中,我们发现了,当TextView的文本大于一行时,就会强制使用特定的字体(textSize = mTabTextMultiLineSize),这就解释了为什么我们的字体设置不奏效了。

那么,我们该怎么办呢?

3.3 自定义标签

条条大路通罗马,TabLayout早就给我们准备了另一条路了,那就是自定义标签布局。(温馨提示:下面的代码对之前的改动较大,大家可能会觉得之前做的都是无用功,但是凡事总是循序渐进的,请不必灰心。)

自定义标签布局

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

    <TextView
        android:id="@+id/tv_tab_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="标签名"
        android:textColor="@drawable/tab_color"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_tab_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="99"
        android:textColor="@drawable/tab_color"
        android:textSize="16sp" />
</LinearLayout>

这里我们用到了一个选择器,代码如下:

标签颜色选择器

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true" android:color="#49dd12"/>
    <item android:state_selected="false" android:color="@android:color/darker_gray"/>
</selector>

在代码中实现

    /**
     * 设置Tab的样式
     */
        private void setTabView() {
        holder = null;
        for (int i = 0; i < tabs.size(); i++) {
            //依次获取标签
            TabLayout.Tab tab = tabLayout.getTabAt(i);
            //为每个标签设置布局
            tab.setCustomView(R.layout.tab_item);
            holder = new ViewHolder(tab.getCustomView());
            //为标签填充数据
            holder.tvTabName.setText(tabs.get(i));
            holder.tvTabNumber.setText(String.valueOf(tabNumbers.get(i)));
            //默认选择第一项
            if (i == 0){
                holder.tvTabName.setSelected(true);
                holder.tvTabNumber.setSelected(true);
                holder.tvTabName.setTextSize(18);
                holder.tvTabNumber.setTextSize(18);
            }
        }

        //tab选中的监听事件
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                holder = new ViewHolder(tab.getCustomView());
                holder.tvTabName.setSelected(true);
                holder.tvTabNumber.setSelected(true);
                //选中后字体变大
                holder.tvTabName.setTextSize(18);
                holder.tvTabNumber.setTextSize(18);
                //让Viewpager跟随TabLayout的标签切换
                viewPager.setCurrentItem(tab.getPosition());

            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                holder = new ViewHolder(tab.getCustomView());
                holder.tvTabName.setSelected(false);
                holder.tvTabNumber.setSelected(false);
                //恢复为默认字体大小
                holder.tvTabName.setTextSize(16);
                holder.tvTabNumber.setTextSize(16);
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });
    }

    class ViewHolder{
        TextView tvTabName;
        TextView tvTabNumber;

        public ViewHolder(View tabView) {
            tvTabName = (TextView) tabView.findViewById(R.id.tv_tab_name);
            tvTabNumber = (TextView) tabView.findViewById(R.id.tv_tab_number);
        }
    }

创建一个setTabView方法来设置Tab的样式,在for循环中为每一个标签创建布局。setCustomView方法可以设置Tab的布局,getCustomView则可以获取当前Tab的布局。

我们既然使用的是自定义的布局,那么选中时的样式也要手动设置了。跟ViewPager类似,TabLayout也有自己的选中监听事件(addOnTabSelectedListener)。在标签被选中时将状态设置为选中,并切换到相应的ViewPager页面,未选中的页面则将选中状态设为false即可。

补充一点,选中Tab后字体变大这一功能是我后面加上去的,所以代码只在GitHub中更新了。由于属性android:textSize 不支持drawable文件,所以这里不能用状态选择器,但好在代码里实现也不复杂,就不必过多解释了。

完成效果图

4、开拓思维

既然TabLayout如此贴心地给我们提供了自定义标签布局的方法,那么我们就要好好利用它,比如除了文字之外,我们还可以添加图片,让标签页的内容更加丰富。另外,TabLayout不一定非要放在顶部,也可以放在底部,去掉下划线之后就可以实现与RadioGroup一样的效果。

最后,是说好的源码了:
CSDN
GitHub


lindroid
214 声望13 粉丝

Android中级魔法师