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

什么是 TabLayout

TabLayout provides a horizontal layout to display tabs.

官方介紹有滑,TabLayout 是一個橫向標(biāo)簽顯示的布局娄琉,效果就是現(xiàn)在很多新聞客戶端的那種頂部標(biāo)簽展示效果往果,并支持指示器、 ViewPager 聯(lián)動

簡單使用

按照慣例颓鲜,我們先看一下效果圖:


TabLayout.gif

從效果圖來看表窘,這是采用 TabLayout + ViewPager 滑動切換和點擊標(biāo)簽切換的一個效果。TabLayout 支持橫向滾動多標(biāo)簽設(shè)置甜滨,還可以支持指示器乐严,支持與 ViewPager 進(jìn)行聯(lián)動。下面看看具體實現(xiàn)

  1. 引入 com.android.support:design
    TabLayout 是屬于 com.android.support:design 包的控件衣摩,所以需要依賴該包
compile 'com.android.support:design:26.1.0'
  1. xml 文件創(chuàng)建
<?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 標(biāo)簽布局的背景色
  • app:tabIndicatorColor 指示器的顏色
  • app:tabIndicatorHeight 指示器的高度(如果不需要指示器可以設(shè)置為0dp)
  • app:tabMode 顯示模式:默認(rèn) fixed(固定)凛篙,scrollable(可橫向滾動)
  • app:tabPadding 標(biāo)簽內(nèi)邊距
  • app:tabSelectedTextColor 標(biāo)簽選中的文本顏色
  • app:tabTextAppearance 標(biāo)簽文本樣式
  • app:tabTextColor 標(biāo)簽未選中的文本顏色

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>

以上是一個簡單的自定義標(biāo)簽布局文件黍匾,可以看到 ImageView 的 src 和 TextView 的 textColor 屬性值都使用了 selector栏渺,這樣寫的話就不需要再在代碼監(jiān)聽選中的 Tab 去改變狀態(tài)了,非常方便锐涯。該 xml 會在下面的代碼中應(yīng)用

  1. Activity 中獲取并設(shè)置相關(guān)屬性值磕诊,以及事件監(jiān)聽

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<>();

// 獲取標(biāo)簽數(shù)組
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);
// 關(guān)聯(lián) viewPager
mTabLayout.setupWithViewPager(mViewPager);

// FIXME 與 viewPager 關(guān)聯(lián)后會為我們添加標(biāo)題,所以可以通過 getTabAt 獲取到標(biāo)題
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 調(diào)用 setupWithViewPager 方法后會為我們設(shè)置與 getCount 等量的空白標(biāo)題纹腌,如果標(biāo)題只有文字可以在此進(jìn)行設(shè)置
//     *
//     * @param position
//     * @return
//     */
//    @Override
//    public CharSequence getPageTitle(int position) {
//        return mTabName[position];
//    }
}

從代碼可以看出霎终,先是獲取到標(biāo)簽數(shù)組,創(chuàng)建 MyFragment 并將標(biāo)簽傳入升薯;創(chuàng)建 MyTabAdapter 并設(shè)置給 ViewPage莱褒。前門這幾步都是 ViewPager 使用的正常操作。調(diào)用 TabLayout 的 setupWithViewPager() 傳入 ViewPager 與之進(jìn)行關(guān)聯(lián)涎劈,關(guān)聯(lián)之后 TabLayout 會根據(jù) MyTabAdapter 的 getCount 方法生成對應(yīng)數(shù)量的 Tab广凸,最后我們通過 getTabAt 方法獲取到對應(yīng)的 Tab,并將 Tab 設(shè)置為我們的自定義視圖蛛枚。到這里已經(jīng)實現(xiàn)了效果圖所展示的效果

PS:雖然 TabLayout 已經(jīng)為我們提供了默認(rèn)標(biāo)簽的布局谅海,可以設(shè)置圖標(biāo)和文字,同時也支持我們的自定義視圖作為標(biāo)簽蹦浦。但是為了應(yīng)對需求的多變扭吁,我們最好還是使用自定義的視圖作為標(biāo)簽布局

如何避坑

調(diào)用 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);
// 關(guān)聯(lián) viewPager
mTabLayout.setupWithViewPager(mViewPager);

看一下上面的代碼,先是為 TabLayout 添加自定義布局的 Tab,然后再設(shè)置 ViewPager侥袜,并調(diào)用 setupWithViewPager() 進(jìn)行關(guān)聯(lián)蝌诡。看起來沒有什么問題枫吧。運行之后送漠,發(fā)現(xiàn) Tab 都是空白的。因為一開始沒有關(guān)聯(lián) 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) {
    // ...省略了一些監(jiān)聽器設(shè)置

    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;
}

從源碼可以看出尼酿,最后都是調(diào)用到 setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) 方法爷狈,重點看 setPagerAdapter(adapter, autoRefresh); 方法,繼續(xù)進(jìn)入到該方法

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)

    }
}

最后調(diào)用到了 populateFromPagerAdapter() 方法裳擎,到了這里基本找到問題了涎永,populateFromPagerAdapter 第一行代碼 removeAllTabs() 移除了所有的 Tab 后又根據(jù) mPagerAdapter.getCount() 的數(shù)量添加新的 Tab,并為新的 Tab 設(shè)置了文本鹿响,文本內(nèi)容則是通過 mPagerAdapter.getPageTitle(int) 獲取羡微。分析到處,我們的問題很容易就可以解決了惶我。正確寫法就是在簡單使用的第三點所貼的代碼

關(guān)鍵知識點總結(jié)

  • 如果要使 Tab 可橫向滑動妈倔,需在 TabLayout 的 app:tabMode 屬性設(shè)置為 scrollable
  • 如果不需要 Tab 的指示器的時候,有兩種方法:方法一绸贡、設(shè)置 app:tabIndicatorColor="@android:color/transparent"盯蝴,方法二、設(shè)置 app:tabIndicatorHeight="0dp"
  • 系統(tǒng)默認(rèn)的 Tab 支持可設(shè)置圖片(上)和文字(下)听怕,如果需要可定制的 Tab 可以使用 setCustomView() 設(shè)置自定義視圖
  • 自定義 Tab 支持使用 selector捧挺,采用 selector 方式更方便的設(shè)置選中和未選中時的狀態(tài),而無需在代碼進(jìn)行狀態(tài)切換
  • 與 ViewPager 關(guān)聯(lián)時尿瞭,如果 Tab 只有本文闽烙,則可以重寫 Adapter 的 getPageTitle() 方法即可
  • 與 ViewPager 關(guān)聯(lián)時,如果需要自定義 Tab声搁,則通過調(diào)用 setupWithViewPager() 方法后再通過 TabLayout 的 getTabAt(int) 方法獲取到對應(yīng)的 Tab 進(jìn)行相關(guān)設(shè)置即可

結(jié)語

本文主要介紹了 TabLayout 的使用以及如何關(guān)聯(lián) ViewPager黑竞。該控件實際開發(fā)也非常實用,目前市場上的很多新聞類客戶端等都有此效果酥艳。本文 demo 已上傳到 github

以下是官方API地址(自備梯子)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摊溶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子充石,更是在濱河造成了極大的恐慌莫换,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拉岁,居然都是意外死亡坷剧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門喊暖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惫企,“玉大人,你說我怎么就攤上這事陵叽∧” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵巩掺,是天一觀的道長偏序。 經(jīng)常有香客問我,道長胖替,這世上最難降的妖魔是什么研儒? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮独令,結(jié)果婚禮上端朵,老公的妹妹穿的比我還像新娘。我一直安慰自己燃箭,他們只是感情好冲呢,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著遍膜,像睡著了一般碗硬。 火紅的嫁衣襯著肌膚如雪瓤湘。 梳的紋絲不亂的頭發(fā)上瓢颅,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音弛说,去河邊找鬼挽懦。 笑死,一個胖子當(dāng)著我的面吹牛木人,可吹牛的內(nèi)容都是我干的信柿。 我是一名探鬼主播甲献,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼宏胯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了媳握?” 一聲冷哼從身側(cè)響起稠曼,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤形病,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漠吻,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡量瓜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了途乃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绍傲。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖耍共,靈堂內(nèi)的尸體忽然破棺而出烫饼,到底是詐尸還是另有隱情,我是刑警寧澤试读,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布枫弟,位于F島的核電站,受9級特大地震影響鹏往,放射性物質(zhì)發(fā)生泄漏淡诗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一伊履、第九天 我趴在偏房一處隱蔽的房頂上張望韩容。 院中可真熱鬧,春花似錦唐瀑、人聲如沸群凶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽请梢。三九已至,卻和暖如春力穗,著一層夾襖步出監(jiān)牢的瞬間毅弧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工当窗, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留够坐,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓崖面,卻偏偏與公主長得像元咙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子巫员,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容