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

简介

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容