什么是 TabLayout
TabLayout provides a horizontal layout to display tabs.
官方介绍,TabLayout 是一个横向标签显示的布局,效果就是现在很多新闻客户端的那种顶部标签展示效果,并支持指示器、 ViewPager 联动
简单使用
按照惯例,我们先看一下效果图:
从效果图来看,这是采用 TabLayout + ViewPager 滑动切换和点击标签切换的一个效果。TabLayout 支持横向滚动多标签设置,还可以支持指示器,支持与 ViewPager 进行联动。下面看看具体实现
- 引入 com.android.support:design
TabLayout 是属于 com.android.support:design 包的控件,所以需要依赖该包
compile 'com.android.support:design:26.1.0'
- xml 文件创建
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/colorPrimary"
app:tabIndicatorColor="#ffffff"
app:tabIndicatorHeight="3dp"
app:tabMode="scrollable"
app:tabPadding="2dp"
app:tabSelectedTextColor="#ffffff"
app:tabTextAppearance="@style/TabTextStyle"
app:tabTextColor="#333333"/>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
从 xml 布局文件看,根布局为一个垂直的线性布局,包含 TabLayout 和 ViewPager。重点看一下 TabLayout 的几个常用属性值
- app:tabBackground 标签布局的背景色
- app:tabIndicatorColor 指示器的颜色
- app:tabIndicatorHeight 指示器的高度(如果不需要指示器可以设置为0dp)
- app:tabMode 显示模式:默认 fixed(固定),scrollable(可横向滚动)
- app:tabPadding 标签内边距
- app:tabSelectedTextColor 标签选中的文本颜色
- app:tabTextAppearance 标签文本样式
- app:tabTextColor 标签未选中的文本颜色
tab_custom_view.xml
<?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="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/iv_tab_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/tab_iv_selector"/>
<TextView
android:id="@+id/tv_tab_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:text="tab"
android:textColor="@color/tab_tv_selector"/>
</LinearLayout>
tab_iv_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_add_black_24dp" android:state_selected="false"/>
<item android:drawable="@drawable/ic_add_white_24dp" android:state_selected="true"/>
</selector>
tab_tv_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/black" android:state_selected="false"/>
<item android:color="@android:color/white" android:state_selected="true"/>
</selector>
以上是一个简单的自定义标签布局文件,可以看到 ImageView 的 src 和 TextView 的 textColor 属性值都使用了 selector,这样写的话就不需要再在代码监听选中的 Tab 去改变状态了,非常方便。该 xml 会在下面的代码中应用
- Activity 中获取并设置相关属性值,以及事件监听
arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="tab">
<item>推荐</item>
<item>视频</item>
<item>热点</item>
<item>社会</item>
<item>娱乐</item>
<item>科技</item>
<item>汽车</item>
<item>体育</item>
</string-array>
</resources>
TabLayoutActivity.java
@BindView(R.id.tab_layout)
TabLayout mTabLayout;
@BindView(R.id.view_pager)
ViewPager mViewPager;
private ArrayList<MyFragment> mFragments = new ArrayList<>();
// 获取标签数组
String[] tabName = getResources().getStringArray(R.array.tab);
for (String s : tabName) {
MyFragment fragment = new MyFragment();
Bundle bundle = new Bundle();
bundle.putString(MyFragment.TAB, s);
fragment.setArguments(bundle);
mFragments.add(fragment);
}
MyTabAdapter adapter = new MyTabAdapter(getSupportFragmentManager(), mFragments, tabName);
mViewPager.setAdapter(adapter);
// 关联 viewPager
mTabLayout.setupWithViewPager(mViewPager);
// FIXME 与 viewPager 关联后会为我们添加标题,所以可以通过 getTabAt 获取到标题
for (int i = 0; i < tabName.length; i++) {
TabLayout.Tab tab = mTabLayout.getTabAt(i);
if (null != tab) {
tab.setCustomView(R.layout.tab_custom_view);
if (null != tab.getCustomView()) {
ImageView imageView = tab.getCustomView().findViewById(R.id.iv_tab_icon);
TextView textView = tab.getCustomView().findViewById(R.id.tv_tab_text);
textView.setText(tabName[i]);
}
}
}
MyFragment.java
public class MyFragment extends Fragment {
public static final String TAB = "TAB";
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
String text = getArguments().getString(TAB);
TextView textView = new TextView(getContext());
textView.setGravity(Gravity.CENTER);
textView.setText(text);
return textView;
}
}
MyTabAdapter.java
public class MyTabAdapter extends FragmentStatePagerAdapter {
private ArrayList<MyFragment> mFragments;
private String[] mTabName;
public MyTabAdapter(FragmentManager fm, ArrayList<MyFragment> fragments, String[] tabName) {
super(fm);
mFragments = fragments;
mTabName = tabName;
}
@Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
@Override
public int getCount() {
return mFragments.size();
}
// /**
// * FIXME 调用 setupWithViewPager 方法后会为我们设置与 getCount 等量的空白标题,如果标题只有文字可以在此进行设置
// *
// * @param position
// * @return
// */
// @Override
// public CharSequence getPageTitle(int position) {
// return mTabName[position];
// }
}
从代码可以看出,先是获取到标签数组,创建 MyFragment 并将标签传入;创建 MyTabAdapter 并设置给 ViewPage。前门这几步都是 ViewPager 使用的正常操作。调用 TabLayout 的 setupWithViewPager()
传入 ViewPager 与之进行关联,关联之后 TabLayout 会根据 MyTabAdapter 的 getCount
方法生成对应数量的 Tab,最后我们通过 getTabAt
方法获取到对应的 Tab,并将 Tab 设置为我们的自定义视图。到这里已经实现了效果图所展示的效果
PS:虽然 TabLayout 已经为我们提供了默认标签的布局,可以设置图标和文字,同时也支持我们的自定义视图作为标签。但是为了应对需求的多变,我们最好还是使用自定义的视图作为标签布局
如何避坑
调用 setupWithViewPager()
后 Tab 的视图不显示
这里贴一下我第一次写的代码
String[] tabName = getResources().getStringArray(R.array.tab);
// 给 TabLayout 添加新 Tab
for (int i = 0; i < tabName.length; i++) {
TabLayout.Tab tab = mTabLayout.newTab();
// 使用自定义tab视图
tab.setCustomView(R.layout.tab_custom_view);
if (null != tab.getCustomView()) {
ImageView imageView = tab.getCustomView().findViewById(R.id.iv_tab_icon);
TextView textView = tab.getCustomView().findViewById(R.id.tv_tab_text);
textView.setText(tabName[i]);
}
mTabLayout.addTab(tab);
}
for (String s : tabName) {
MyFragment fragment = new MyFragment();
Bundle bundle = new Bundle();
bundle.putString(MyFragment.TAB, s);
fragment.setArguments(bundle);
mFragments.add(fragment);
}
MyTabAdapter adapter = new MyTabAdapter(getSupportFragmentManager(), mFragments, tabName);
mViewPager.setAdapter(adapter);
// 关联 viewPager
mTabLayout.setupWithViewPager(mViewPager);
看一下上面的代码,先是为 TabLayout 添加自定义布局的 Tab,然后再设置 ViewPager,并调用 setupWithViewPager()
进行关联。看起来没有什么问题。运行之后,发现 Tab 都是空白的。因为一开始没有关联 ViewPager 的时候,我运行是正常的,所以问题肯定是出在 setupWithViewPager()
这个方法。下面我们看一下这个方法的源码
public void setupWithViewPager(@Nullable ViewPager viewPager) {
setupWithViewPager(viewPager, true);
}
public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) {
setupWithViewPager(viewPager, autoRefresh, false);
}
private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
boolean implicitSetup) {
// ...省略了一些监听器设置
if (viewPager != null) {
mViewPager = viewPager;
// Add our custom OnPageChangeListener to the ViewPager
if (mPageChangeListener == null) {
mPageChangeListener = new TabLayoutOnPageChangeListener(this);
}
mPageChangeListener.reset();
viewPager.addOnPageChangeListener(mPageChangeListener);
// Now we'll add a tab selected listener to set ViewPager's current item
mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
addOnTabSelectedListener(mCurrentVpSelectedListener);
final PagerAdapter adapter = viewPager.getAdapter();
if (adapter != null) {
// Now we'll populate ourselves from the pager adapter, adding an observer if
// autoRefresh is enabled
1. setPagerAdapter(adapter, autoRefresh);
}
// Add a listener so that we're notified of any adapter changes
if (mAdapterChangeListener == null) {
mAdapterChangeListener = new AdapterChangeListener();
}
mAdapterChangeListener.setAutoRefresh(autoRefresh);
viewPager.addOnAdapterChangeListener(mAdapterChangeListener);
// Now update the scroll position to match the ViewPager's current item
setScrollPosition(viewPager.getCurrentItem(), 0f, true);
} else {
// We've been given a null ViewPager so we need to clear out the internal state,
// listeners and observers
mViewPager = null;
setPagerAdapter(null, false);
}
mSetupViewPagerImplicitly = implicitSetup;
}
从源码可以看出,最后都是调用到 setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup)
方法,重点看 setPagerAdapter(adapter, autoRefresh);
方法,继续进入到该方法
void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
if (mPagerAdapter != null && mPagerAdapterObserver != null) {
// If we already have a PagerAdapter, unregister our observer
mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
}
mPagerAdapter = adapter;
if (addObserver && adapter != null) {
// Register our observer on the new adapter
if (mPagerAdapterObserver == null) {
mPagerAdapterObserver = new PagerAdapterObserver();
}
adapter.registerDataSetObserver(mPagerAdapterObserver);
}
// Finally make sure we reflect the new adapter
2. populateFromPagerAdapter();
}
void populateFromPagerAdapter() {
3. removeAllTabs();
if (mPagerAdapter != null) {
final int adapterCount = mPagerAdapter.getCount();
for (int i = 0; i < adapterCount; i++) {
4. addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
}
// Make sure we reflect the currently set ViewPager item
if (mViewPager != null && adapterCount > 0) {
final int curItem = mViewPager.getCurrentItem();
if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
selectTab(getTabAt(curItem));
}
}![TabLayout.gif](http://upload-images.jianshu.io/upload_images/1131117-d9a80453a1363fbd.gif?imageMogr2/auto-orient/strip)
}
}
最后调用到了 populateFromPagerAdapter()
方法,到了这里基本找到问题了,populateFromPagerAdapter
第一行代码 removeAllTabs()
移除了所有的 Tab 后又根据 mPagerAdapter.getCount()
的数量添加新的 Tab,并为新的 Tab 设置了文本,文本内容则是通过 mPagerAdapter.getPageTitle(int)
获取。分析到处,我们的问题很容易就可以解决了。正确写法就是在简单使用的第三点所贴的代码
关键知识点总结
- 如果要使 Tab 可横向滑动,需在 TabLayout 的 app:tabMode 属性设置为 scrollable
- 如果不需要 Tab 的指示器的时候,有两种方法:方法一、设置
app:tabIndicatorColor="@android:color/transparent"
,方法二、设置app:tabIndicatorHeight="0dp"
- 系统默认的 Tab 支持可设置图片(上)和文字(下),如果需要可定制的 Tab 可以使用
setCustomView()
设置自定义视图 - 自定义 Tab 支持使用 selector,采用 selector 方式更方便的设置选中和未选中时的状态,而无需在代码进行状态切换
- 与 ViewPager 关联时,如果 Tab 只有本文,则可以重写 Adapter 的
getPageTitle()
方法即可 - 与 ViewPager 关联时,如果需要自定义 Tab,则通过调用
setupWithViewPager()
方法后再通过 TabLayout 的getTabAt(int)
方法获取到对应的 Tab 进行相关设置即可
结语
本文主要介绍了 TabLayout 的使用以及如何关联 ViewPager。该控件实际开发也非常实用,目前市场上的很多新闻类客户端等都有此效果。本文 demo 已上传到 github
以下是官方API地址(自备梯子)