Android開發之TabLayout

mex發表於2017-12-11

介紹

TabLayout是support.design包中提供的一個控制元件,如果要使用需要在app下的build.gradle中加入依賴:

dependencies {
    compile 'com.android.support:design:25.+'
}
複製程式碼

後面的版本根據自己的專案需要而定。

使用場景

TabLayout從命名上來看就跟Tab有關,所以它是用來做Tab選項卡的。先上一波圖:

掘金APP

Android開發之TabLayout
360手機助手
Android開發之TabLayout
慕課網
Android開發之TabLayout
等等很多APP都使用了TabLayout這個控制元件來做Tab選項卡,而且通常都是配合ViewPager來使用,當然它也可以單獨作為Tab來使用。

用法

在佈局檔案中加入:

<android.support.design.widget.TabLayout
    android:id="@+id/TabLayout"
    android:layout_width="match_parent"
    android:layout_height="35dp" />
複製程式碼

給TabLayout新增Tab,有兩種方式:

  • 在佈局檔案中新增
<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="35dp">
    
    <android.support.design.widget.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="C" />
        
    <android.support.design.widget.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="C++" />
        
    <android.support.design.widget.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Java" />
        
    <android.support.design.widget.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Kotlin" />
</android.support.design.widget.TabLayout>
複製程式碼
  • 在Java程式碼中動態新增
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
tabLayout.addTab(tabLayout.newTab().setText("C"));
tabLayout.addTab(tabLayout.newTab().setText("C++"));
tabLayout.addTab(tabLayout.newTab().setText("Java"));
tabLayout.addTab(tabLayout.newTab().setText("Kotlin"));
複製程式碼

執行效果:

Android開發之TabLayout

  • Tab文字英文變成大寫問題

不知道大家發現沒有,我們新增的英文Java和Kotlin怎麼變成大寫了?解決方案如下:

<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="35dp"
    app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget"/>
複製程式碼

重新設定TabLayout的tabTextApperance屬性給它指定文字顯示樣式。重新執行就不會出現英文變成大寫:

Android開發之TabLayout
造成這個原因主要是因為"textAllCaps"=true這個屬性,TabLayout給tabTextAppearance設定了預設的樣式:
Android開發之TabLayout
引用了下面的這個樣式:
Android開發之TabLayout

  • 修改Tab文字樣式
<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="35dp"
    app:tabSelectedTextColor="#000070"
    app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget"
    app:tabTextColor="#000000"/>
複製程式碼

app:tabTextColor是Tab文字正常顯示顏色,app:tabSelectedTextColor是Tab選中後文字顯示顏色,app:tabTextAppearance是Tab文字顯示外觀樣式,可以給它設定更多的text相關屬性。

  • 自定義Tab顯示樣式

TabLayout的Tab不僅僅只能顯示文字還可以顯示圖示等,完全自定義佈局也是可以的。 還是360手機助手的【熱點】模組,可以看到上面的Tab選項卡"房產"那一個欄右邊有個小紅點,那這個怎麼做呢?TabLayout是否能適應我們的需求?先上個圖:

Android開發之TabLayout
先要自定義一個佈局,用來替代TabLayout預設的Tab:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:gravity="center"
        android:maxLines="2"
        tools:text="aa" />

    <View
        android:id="@+id/dot"
        android:layout_width="3dp"
        android:layout_height="3dp"
        android:layout_gravity="top"
        android:background="@drawable/tab_dot" />
</LinearLayout>
複製程式碼

小紅點樣式:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/colorAccent" />
    <corners android:radius="3dp" />
</shape>
複製程式碼

然後在Java程式碼中改動TabLayout新增Tab的方式:

TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
tabLayout.addTab(tabLayout.newTab().setText("C"));
tabLayout.addTab(tabLayout.newTab().setText("C++"));
tabLayout.addTab(tabLayout.newTab().setText("Java"));
TabLayout.Tab tab = tabLayout.newTab().setCustomView(R.layout.tab_custom);
final View customTabView = tab.getCustomView();
final TextView tabText = (TextView) customTabView.findViewById(R.id.text);
tabText.setText("Kotlin");
tabLayout.addTab(tab);
複製程式碼

執行結果:

Android開發之TabLayout
可以看到Tab上已經顯示了小紅點,但是有個問題不知道大家注意到了沒有?Kotlin的文字顏色好像跟前面幾個不一樣,而且Tab選中後的文字顏色也不一樣。其實也很簡單,估計你們都想到了,沒錯,就是給自定義的Tab佈局中的TextView的textColor設定selector:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#000070" android:state_selected="true" />
    <item android:color="#000000" />
</selector>
複製程式碼

為了保持TabLayout整體Tab效果一致,我們需要參照定義TabLayout佈局中的app:tabSelectedTextColor和 app:tabTextColor設定的顏色值。再次執行效果就都一樣了:

Kotlin未選中時效果

Android開發之TabLayout
Kotlin選中時效果
Android開發之TabLayout
我在想有沒有更高階的或更簡單點的方法呢?外部只需要通過:

tabLayout.addTab(tabLayout.newTab().setText("Kotlin"));
複製程式碼

就行了,至於Tab文字選中和未選中文字顏色和TabLayout保持一致就行了,上面那樣做好麻煩啊!我想應該是有的。當然這種情況只適用像這種顯示個小紅點什麼的,如果小紅點裡還要顯示數字,那你還是老老實實的按照上面這種方法去寫吧!

  • 監聽TabLayout切換事件

小紅點操作升級!我觀察了一下360手機助手的【熱點】模組,當切換到"房產"這一欄時,小紅點會顯示。那這個又該怎麼做呢?這個時候我們需要監聽TabLayout的滑動切換事件。TabLayout提供了一個addOnTabSelectedListener方法用來監聽Tab選中事件:

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        final View customTabView = tab.getCustomView();
        if (customTabView != null) {
            final View dotView = customTabView.findViewById(R.id.dot);
            dotView.setVisibility(View.INVISIBLE);
        }
    }
    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
    }
    @Override
    public void onTabReselected(TabLayout.Tab tab) {
    }
});
複製程式碼

注意:在onTabSelected回撥方法中tab.getCustomView需要非空判斷,因為我們只給Kotlin這個Tab設定了自定義佈局。 如果我想Kotlin欄未選中時又顯示小紅點呢?簡單,在onTabUnselected回撥方法中給它設為可見就行了:

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        final View customTabView = tab.getCustomView();
        if (customTabView != null) {
            final View dotView = customTabView.findViewById(R.id.dot);
            dotView.setVisibility(View.INVISIBLE);
        }
    }
    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        final View customTabView = tab.getCustomView();
        if (customTabView != null) {
            final View dotView = customTabView.findViewById(R.id.dot);
            dotView.setVisibility(View.VISIBLE);
        }
    }
    @Override
    public void onTabReselected(TabLayout.Tab tab) {
    }
});
複製程式碼

案例

1、使用TabLayout做引導頁指示器

上效果圖:

Android開發之TabLayout
底部的指示器不是用的第三方的庫,也不是自己擺的幾個View,而是用TabLayout實現!按照上面的講解,這種Tab效果系統沒有,所以我們要自定義Tab佈局來替換預設的Tab。自定義Tab佈局R.layout.tab_custom_dot:

<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dot"
    android:layout_width="8dp"
    android:layout_height="8dp"
    android:background="@drawable/selector_tab_dot" />
複製程式碼

Tab選中和未選中背景樣式:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/tab_dot_selected" android:state_selected="true" />
    <item android:drawable="@drawable/tab_dot_normal" />
</selector>
複製程式碼

R.drawable.tab_dot_selected:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#efef00" />
    <corners android:radius="8dp" />
</shape>
複製程式碼

R.drawable.tab_dot_normal:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#efefef" />
    <corners android:radius="8dp" />
</shape>
複製程式碼

Java程式碼:

TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab_custom_dot));
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab_custom_dot));
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab_custom_dot));
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab_custom_dot));
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab_custom_dot));
複製程式碼

佈局檔案:

<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="20dp"
    app:tabIndicatorHeight="0dp"
    app:tabGravity="center"
    app:tabMode="fixed" />
複製程式碼

這裡要注意的是,需要加上app:tabIndicatorHeight="0dp"表示隱藏Tab指示器,預設的TabLayout會在底部顯示指示條,或者加上app:tabIndicatorColor="@android:color/transparent"給它顏色設定為透明。不然是整個樣子的:

Android開發之TabLayout
還有要加上app:tabGravity="center"讓它水平居中顯示,否則Tab會填充整個螢幕。

2、配合ViewPager一起滑動切換page並指示

TabLayou還有一個比較強大的地方就是可以配合ViewPager這個控制元件一起使用,可以隨著ViewPager的滑動一起滑動切換Tab,並且點選TabLayout的Tab也可以讓ViewPager切換到指定的page,主要是因為TabLayout有一個setupWithViewPager方法可以關聯ViewPager。不多說上程式碼,activity佈局:

<?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="35dp"
        app:tabIndicatorColor="@color/colorPrimary"
        app:tabMode="fixed"
        app:tabSelectedTextColor="#000070"
        app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget"
        app:tabTextColor="#000000" />

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

Java程式碼:

 TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
 ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
 tabLayout.addTab(tabLayout.newTab().setText("C"));
 tabLayout.addTab(tabLayout.newTab().setText("C++"));
 tabLayout.addTab(tabLayout.newTab().setText("Java"));
 tabLayout.addTab(tabLayout.newTab().setText("Swift"));
 tabLayout.addTab(tabLayout.newTab().setText("C#"));
 tabLayout.addTab(tabLayout.newTab().setText("Kotlin"));
 TestViewPagerAdapter adapter = new TestViewPagerAdapter(this, tabs);
 viewPager.setAdapter(adapter);
 tabLayout.setupWithViewPager(viewPager);
複製程式碼

自定義ViewPager的PagerAdapter:

private class TestViewPagerAdapter extends PagerAdapter {
        private final LayoutInflater layoutInflater;
        
        TestViewPagerAdapter(Context context) {
            layoutInflater = LayoutInflater.from(context);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            final View view = layoutInflater.inflate(R.layout.tab_1, container, false);
            container.addView(view);
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public int getCount() {
            return 6;
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }
    }
複製程式碼

然後執行:

Android開發之TabLayout
WTF??Tab一片白??正確姿勢:

private class TestViewPagerAdapter extends PagerAdapter {
    private final LayoutInflater layoutInflater;
    private List<String> data;
    TestViewPagerAdapter(Context context, List<String> data) {
        layoutInflater = LayoutInflater.from(context);
        this.data = data;
    }
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        final View view = layoutInflater.inflate(R.layout.tab_1, container, false);
        container.addView(view);
        return view;
    }
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }
    @Override
    public int getCount() {
        return data.size();
    }
    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }
    @Override
    public CharSequence getPageTitle(int position) {
        return data.get(position);
    }
}
 TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
 ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
 List<String> tabs = new ArrayList<>();
 tabs.add("C");
 tabs.add("C++");
 tabs.add("Java");
 tabs.add("Swift");
 tabs.add("C#");
 tabs.add("Kotlin");
 TestViewPagerAdapter adapter = new TestViewPagerAdapter(this, tabs);
 viewPager.setAdapter(adapter);
 tabLayout.setupWithViewPager(viewPager);
複製程式碼

再次執行,就正常顯示:

Android開發之TabLayout
PagerAdapter有一個getPageTitle方法,下面是它的定義:

/**
 * This method may be called by the ViewPager to obtain a title string
 * to describe the specified page. This method may return null
 * indicating no title for this page. The default implementation returns
 * null.
 *
 * @param position The position of the title requested
 * @return A title for the requested page
 */
public CharSequence getPageTitle(int position) {
    return null;
}
複製程式碼

看到這裡我們不禁有個疑問,為什麼覆蓋PagerAdapter的getPageTitle方法返回指定的Page標題就可以顯示在TabLayout的Tab上呢?想要知道答案,我們得從原始碼入手,從TabLayout關聯ViewPager的setupWithViewPager方法開始,我在下面找到了核心程式碼:

 private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
            boolean implicitSetup) {
         // 省略部分程式碼
         final PagerAdapter adapter = viewPager.getAdapter();
         if (adapter != null) {
             // Now we'll populate ourselves from the pager adapter, adding an observer if
             // autoRefresh is enabled
             setPagerAdapter(adapter, autoRefresh);
         }
         // 省略部分程式碼
    }
複製程式碼

setPagerAdapter方法邏輯:

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
    populateFromPagerAdapter();
}
複製程式碼

最終的邏輯是在內部方法populateFromPagerAdapter方法中將PagerAdapter的getPageTitle返回的標題新增到TabLayout中:

void populateFromPagerAdapter() {
    removeAllTabs();
    if (mPagerAdapter != null) {
        final int adapterCount = mPagerAdapter.getCount();
        for (int i = 0; i < adapterCount; i++) {
            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));
            }
        }
    }
}
複製程式碼

相關文章