TabLayout

一、簡述

TabLayout是Android Support Design庫的新控件,可以用來實現開源框架ViewPageIndicator的效果(在MaterialDesign沒出來之前基本都用這玩意兒吧~),TabLayout相比它使用上更加簡單,且不一定要跟ViewPager一起使用,畢竟谷歌做出來的,穩定性更是不用說啦,此外,本文還會仔細列出本人對該控件的探索過程,從而實現一些控件本身沒法實現的自定義效果,下面來看看它都有哪些操作吧。

二、使用

1、創建Tab及Tab的點擊事件
要使用TabLayout,一般會先在布局文件中放好,如:

<?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/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

然后在Activity中找到它,對它進行設置,如果不跟ViewPager一起使用的話,可以對TabLayout手動添加多個tab,并設置其點擊事件,如:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_tab_layout);
    mTabLayout = (TabLayout) findViewById(R.id.tabLayout);
    // 添加多個tab
    for (int i = 0; i < title.length; i++) {
        TabLayout.Tab tab = mTabLayout.newTab();
        tab.setText(title[i]);
        // tab.setIcon(R.mipmap.ic_launcher);//icon會顯示在文字上面
        mTabLayout.addTab(tab);
    }
    // 給tab設置點擊事件
    mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            Toast.makeText(getApplicationContext(), title[tab.getPosition()], Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });
}

這里比較有意思的是Tab的創建需要調用TabLayout對象的newTab()方法,而不是直接new出一個Tab。Tab除了可以設置文字外,還能設置Icon,甚至可以自定義View,分別調用的是setIcon()和setCustomView(),有興趣的可以試試看,上面代碼效果如下:


image

2、自定義TabLayout樣式

這個TabLayout還是挺好看的,但開發中難免會要定制TabLayout的樣式,如設置默認或選中文字的顏色和大小等,還好,TabLayout盡可能多的提供了這些自定義屬性,可以讓開發者很方便的修改樣式,下面來看看都有哪些控件屬性可以設置:

<!--設置Tab指示器-->
app:tabIndicatorColor=""
app:tabIndicatorHeight=""

<!--設置Tab位置及顯示模式-->
app:tabGravity=""
app:tabMode=""

<!--設置Tab文字樣式-->
app:tabSelectedTextColor=""
app:tabTextAppearance=""
app:tabTextColor=""

<!--設置Tab的寬度、背景、內間距-->
app:tabMaxWidth=""
app:tabMinWidth=""
app:tabBackground=""
app:tabPadding=""
  1. 設置Tab指示器
    TabLayout的指示器默認顏色是color.xml中的colorAccent,通過TabLayout提供的自定義屬性,可以設置指示器的高度和顏色,如果不想顯示指示器(Indicator),可以將其高度設置為0dp或設置其顏色為透明,這里為演示,我就把顯示指示器(Indicator)的高度提高,顏色改為刺眼的紅眼,如下:
<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabIndicatorColor="@color/red"
    app:tabIndicatorHeight="8dp"/>
image

設置Tab指示器樣式

2. 設置Tab位置及顯示模式

TabLayout的顯示模式(tabMode)默認是固定不可滾動(fixed),位置(tabGravity)默認填滿(fill)整個TabLayout,我們先保持app:tabMode="fixed",把tabGravity的值換成fill和center對比下,為了方便對比,我把背景也設置了。

<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabBackground="@color/colorPrimaryDark"
    app:tabGravity="fill" // 再換成center
    app:tabMode="fixed"/>
image

image

當tab比較多的時候,一個屏幕寬度容納不下,這時候就需要讓TabLayout可以橫向滾動了,只需要修改app:tabMode="scrollable"即可。注意,當app:tabMode="scrollable"時,app:tabGravity=""不管取什么值都不會生效。

<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabBackground="@color/colorPrimaryDark"
    app:tabGravity="center"
    app:tabMode="scrollable"/>
image

3. 設置Tab文字樣式

上面的效果不好看,我想讓它默認文字顏色為灰色,選中時文字為白色,文字大小為16sp,但TabLayout沒有提供直接設置文字大小的屬性,這時候就需要用到app:tabTextAppearance=""了。操作如下:

在Style.xml中聲明文字樣式
<style name="TabLayout.TabText" parent="TextAppearance.Design.Tab">
    <item name="android:textSize">16sp</item>
    <item name="textAllCaps">false</item>
</style>

其中除了可以設置字體大小外,還可以設置英文是否都全部大寫顯示。textAllCaps的默認值為true,即英文全部大寫。

在布局文件中設置TabLayout的文字相關屬性
<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabBackground="@color/colorPrimaryDark"
    app:tabGravity="center"
    app:tabMode="scrollable"
    ...
    app:tabSelectedTextColor="@android:color/white"
    app:tabTextAppearance="@style/TabLayout.TabText"
    app:tabTextColor="@android:color/darker_gray"/>

好了,看看效果如何:


image

好了,關于Tab的寬度、內間距等設置比較簡單,自己需要的時候試試吧,這里就不演示了。

3、與ViewPager結合

上面通過對TabLayout的單獨使用學習了TabLayout的樣式自定義、創建Tab及設置Tab的點擊事件等,可以說常用的也就那些了,下面來看看TabLayout如何與ViewPager的結合使用。這種需求也是很常見的,界面頂部有一個標簽欄,中下部是與標簽對應的內容,可以左右滑動,同時標簽也跟隨其切換,相反的,在切換標簽時,內容部分也會跟著變化,不太明白的可以參考下“今日頭條”APP的首頁界面。這樣的效果就可以用TabLayout+ViewPager+Fragment來實現。

1. 先在布局文件中放好TabLayout和ViewPager:
<?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/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabBackground="@color/colorPrimaryDark"
        app:tabMode="scrollable"
        app:tabSelectedTextColor="@android:color/white"
        app:tabTextColor="@android:color/darker_gray"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

2. 在代碼中設置TabLayout與ViewPager相互關聯:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_tab_layout);
    mTabLayout = (TabLayout) findViewById(R.id.tabLayout);
    mViewPager = (ViewPager) findViewById(R.id.viewPager);
    MyViewPagerAdapter adapter = new MyViewPagerAdapter(getSupportFragmentManager());
    mViewPager.setAdapter(adapter);

    // 適配器必須重寫getPageTitle()方法 
    mTabLayout.setTabsFromPagerAdapter(adapter);
    // 監聽TabLayout的標簽選擇,當標簽選中時ViewPager切換
    mTabLayout.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager));
    // 監聽ViewPager的頁面切換,當頁面切換時TabLayout的標簽跟著切換
    mViewPager.setOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout));
}

這三句代碼不難理解,就字面上的意思,但是這三句代碼都已經過時,因為要關聯TabLayout與ViewPager就得寫三句代碼似乎是麻煩了一點點(其實我覺得還好吧),所以TabLayout提供了可以通過一句代碼搞定兩者關聯的方法:setupWithViewPager(),因此,上面的代碼可以簡化如下:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_tab_layout);
    mTabLayout = (TabLayout) findViewById(R.id.tabLayout);
    mViewPager = (ViewPager) findViewById(R.id.viewPager);
    MyViewPagerAdapter adapter = new MyViewPagerAdapter(getSupportFragmentManager());
    mViewPager.setAdapter(adapter);

    // 關聯TabLayout與ViewPager,且適配器必須重寫getPageTitle()方法 
    mTabLayout.setupWithViewPager(mViewPager);
}

來看下效果:


image

關聯TabLayout與ViewPager相當簡單,只要注意ViewPager適配器需重寫getPageTitle()方法,這里順便貼出Demo中適配器的代碼:

class MyViewPagerAdapter extends FragmentPagerAdapter {

    private final String[] title = new String[]{
            "推薦", "熱點", "視頻", "深圳", "通信",
            "互聯網", "問答", "圖片", "電影",
            "網絡安全", "軟件"};

    public MyViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int i) {
        Fragment fragment = new TextFragment();
        Bundle bundle = new Bundle();
        bundle.putString("title", title[i]);
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public int getCount() {
        return title.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return title[position];
    }
}

拓展

上面部分是TabLayout的正規使用說明,而這部分是對TabLayout的進一步探索,同時將列出本人在這個過程中的探索思路,可以說是對TabLayout的進一步自定義吧。廢話不多說,下面就直接開車了。
假如,你手中的APP設計稿中有如下的三個需求那該怎么辦:

  • 為TabLayout添加分割線,且分割線距離上下存在間距。
  • 選中時tab字體變大,未選中時tab字體變小。
  • 指示器(Indicator)不要充滿整個標簽(Tab)

簡單的說就是為TabLayout添加分割線、設置不同狀態下的字體大小和指示器的“長度”,這些在TabLayout中并沒有提供直接的修改方法,你可能會想,那我們對TabLayout進行源碼分析,然后通過反射等手段拿到其中的控件來設置?不!有時候解決問題不要循規蹈矩,應該適當轉變下思路,或許解決問題的方法并不需要去看源碼那么困難(如果你是大神,就當我沒說),下面看我操作:

從結構上我們可以知道TabLayout(就是HorizontalScrollView)并不是直接就包裹這些Tab的,而是包裹了一個LinearLayout,然后這些Tab放在這個LinearLayout中,此外,可以發現Tab里包含了一個TextView,到這里對前面2個需求是不是有點想法了呢?

為TabLayout添加分割線

LinearLayout自帶就有設置分割線的方法,我們可以通過它來添加分割線,也沒什么好說的,直接上代碼:

mLinearLayout = (LinearLayout) mTabLayout.getChildAt(0);
// 在所有子控件的中間顯示分割線(還可能只顯示頂部、尾部和不顯示分割線)
mLinearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
// 設置分割線的距離本身(LinearLayout)的內間距
mLinearLayout.setDividerPadding(20);
// 設置分割線的樣式
mLinearLayout.setDividerDrawable(ContextCompat.getDrawable(this, R.drawable.divider_vertical));

divider_vertical.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ccc"/>
    <size android:width="1dp" />
</shape>

這樣,分割線就有了。


image

看起來有點怪是吧,這是因為我們前面設置的app:tabBackground="@color/colorPrimaryDark"只是給Tab設置了背景色,而不是給Tab的父級控件LinearLayout設置,這個LinearLayout默認的背景色是白色,所以才會是這個樣子,解決方法自然就是給LinearLayout設置跟Tab一樣的背景色就好了。

mLinearLayout = (LinearLayout) mTabLayout.getChildAt(0);
...
mLinearLayout.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));

這樣就完美的為TabLayout設置分割線了。


image

為TabLayout設置不同狀態下的字體大小(并不能成功)

用同樣的方式拿到Tab中的文本控件,判斷當前是否被選中,再對該文本控件進行字體大小設置就歐了。借助上面拿到的用來包裹Tab的LinearLayout(mLinearLayout),遍歷LinearLayout中的子控件,拿到一個個的子view(即Tab),再從Tab中拿到文本控件設置文字大小。

// 默認讓所有沒有選中的Tab的文字設置為小字體
for (int i = 0; i < mTabLayout.getTabCount(); i++) {
    ((TextView) ((LinearLayout) mLinearLayout.getChildAt(i)).getChildAt(1)).setTextSize(10);
    // 也可以這么寫,一樣的
    // ((TextView) ((LinearLayout) ((LinearLayout) mTabLayout.getChildAt(0)).getChildAt(i)).getChildAt(0)).setTextSize(12);
}
// 再把當前被選中的Tab文字設置為大字體
((TextView) ((LinearLayout) mLinearLayout.getChildAt(mTabLayout.getSelectedTabPosition())).getChildAt(1)).setTextSize(30);

// 當選中的Tab切換時,再調整Tab的字體大小
mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        ((TextView) ((LinearLayout) mLinearLayout.getChildAt(tab.getPosition())).getChildAt(1)).setTextSize(30);
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        ((TextView) ((LinearLayout) mLinearLayout.getChildAt(tab.getPosition())).getChildAt(1)).setTextSize(12);
    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }
});

上面代碼中把得到的Tab強轉成LinearLayout,這是因為Tab實際上是TabView,而TabView繼承自LinearLayout,所以可以這樣轉換,我們可以看下TabLayout的newTab()方法:


image

然而事實并不如意,完全沒有效果,去看了下源碼,也不是很確定,我的猜想是這樣的,當我們對Tab中的文本控件設置字體大小后,TabView的onMeasuer()方法會被重新調用,而這個方法里就對文字大小重新進行賦值,導致文字大小沒法按上面的方式進行修改。


image

所以,文字的大小只能通過Style的方法去修改,且只能統一設置選中和未選中的文字大小,故,這個需求沒法完成。

自定義指示器長度

其實這有點標題黨的意思了,TabLayout的指示器長度沒法指定,它原本多長就是多長,但可以通過設置Tab外間距的方式,讓指示器看起來像是與Tab保持一定距離,這里我在網上找到了方法,方法如下:

// 設置TabLayout的“長度”
setIndicator(mTabLayout,10,10);

// 具體方法(通過反射的方式)
public void setIndicator(TabLayout tabs, int leftDip, int rightDip) {
    Class<?> tabLayout = tabs.getClass();
    Field tabStrip = null;
    try {
        tabStrip = tabLayout.getDeclaredField("mTabStrip");
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }

    tabStrip.setAccessible(true);
    LinearLayout llTab = null;
    try {
        llTab = (LinearLayout) tabStrip.get(tabs);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    int left = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, leftDip, Resources.getSystem().getDisplayMetrics());
    int right = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightDip, Resources.getSystem().getDisplayMetrics());

    for (int i = 0; i < llTab.getChildCount(); i++) {
        View child = llTab.getChildAt(i);
        child.setPadding(0, 0, 0, 0);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
        params.leftMargin = left;
        params.rightMargin = right;
        child.setLayoutParams(params);
        child.invalidate();
    }
}

這個setIndicator()方法主要是通過反射的方式,先拿到mTabStrip(其實就是TabLayout直接包裹的LinearLayout),再遍歷出mTabStrip中的子控件Tab,再設置Tab的外間距,為了證明就是設置了Tab的外間距,這里我分別對mTabStrip設置了背景色和不設置其背景色,來看看對比:


mTabStrip設置了背景色

mTabStrip不設置背景色

設置了背景色看起來效果還馬馬虎虎吧,但這樣的方式沒辦法讓指示器的長度比文字長度短(無奈~)。好了,不管這個了,既然我前面說了mTabStrip其實就是TabLayout直接包裹的LinearLayout,那通過這個LinearLayout來設置也是可以的,證明一下:

// 得到TabLayout包裹的LinearLayout并設置背景色
mLinearLayout = (LinearLayout) mTabLayout.getChildAt(0);
mLinearLayout.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));
...
// 設置LinearLayout中子View(Tab)的外間距
int left = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, Resources.getSystem().getDisplayMetrics());
int right = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, Resources.getSystem().getDisplayMetrics());
for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
    View tabView = mLinearLayout.getChildAt(0);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
    params.leftMargin = left;
    params.rightMargin = right;
    tabView.setLayoutParams(params);
}
通過查找控件方式,讓指示器與Tab存在間距

最后附上Demo鏈接

https://github.com/GitLqr/MaterialDesignDemo

作者:CSDN_LQR
鏈接:http://www.lxweimin.com/p/bbefb97cccdd

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容