原生TabLayout依旧很香,信不信由你,反正我是信了

TabLayout是项目开发中常用的一个控件,常和ViewPager结合使用。本文基于Androidx com.google.android.material:material:1.0.0版本根据TabLayout源码对其进行分析并对其深度定制。TabLayout源码不足2000行,短小精悍,小巧精致,非常适合我们阅读研究。其实大部分场景下,TabLayout原有的功能或对TabLayout修改定制一下,便可满足我们的需求。原生的往往是最好的,抱紧Google大腿就对了。

TabLayout整体结构

TabLayout类关系图

默认Style

TabLayout默认styleWidget.Design.TabLayout,定义如下:

  <dimen name="design_tab_max_width">264dp</dimen>
  <dimen name="design_tab_scrollable_min_width">72dp</dimen>
  <dimen name="design_tab_text_size">14sp</dimen>
  <dimen name="design_tab_text_size_2line">12sp</dimen>
  <integer name="design_tab_indicator_anim_duration_ms">300</integer>
  <style name="Base.Widget.Design.TabLayout" parent="android:Widget">
    <item name="android:background">@null</item>
    <item name="tabIconTint">@null</item>
    <item name="tabMaxWidth">@dimen/design_tab_max_width</item>
    <item name="tabIndicatorAnimationDuration">@integer/design_tab_indicator_anim_duration_ms</item>
    <item name="tabIndicatorColor">?attr/colorAccent</item>
    <item name="tabIndicatorGravity">bottom</item>
    <item name="tabIndicator">@drawable/mtrl_tabs_default_indicator</item>
    <item name="tabPaddingStart">12dp</item>
    <item name="tabPaddingEnd">12dp</item>
    <item name="tabTextAppearance">@style/TextAppearance.Design.Tab</item>
    <item name="tabRippleColor">?attr/colorControlHighlight</item>
    <item name="tabUnboundedRipple">false</item>
  </style>

  <style name="Widget.Design.TabLayout" parent="Base.Widget.Design.TabLayout">
    <item name="tabGravity">fill</item>
    <item name="tabMode">fixed</item>
    <item name="tabIndicatorFullWidth">true</item>
  </style>

tabIndicator默认为高度为2dp,颜色为white的矩形,对应的文件为mtrl_tabs_default_indicator,定义如下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item>
    <shape android:shape="rectangle">
      <solid android:color="@android:color/white"/>
      <size android:height="2dp"/>
    </shape>
  </item>
</selector>

TextAppearance.Design.Tab对应的定义为:

 <style name="TextAppearance.Design.Tab" parent="TextAppearance.AppCompat.Button">
    <item name="android:textSize">@dimen/design_tab_text_size</item>
    <item name="android:textColor">@color/mtrl_tabs_legacy_text_color_selector</item>
    <item name="textAllCaps">true</item>
  </style>

mtrl_tabs_legacy_text_color_selector对应的定义为:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:color="?attr/android:textColorPrimary" android:state_selected="true"/>
  <item android:color="?attr/android:textColorSecondary"/>
</selector>

相关属性

  • tabMode
    取值为scrollablefixed,默认为fixedscrollable表示TabLayout是可以滑动的,适用于Tab数量较多的情况,如新闻资讯类APP首页。fixed表示宽度固定,适用于Tab数量固定的情况。
<attr name="tabMode">
   <enum name="scrollable" value="0"/>
   <enum name="fixed" value="1"/>
</attr>
tabMode为scrollable时显示效果
tabMode为fixed时显示效果
  • tabGravity
    取值为fillcenter,默认值为fill。此属性只在app:tabMode="fixed"时起作用,fill表示平分宽度模式,center表示居中显示模式
<attr name="tabGravity">
   <enum name="fill" value="0"/>
   <enum name="center" value="1"/>
 </attr>
private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
     if (this.mode == MODE_FIXED && this.tabGravity == GRAVITY_FILL) {
          lp.width = 0;
          lp.weight = 1.0F;
     } else {
          lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
          lp.weight = 0.0F;
     }
 }
tabGravity为fill时显示效果
tabGravity为center时显示效果
  • tabPaddingStart|tabPaddingTop|tabPaddingEnd|tabPaddingBottom
    tabPaddingStart表示每个Tab的左边距,默认值为12dptabPaddingTop表示每个Tab的上边距,默认值为0dptabPaddingEnd表示每个Tab的右边距,默认值为12dptabPaddingBottom表示每个Tab的下边距,默认值为0dp
  public TabView(Context context) {
     super(context);
     this.updateBackgroundDrawable(context);
     ViewCompat.setPaddingRelative(this, TabLayout.this.tabPaddingStart,TabLayout.this.tabPaddingTop,TabLayout.this.tabPaddingEnd, TabLayout.this.tabPaddingBottom);
     this.setGravity(Gravity.CENTER);
     this.setOrientation(TabLayout.this.inlineLabel ? HORIZONTAL : VERTICAL);
     this.setClickable(true);
     ViewCompat.setPointerIcon(this, PointerIconCompat.getSystemIcon(this.getContext(), 1002));
   }   
  • tabMinWidth
    此属性表示每个Tab的最小宽度,默认值为-1。如果tabMinWidth有设置,则Tab的最小宽度为设定的值,否则如果 app:tabMode="scrollable"Tab最小宽度为scrollableTabMinWidth72dp,其余情况最小宽度为0
 private int getTabMinWidth() {
     if (this.requestedTabMinWidth != -1) {
         return this.requestedTabMinWidth;
     } else {
         return this.mode == MODE_SCROLLABLE ? this.scrollableTabMinWidth : 0;
     }
 }
  • tabMaxWidth
    此属性表示每个Tab的最大宽度,默认值为264dp此属性在TabLayout适配平板电脑时有用,需要同时设置app:tabGravity="fill"app:tabMaxWidth="0dp"才能平分屏幕宽度
 <com.google.android.material.tabs.TabLayout
     app:tabMode="fixed"
     app:tabGravity="fill"
     app:tabMaxWidth="0dp"
   />

分析一下原因,主要有以下两个方面的原因:

  1. 针对平板电脑,TabLayout的默认styletabGravity属性值被修改了,由fill改为了center,使用的stylecom.google.android.material:material:1.0.0包下的res\values-sw600dp-v13\Widget.Design.TabLayout,定义如下:
 <style name="Widget.Design.TabLayout" parent="Base.Widget.Design.TabLayout">
    <item name="tabGravity">center</item>
    <item name="tabMode">fixed</item>
 </style>

可根据以下代码判断当前机器是否为平板电脑:

 fun isPad(context: Context): Boolean {
    return context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE
 }

2.TabLayout源码中对tabMaxWidth属性的处理

 int tabMaxWidth;
 private final int requestedTabMaxWidth;
 public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
   super(context, attrs, defStyleAttr);
   this.tabMaxWidth = 2147483647;
   this.requestedTabMaxWidth = a.getDimensionPixelSize(styleable.TabLayout_tabMaxWidth, -1);
 }

TabLayoutonMeasure方法里,如果requestedTabMaxWidth>0,则tabMaxWidth 值为requestedTabMaxWidth,默认为264dp,否则tabMaxWidth值为specWidth - this.dpToPx(56),即其值为屏幕宽度 - 56dp

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   ......省略部分代码
   int specWidth = MeasureSpec.getSize(widthMeasureSpec);
   if (MeasureSpec.getMode(widthMeasureSpec) != 0) {
       this.tabMaxWidth = this.requestedTabMaxWidth > 0 ? this.requestedTabMaxWidth : specWidth - this.dpToPx(56);
   }
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   ......省略部分代码
 }

TabViewonMeasure方法里,根据tabMaxWidth值计算测量自身的宽度

 int getTabMaxWidth() {
    return this.tabMaxWidth;
 }

 public void onMeasure(int origWidthMeasureSpec, int origHeightMeasureSpec) {
    int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
    int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
    int maxWidth = TabLayout.this.getTabMaxWidth();
    int widthMeasureSpec;
    if (maxWidth <= 0 || specWidthMode != 0 && specWidthSize <= maxWidth) {
       widthMeasureSpec = origWidthMeasureSpec;
    } else {
       widthMeasureSpec = MeasureSpec.makeMeasureSpec(TabLayout.this.tabMaxWidth, MeasureSpec.AT_MOST);
    }
   super.onMeasure(widthMeasureSpec, origHeightMeasureSpec);
   ......省略部分代码
 }

TabLayoutEx

基于com.google.android.material:material:1.0.0中TabLayout源码修改而来

亮点

  • 基于原生TabLayout源码修改而来,支持原TabLayout所有功能,用法也基本保持一致
  • 取消原生TabLayout默认将文字转换为大写的属性
  • 添加选中字体变大和加粗效果
  • 添加Tab圆角背景动画,支持背景越界回弹效果
  • 添加指示符跳跃动画

截图

基于原生TabLayout的TabLayoutEx

相关属性

TabLayout原有的属性基本都支持,此处仅列出新添加的属性

属性名称 类型 说明
tabUnSelectedTextSize dimension 未选中字体大小
tabSelectedTextSize dimension 选中字体大小
tabBoldWhenSelected boolean 选中字体是否加粗
tabBackgroundIsCorner boolean 是否使用圆角背景
tabSlideAnimType enum 跳跃动画样式,none表示不启用跳跃动画,half_glue表示启用跳跃动画1,glue表示启用跳跃动画2

TabLayoutEx和原生TabLayout功能相同但名字有修改的属性

  • tabMode改为tabModeEx
  • tabGravity改为tabGravityEx
  • tabIconTintMode改为tabIconTintModeEx
  • tabIndicatorGravity改为tabIndicatorGravityEx

用法

<com.github.kongpf8848.viewworld.views.TabLayoutEx
    android:id="@+id/tab_layout"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_marginTop="10dp"
    android:background="@color/white"
    <!--每个TabView的左边距-->
    app:tabPaddingStart="10dp"
    <!--每个TabView的右边距-->
    app:tabPaddingEnd="10dp"
    <!--SlidingTabIndicator的左边距,其值=app:tabPaddingStart+实际的左边距-->
    app:tabContentStart="25dp"
    <!--tab模式,scrollable或fixed-->
    app:tabModeEx="scrollable"
    <!--指示符和TabView宽度是否相同-->
    app:tabIndicatorFullWidth="true"
    <!--指示符高度-->
    app:tabIndicatorHeight="32dp"
    <!--未选中文字颜色-->
    app:tabTextColor="#999999"
    <!--选中文字颜色-->
    app:tabSelectedTextColor="@color/black"
    <!--点击波纹颜色,透明即去除波纹-->
    app:tabRippleColor="@color/transparent"
    <!--未选中文字大小-->
    app:tabUnSelectedTextSize="14sp"
    <!--选中文字大小-->
    app:tabSelectedTextSize="16sp"
    <!--是否为圆角背景-->
    app:tabBackgroundIsCorner="true"
    <!--选中字体是否加粗-->
    app:tabBoldWhenSelected="true"
    />

GitHub

https://github.com/kongpf8848/ViewWorld

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

推荐阅读更多精彩内容