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

什么是 TabLayout

TabLayout provides a horizontal layout to display tabs.

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

簡(jiǎn)單使用

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


TabLayout.gif

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

  1. 引入 com.android.support:design
    TabLayout 是屬于 com.android.support:design 包的控件,所以需要依賴(lài)該包
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 布局文件看,根布局為一個(gè)垂直的線(xiàn)性布局,包含 TabLayout 和 ViewPager。重點(diǎn)看一下 TabLayout 的幾個(gè)常用屬性值

  • app:tabBackground 標(biāo)簽布局的背景色
  • app:tabIndicatorColor 指示器的顏色
  • app:tabIndicatorHeight 指示器的高度(如果不需要指示器可以設(shè)置為0dp)
  • app:tabMode 顯示模式:默認(rèn) fixed(固定),scrollable(可橫向滾動(dòng))
  • 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>

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

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

arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="tab">
        <item>推薦</item>
        <item>視頻</item>
        <item>熱點(diǎn)</item>
        <item>社會(huì)</item>
        <item>娛樂(lè)</item>
        <item>科技</item>
        <item>汽車(chē)</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)后會(huì)為我們添加標(biāo)題,所以可以通過(guò) 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 方法后會(huì)為我們?cè)O(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。前門(mén)這幾步都是 ViewPager 使用的正常操作。調(diào)用 TabLayout 的 setupWithViewPager() 傳入 ViewPager 與之進(jìn)行關(guān)聯(lián),關(guān)聯(lián)之后 TabLayout 會(huì)根據(jù) MyTabAdapter 的 getCount 方法生成對(duì)應(yīng)數(shù)量的 Tab,最后我們通過(guò) getTabAt 方法獲取到對(duì)應(yīng)的 Tab,并將 Tab 設(shè)置為我們的自定義視圖。到這里已經(jīng)實(shí)現(xiàn)了效果圖所展示的效果

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

如何避坑

調(diào)用 setupWithViewPager() 后 Tab 的視圖不顯示

這里貼一下我第一次寫(xiě)的代碼

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)??雌饋?lái)沒(méi)有什么問(wèn)題。運(yùn)行之后,發(fā)現(xiàn) Tab 都是空白的。因?yàn)橐婚_(kāi)始沒(méi)有關(guān)聯(lián) ViewPager 的時(shí)候,我運(yùn)行是正常的,所以問(wèn)題肯定是出在 setupWithViewPager() 這個(gè)方法。下面我們看一下這個(gè)方法的源碼

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)聽(tīng)器設(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) 方法,重點(diǎn)看 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() 方法,到了這里基本找到問(wèn)題了,populateFromPagerAdapter 第一行代碼 removeAllTabs() 移除了所有的 Tab 后又根據(jù) mPagerAdapter.getCount() 的數(shù)量添加新的 Tab,并為新的 Tab 設(shè)置了文本,文本內(nèi)容則是通過(guò) mPagerAdapter.getPageTitle(int) 獲取。分析到處,我們的問(wèn)題很容易就可以解決了。正確寫(xiě)法就是在簡(jiǎn)單使用的第三點(diǎn)所貼的代碼

關(guān)鍵知識(shí)點(diǎn)總結(jié)

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

結(jié)語(yǔ)

本文主要介紹了 TabLayout 的使用以及如何關(guān)聯(lián) ViewPager。該控件實(shí)際開(kāi)發(fā)也非常實(shí)用,目前市場(chǎng)上的很多新聞?lì)惪蛻?hù)端等都有此效果。本文 demo 已上傳到 github

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容