MaterialDesign(五),TabLayout使用以及如何避坑

什么是 TabLayout

TabLayout provides a horizontal layout to display tabs.

官方介绍,TabLayout 是一个横向标签显示的布局,效果就是现在很多新闻客户端的那种顶部标签展示效果,并支持指示器、 ViewPager 联动

简单使用

按照惯例,我们先看一下效果图:


TabLayout.gif

从效果图来看,这是采用 TabLayout + ViewPager 滑动切换和点击标签切换的一个效果。TabLayout 支持横向滚动多标签设置,还可以支持指示器,支持与 ViewPager 进行联动。下面看看具体实现

  1. 引入 com.android.support:design
    TabLayout 是属于 com.android.support:design 包的控件,所以需要依赖该包
compile 'com.android.support:design:26.1.0'
  1. 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 会在下面的代码中应用

  1. 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地址(自备梯子)

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

推荐阅读更多精彩内容