MaterialDesign之對TabLayout的探索

1、簡述

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

2、使用

一、建立Tab及Tab的點擊事件

要使用TabLayout,通常會先在佈局文件中放好,如:java

<?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,並設置其點擊事件,如:android

@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(),有興趣的能夠試試看,上面代碼效果以下:git

手動建立Tab,並設置點擊事件

二、自定義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)的高度提升,顏色改成刺眼的紅眼,以下:github

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

設置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"/>複製代碼

app:tabGravity="fill"

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

app:tabMode="scrollable"

3)設置Tab文字樣式

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

在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"/>複製代碼

好了,看看效果如何:

設置Tab文字樣式

好了,關於Tab的寬度、內間距等設置比較簡單,本身須要的時候試試吧,這裏就不演示了。

三、與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);
}複製代碼

來看下效果:

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];
    }
}    複製代碼

3、拓展

上面部分是TabLayout的正規使用說明,而這部分是對TabLayout的進一步探索,同時將列出本人在這個過程當中的探索思路,能夠說是對TabLayout的進一步自定義吧。廢話很少說,下面就直接開車了。

假如,你手中的APP設計稿中有以下的三個需求那該怎麼辦:

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

簡單的說就是爲TabLayout添加分割線、設置不一樣狀態下的字體大小和指示器的「長度」,這些在TabLayout中並無提供直接的修改方法,你可能會想,那咱們對TabLayout進行源碼分析,而後經過反射等手段拿到其中的控件來設置?不!有時候解決問題不要循規蹈矩,應該適當轉變下思路,或許解決問題的方法並不須要去看源碼那麼困難(若是你是大神,就當我沒說),下面看我操做:

一、分析TabLayout的結構

將APP運行起來,而後回到AS,在菜單欄中依次找到Tools-->Android-->Android Device Monitor,用過Eclipse開發的Android程序員應該都知道這久違的老夥計———Android 設備監測儀。

Android 設備監測儀

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

Dump View Hierarchy for UI Automator

可能會卡一下,而後它會自動把當前界面的控件結構展現出來。

界面控件結構圖

從這個結構上咱們能夠知道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>複製代碼

這樣,分割線就有了。

爲TabLayout添加分割線(有瑕疵)

看起來有點怪是吧,這是由於咱們前面設置的app:tabBackground="@color/colorPrimaryDark"只是給Tab設置了背景色,而不是給Tab的父級控件LinearLayout設置,這個LinearLayout默認的背景色是白色,因此纔會是這個樣子,解決方法天然就是給LinearLayout設置跟Tab同樣的背景色就行了。

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

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

爲TabLayout添加分割線(完美)

三、爲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()方法:

經過newTab()源碼查看TabView本質

然而事實並不如意,徹底沒有效果,去看了下源碼,也不是很肯定,個人猜測是這樣的,當咱們對Tab中的文本控件設置字體大小後,TabView的onMeasuer()方法會被從新調用,而這個方法裏就對文字大小從新進行賦值,致使文字大小無法按上面的方式進行修改。

TabLayout源碼中對Tab文字大小作了限制

因此,文字的大小隻能經過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存在間距

好,到這裏我對TabLayout的探索之旅就結束了,本文經過直接查找TabLayout中控件的方式,來自定義TabLayout自己無法直接設置的樣式效果,從而來知足咱們項目的需求。這僅僅是我我的對TabLayout的理解,可能存在些瑕疵,請多包涵,若是對「爲TabLayout設置不一樣狀態下的字體大小」和「自定義指示器長度」其餘確實可行的方法,請留言告訴我一下吧,多多指教,謝謝。

最後附上Demo連接

github.com/GitLqr/Mate…

相關文章
相關標籤/搜索