MaterialDesign--(7)TabLayout的使用及其源码分析

1,130 阅读9分钟

简介

TabLayout继承自 HorizontalScrollView


TabLayout 提供了一个水平布局来显示标签。所有的 Tab 选项卡实例化都是通过 TabLayout.Tab 完成的。你可以通过 TabLayout.newTab()来创建 Tab 对象。你可以通过更改Tab 的setText()、setIcon()分别设置选项卡的文字和 Icon。要显示选项卡 Tab,你必须通过一个方法 addTab(tab)方法将其添加到布局。例如:

TabLayout tabLayout = ...;
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));

你可以设一个监听setOnTabSelectedListener(OnTabSelectedListener),当任何表情的选择状态改变的时候回调。你也可以在 xml 布局中使用TabItem添加 tab 到TabLayout 里面,例如

<android.support.design.widget.TabLayout
     android:layout_height="wrap_content"
     android:layout_width="match_parent">

 <android.support.design.widget.TabItem
         android:text="@string/tab_text"/>

 <android.support.design.widget.TabItem
         android:icon="@drawable/ic_android"/>
</android.support.design.widget.TabLayout>

结合ViewPager

如果你的 ViewPager 和这个布局用在一起,你可以调用 setupWithVIewPager(ViewPager)两个链接在一起,这种布局将会自动填充 PagerAdapter 的页面标题

你也可以把这种用法当成 ViewPager 的装饰,并且可以这样写布局资源直接添加到 ViewPager 当中:

<android.support.v4.view.ViewPager
 android:layout_width="match_parent"
 android:layout_height="match_parent">

  <android.support.design.widget.TabLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="top" />

</android.support.v4.view.ViewPager>

哈哈哈哈哈,有木有感觉读起来很尴尬,上面这段文字翻译自Google 官方文档,是用我三级没过的英语翻译的,官方文档真的讲的很清楚,大家真的不要恐惧看官方文档。but,上文中的两种 xml写法,我用了这么久 TabLayout,真的是第一次知道。

XML attributes

<declare-styleable name="TabLayout">
 <attr format="color" name="tabIndicatorColor"/>
 <attr format="dimension" name="tabIndicatorHeight"/>
 <attr format="dimension" name="tabContentStart"/>
 <attr format="reference" name="tabBackground"/>
 <attr name="tabMode">
        <enum name="scrollable" value="0"/>
        <enum name="fixed" value="1"/>
    </attr>
    <attr name="tabGravity">
        <enum name="fill" value="0"/>
        <enum name="center" value="1"/>
    </attr>
    <attr format="dimension" name="tabMinWidth"/>
    <attr format="dimension" name="tabMaxWidth"/>
    <attr format="reference" name="tabTextAppearance"/>
    <attr format="color" name="tabTextColor"/>
    <attr format="color" name="tabSelectedTextColor"/>
    <attr format="dimension" name="tabPaddingStart"/>
    <attr format="dimension" name="tabPaddingTop"/>
    <attr format="dimension" name="tabPaddingEnd"/>
    <attr format="dimension" name="tabPaddingBottom"/>
    <attr format="dimension" name="tabPadding"/>
</declare-styleable>
  • tabIndicatorColor 下标颜色
  • tabIndicatorHeight 下标高度
  • tabContentStart 设置左边的 padding
  • tabBackground 背景颜色
  • tabGravity fill:tabs 平均填充整个宽度 center:tabs 居中显示
  • tabMode scrollable:可滑动;fixed:不能滑动,平分tabLayout宽度
  • tabMinWidth tab 的最新宽度
  • tabMaxWidth tab 的最大宽度
  • tabTextAppearance tab 的文字style
  • tabTextColor tab 文字颜色
  • tabSelectedTextColor tab 选中文字颜色
  • tabPadding* tab 的 padding 值

Public methods

方法名 作用
addOnTabSelectedListener() 添加 tab 选中监听
addTab() 添加一个 tab
addView 添加一个 View。注意只能是TabItem,然后最终转换成 tab
clearOnTabSelectedListener() 移除条目选中监听
generateLayoutparams() 获取 layoutParams
getSelectedTabPositing() 获取当前所选标签 position
getTabAt(int index) 获取指定索引的 Tab
gettabCount() 获取 tab 数
getTabGravity() 获取tabGravity
getTabMode() 获取 TabMode
getTabTextColors() 获取选项卡中不同状态颜色
newTab() 创建并返回一个新的 TabLayout.Tab
removeAllTabs() 删除所有选项卡
removeOnTabSelectedListener() 删除所有 OnTabSelectedListener
removeTab(Tab) 移除指定 tab
removetabAt(position) 移除指定 position 的 tab
setOnTabSelectedListener() 等同 addOnTabSelectedListener()
setScrollPosition() 设置选项卡滚动位置
setSelectedTabIndicatorColor() 设置选中下标颜色
setTabGravity() 设置 TabGravity
setTabMode() 设置 TabMode
setTabTextCloros() 设置 tab 的文字颜色
setTabsFromPagerAdapter() 已过期,使用 setupWithViewPager()
setupViewPager() 绑定 ViewPager
shouldDelayChildPressedState() 如果tab可以滚动,只延迟按下状态

其实没什么好写的,基本上看到方法名就能知道是干嘛的,初入 android 开发的同学切记不要死记硬背这些 api,有个大概的印象就行。老司机权当查漏补缺吧。

一张图看懂 TabLayout 的类层次


View层次.jpg

可能有些同学没看懂SlidingTabStrip是什么。TabLayout继承自 HorizontalScrollView,ScrollView 只能添加一个子 View,所以 SlidingTabStrip 就是那个用来添加子View 的HorizontalLinearLayout。

花式玩法

1.tab 之间分割线

Ui 说要在 tab 条目之间添加分割线,很操蛋有木有。拿到需求之后研究了一遍 Api,然而发现并没有提供添加分割线的方法,然后自己去手撸一个 TabLayout。

其实不用手撸,没有Api 我们可以曲线救国。鲁迅当年觉得学医救不了中国人,不也选择了 DJ 来曲线救国么。


用过 ScrollView 的童鞋都知道,ScrollView 只能有一个子 View,因此,ScrollView 都会有一个 LinearLayout。然后 LinearLayout 有个方法setShowDividers()可以设置分割线,而 TabLayout 就算继承自HorizontalScrollView,那么我们是不是可以去找一下,TabLayout 里面的 ScrollView。通过阅读源码,我们找到了这样几行代码

mTabStrip = new SlidingTabStrip(context);
super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
            ...       

private class SlidingTabStrip extends LinearLayout {

这里的mTabStrip就是我们要找的LinearLayout。but,mTabStrip是一个 private 变量。

当然,获取一个 private 变量拦不到我们牛逼的 java 程序员,tabLayout.getClass()...分分钟获取到mTabStrip对象。

stop,我们这里有个优雅的方法获取mTabStrip对象。我们都知道TabLayout 继承自HorizontalScrollView,HorizontalScrollView只能有一个子类!!!tabLayout.getChildAt(0)是不是就获取到了 mTabStrip对象。然后调用以下方法给 LinearLayout 设置分割线即可

 LinearLayout linearLayout = (LinearLayout) toolbar_tab.getChildAt(0);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(ContextCompat.getDrawable(this,R.drawable.divider)); //设置分割线的样式linearLayout.setDividerPadding(20); //设置分割线间隔

这里贴上 R.drawable.divider 的代码
<?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
   <solid android:color="#c0c0c0" />
   <size android:width="1px"/>
 </shape>

绑定 ViewPager

哈哈,其实这个只是基本功能。两个步骤1.如果你的 TabLayout 的节点不是在 ViewPager 节点内部,需要把TabLayout 和 ViewPager 绑定起来。否则可以跳过这一步

 mTabLayout.setupWithViewPager(mViewPager);

2.重写 PagerAdapter 的 getPageTitle()方法。

自定义指示器的长度

UI 说,指示器的长度不要充满屏幕~
这里有个办法通过反射的方式修改指示器长度,如果需要指示器宽度等于文字宽度需要自己微调。原理就是通过反射的方式获取 TabLayout 的字段 mTabStrip,然后再去遍历修改每一个子 View 的 padding 值。代码如下:

 /**
 * 通过反射设置TabLayout每一个的长度
 * @param left 左边 Padding 单位 dp
 * @param right 右边 Padding 单位 dp
 */
public void setIndicator(TabLayout tabLayout, int left, int right) {
    Class<?> tabLayoutClass = tabLayout.getClass();
    Field tabStrip = null;
    try {
        tabStrip = tabLayoutClass.getDeclaredField("mTabStrip");
        tabStrip.setAccessible(true);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }

    LinearLayout llTab = null;
    try {
        llTab = (LinearLayout) tabStrip.get(tabLayout);
    } catch (Exception e) {
        e.printStackTrace();
    }

    int l = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, left, Resources.getSystem().getDisplayMetrics());
    int r = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, right, Resources.getSystem().getDisplayMetrics());

    if (llTab != null) {
        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 = l;
            params.rightMargin = r;
            child.setLayoutParams(params);
            child.invalidate();
        }
    }
}

自定义 Tab

可能有些童鞋不满足于TabLayout 当前的定制,想要完全自定义。可以的~很有想法大家对这种方式添加一个 Tab 条目肯定不陌生吧

 tabLayout.addTab(tabLayout.newTab());

tabLayout.newTab()的返回值是一个TabLayout.Tab。既然 tabLayout.addTab(Tab)就能添加一个条目,那么可以大胆的断定 Tab 就是代表一个条目,然后我们通过查看源码可以知道 tab.getCustomView()可以获得这个 View,这时就很简单了,你可以直接设置这个 mCustomView ,然后自己处理 mCustomView 的显示。

能干什么?比如说首页底部导航,选中条目放大等等。。。。自由发挥

源码分析

这个源码好像比较简单,我们先从构造方法开始

 public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
   //检查当前主题是否是 AppCompat 系列的,否则报错,里面代码就只有几行
    ThemeUtils.checkAppCompatTheme(context);

    // Disable the Scroll Bar 禁用滚动条
    setHorizontalScrollBarEnabled(false);

    // Add the TabStrip 创建SlidingTabStrip,
    // 以后 tabView 就是添加到这里面
    mTabStrip = new SlidingTabStrip(context);
    super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
            LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
            defStyleAttr, R.style.Widget_Design_TabLayout);

      ...读取attributes属性代码,省略

    final Resources res = getResources();
    //设置默认文字大小12sp
    mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
    //设置默认最小宽度72dp
    mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);

    // Now apply the tab mode and gravity
    //设置 mode 和 gravity,不明白这两个属性的请回头看attributes
    applyModeAndGravity();
}

//这个应该能看懂吧~~~ 
 private void applyModeAndGravity() {
    int paddingStart = 0;
    if (mMode == MODE_SCROLLABLE) {
        // If we're scrollable, or fixed at start, inset using padding
        paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
    }
    ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);

    switch (mMode) {
        case MODE_FIXED:
            mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
            break;
        case MODE_SCROLLABLE:
            mTabStrip.setGravity(GravityCompat.START);
            break;
    }

    updateTabViews(true);
}
 //遍历所有子 View,并更新 LayoutParams
void updateTabViews(final boolean requestLayout) {
    for (int i = 0; i < mTabStrip.getChildCount(); i++) {
        View child = mTabStrip.getChildAt(i);
        child.setMinimumWidth(getTabMinWidth());
        updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
        if (requestLayout) {
            child.requestLayout();
        }
    }
}
//layoutParams 属性请参照 mode 和 gravity 的属性看
private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
    if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
        lp.width = 0;
        lp.weight = 1;
    } else {
        lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
        lp.weight = 0;
    }
}
 -----------构造方法结束-----
 -----如何添加一个 tab------
 //创建了一个Tab对象,并持有对 TabLayout 的引用
 //这里的 sTabPool 继承自 pool,一个可以设置最大创建个数的工具类,如果
 //超过最大创建个数则不再创建返回 null,这里还加了一个非空判断,表示没看懂
 //为什么要用Pools.SynchronizedPool来创建Tab
 public Tab newTab() {
    Tab tab = sTabPool.acquire();
    if (tab == null) {
        tab = new Tab();
    }
    tab.mParent = this;
    //创建一个 TabView,TabView 就是真正的每个条目View
    //Tab 只是一个简单的 View Model
    tab.mView = createTabView(tab);
    return tab;
}
//创建 TabView
private TabView createTabView(@NonNull final Tab tab) {
    TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
    if (tabView == null) {
        tabView = new TabView(getContext());
    }
    tabView.setTab(tab);
    tabView.setFocusable(true);
    tabView.setMinimumWidth(getTabMinWidth());
    return tabView;
}
//添加到
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
    if (tab.mParent != this) {
        throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
    }
    //配置这个方法不重要,就几行代码
    configureTab(tab, position);
    //调用方法添加 TabView 到mTabStrip里面
    addTabView(tab);
    if (setSelected) {
     //设置条目选中
        tab.select();
    }
}
//添加TabView到mTabStrip里的执行方法
private void addTabView(Tab tab) {
    final TabView tabView = tab.mView;
    mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
}
//设置 Tab 选中,并且将之前选中的 Tab 设为未选中状态
//然后更新下标 updateIndicator
 void selectTab(Tab tab) {
    selectTab(tab, true);
}
----------------------------
--------ViewPager 绑定-------
-----------------------------
//上文说过,绑定 ViewPager 只需要一行代码mTabLayout.setupWithViewPager(mViewPager)
//那么我们就从这个方法开始看
private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
        boolean implicitSetup) {
    if (mViewPager != null) {
        // If we've already been setup with a ViewPager, remove us from it
        if (mPageChangeListener != null) {
            mViewPager.removeOnPageChangeListener(mPageChangeListener);
        }
        if (mAdapterChangeListener != null) {
            mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);
        }
    }

    if (mCurrentVpSelectedListener != null) {
        // If we already have a tab selected listener for the ViewPager, remove it
        removeOnTabSelectedListener(mCurrentVpSelectedListener);
        mCurrentVpSelectedListener = null;
    }

    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
            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);
    }
 //这个变量我没看懂有什么用,private,没有提供修改方法,
 //几个赋值的地方都是被赋值为 false。
    mSetupViewPagerImplicitly = implicitSetup;
}

好了,看完了,踏马源码里面都有写代码注释,我三级的英语水平都看得懂,这里为了给大家原值原味的感觉,我就不再翻译了,希望大家阅读愉快。

好了,TabLayout 分析到此结束。本来还想写 SearchView 和 CardView 的,但是我觉得从MaterialDesign(1)开始看过来的朋友现在都已经学会了自己去看源码,所以下一篇不准备写 View 了,没意思。
明天一起来学沉浸式设计以及沉浸式设计里面的那些坑。
加油~Coder。加油~Android Developer。