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一样的效果。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。