MaterialDesign之對TabLayout的探索

GitLqr發表於2017-05-23

一、簡述

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(),有興趣的可以試試看,上面程式碼效果如下:

MaterialDesign之對TabLayout的探索
手動建立Tab,並設定點選事件

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"/>複製程式碼

MaterialDesign之對TabLayout的探索
設定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"/>複製程式碼

MaterialDesign之對TabLayout的探索
app:tabGravity="fill"

MaterialDesign之對TabLayout的探索
app:tabGravity="center"

當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"/>複製程式碼

MaterialDesign之對TabLayout的探索
app:tabMode="scrollable"

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"/>複製程式碼

好了,看看效果如何:

MaterialDesign之對TabLayout的探索
設定Tab文字樣式

好了,關於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);
}複製程式碼

來看下效果:

MaterialDesign之對TabLayout的探索
TabLayout與ViewPager相互關聯

關聯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設計稿中有如下的三個需求那該怎麼辦:

  1. 為TabLayout新增分割線,且分割線距離上下存在間距。
  2. 選中時tab字型變大,未選中時tab字型變小。
  3. 指示器(Indicator)不要充滿整個標籤(Tab)

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

1、分析TabLayout的結構

將APP執行起來,然後回到AS,在選單欄中依次找到Tools-->Android-->Android Device Monitor,用過Eclipse開發的Android程式設計師應該都知道這久違的老夥計———Android 裝置監測儀。

MaterialDesign之對TabLayout的探索
Android 裝置監測儀

選中正在執行的APP,點選Dump View Hierarchy for UI Automator。

MaterialDesign之對TabLayout的探索
Dump View Hierarchy for UI Automator

可能會卡一下,然後它會自動把當前介面的控制元件結構展示出來。

MaterialDesign之對TabLayout的探索
介面控制元件結構圖

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

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>複製程式碼

這樣,分割線就有了。

MaterialDesign之對TabLayout的探索
為TabLayout新增分割線(有瑕疵)

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

mLinearLayout = (LinearLayout) mTabLayout.getChildAt(0);
...
mLinearLayout.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));複製程式碼

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

MaterialDesign之對TabLayout的探索
為TabLayout新增分割線(完美)

3、為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()方法:

MaterialDesign之對TabLayout的探索
通過newTab()原始碼檢視TabView本質

然而事實並不如意,完全沒有效果,去看了下原始碼,也不是很確定,我的猜想是這樣的,當我們對Tab中的文字控制元件設定字型大小後,TabView的onMeasuer()方法會被重新呼叫,而這個方法裡就對文字大小重新進行賦值,導致文字大小沒法按上面的方式進行修改。

MaterialDesign之對TabLayout的探索
TabLayout原始碼中對Tab文字大小做了限制

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

4、自定義指示器長度

其實這有點標題黨的意思了,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設定了背景色和不設定其背景色,來看看對比:

MaterialDesign之對TabLayout的探索
mTabStrip設定了背景色

MaterialDesign之對TabLayout的探索
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);
}複製程式碼

MaterialDesign之對TabLayout的探索
通過查詢控制元件方式,讓指示器與Tab存在間距

好,到這裡我對TabLayout的探索之旅就結束了,本文通過直接查詢TabLayout中控制元件的方式,來自定義TabLayout本身沒法直接設定的樣式效果,從而來滿足我們專案的需求。這僅僅是我個人對TabLayout的理解,可能存在些瑕疵,請多包涵,如果對“為TabLayout設定不同狀態下的字型大小”和“自定義指示器長度”其他確實可行的方法,請留言告訴我一下吧,多多指教,謝謝。

最後附上Demo連結

github.com/GitLqr/Mate…

相關文章