目录
前言
TabLayout是一个遵循Material Design设计规范的官方控件,也是日常工作中最常用的控件之一,手机上随便打开一个APP,都能看到它的身影。TabLayout可以为一个多页面的布局提供页面指示器,使得一个Activity或者Fragment中能够展示更加丰富的内容。结合ViewPage使用更是可以实现丰富的页面切换效果,并能带来流畅的页面切换体验。
TabLayout的功能强大,使用灵活,许多属性都支持使用者自由的定义,甚至可以直接给Tab指定一个布局。然而如此强大的控件却偏偏不支持修改底部指示器的宽度,不得不说,写这个控件的工程师的心思是真的难以捉摸啊!
看看这个效果,是不是有些难以直视。再看看主流的APP,几乎都会对这个指示器宽度做一定的修改。UI设计的时候,这里通常也会带有公司的风格。指示器的宽度这么长,再好看的指示器也会被拉伸到变形的。这里就给出几种常见的修改方案
TabLayout的视图结构
TabLayout继承自HorizontalScrollView,具有横向滚动的功能。它拥有ScrollView的特点,即内部只能有一个子布局。TabLayout初始化时,会调用一次addView()来添加这个布局。
super.addView(slidingTabIndicator, 0, new HorizontalScrollView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
同样我们也可以根据index = 0来获取这个布局
View view = tabLayout.getChildAt(0);
它是一个SlidingTabIndicator,继承自LinearLayout。翻译过来就是滑动标签指示器,也是TabView的直接父布局。你可能疑惑了,指示器不是指的TabView底部的横线么,怎么这个TabLayout的唯一子布局也叫指示器呢?
简单理解来说,TabView底部的指示器是视觉上的指示器,其作用是给用户标识当前被选中的Tab。我们常说的改指示器宽度就是修改它的宽度,它并不是一个View,本质是SlidingTabIndicator绘制在特定位置的一个Drawable。
而SlidingTabIndicator是数据上的指示器,其作用是给与TabLayout联动的视图,如ViewPager,标识出当前被选中的位置,以便于联动视图做出相应的操作。因此对于联动视图来讲,它也可以叫指示器。
TabLayout持有一个Lis< Tab >来管理各个Tab,每个Tab会有属于自己的视图TabView。当TabLayout增加Tab时,会调用SlidingTabIndicator的addView方法,将Tab的TabView添加进来
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
...
configureTab(tab, position);
addTabView(tab);
...
}
private void addTabView(@NonNull Tab tab) {
final TabView tabView = tab.view;
tabView.setSelected(false);
tabView.setActivated(false);
slidingTabIndicator.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
}
因此TabLayout的视图结构应该如下图所示。其中指示器并不是一个控件,它是绘制在SlidingTabIndicator底部的一个Drawable。
TabLayout绘制指示器的过程
前面提到,所谓的指示器其实只是一个TabLayout中的一个Drawable对象,TabLayout还为它提供了相应的getter和setter。
@Nullable Drawable tabSelectedIndicator;
当Tab切换时,对指示器的操作如下
(1) 获取tabSelectedIndicator的边界
(2) 调用tabSelectedIndicator的draw()方法,将tabSelectedIndicator绘制出来
步骤已经明了,如此就可以通过修改某一步骤的过程来实现指示器宽度的修改。
1.从获取tabSelectedIndicator边界着手
1.1 默认情况下的指示器宽度
默认情况下使用被选中TabView的宽度,即文章开始时图中看到的指示器充满整个TabView的情况。它使用当前TabView的宽度作为指示器宽度
.....
View selectedTitle = getChildAt(this.selectedPosition);
int left;
int right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
// 使用当前选中项的边界作为指示器的边界
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
......
}
// 设置指示器的左右边界
setIndicatorPosition(left, right)
1.2 tabIndicatorFullWidth="false"时,指示器宽度
当设置了tabIndicatorFullWidth="false"时,遍历当前TabView的子控件,获取其中left的最小值作为指示器的左边界,right的最大值作为指示器的右边界。在大多数情况下文字才是一个Tab中宽度最大的,因此可以认为这种情况下指示器的宽度与文字宽度相同。
......
if (!tabIndicatorFullWidth && selectedTitle instanceof TabView) {
calculateTabViewContentBounds((TabView) selectedTitle