JasonGao Thoughts

关于TabLayout的indicator宽度的一次探索

2019-03-22

本文的微信公众号链接:关于TabLayout的indicator宽度的一次探索

关于TabLayout,做过android开发的都知道,在android开发中应用特别广泛。这里来简单记录一下前不久遇到的关于TabLayout的一个问题。

先来看看下面一张图,

这里写图片描述
这是我们app首页顶部的部分截图,典型的TabLayout的应用场景,现在遇到一个问题,设计需要TabLayout的indicator的宽度能够与Tab的文字等宽,甚至需要比文字的宽度短。

那么现在就需要想办法解决这个问题了,另外需要注意的是,这里的TabLayout是与ViewPager绑定的,需要兼容原来的TabLayout的的逻辑。

首先,想想能否在原生TabLayout的基础上,做一些调整。查看TabLayout的源码,我们发现TabLayout的有一个私有内部类SlidingTabStrip,这个SlidingTabLayout继承自LinearLayout,TabLayout的每个Tab的indicator就是由这个类负责绘制的。

SlidingTabStrip的draw方法
这里写图片描述

可以看到,Tab的indicator的是SlidingTabStrip绘制的一个Rect,这个Rect的宽度是由IndicatorLeft和mIndicatorRight来决定的,看到上面截图的第一个箭头所指的地方,注意到这两行代码left = selectedTitle.getLeft(); right = selectedTitle.getRight();基本可以确定每个Indicator的宽度是和每个Tab的宽度确定的。所以,可以考虑针对这个特点做点调整。

第一种方法:通过反射设置TabLayout每个Tab的Margin值来实现压缩Tab的indicator宽度的效果。

链接地址:方法一

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tabLayoutThree.post(new Runnable() {
@Override
public void run() {
try {
Class<?> tablayout = tabLayoutThree.getClass();
Field tabStrip = tablayout.getDeclaredField("mTabStrip");
tabStrip.setAccessible(true);
LinearLayout ll_tab= (LinearLayout) tabStrip.get(tabLayoutThree);
for (int i = 0; i < ll_tab.getChildCount(); i++) {
View child = ll_tab.getChildAt(i);
child.setPadding(0,0,0,0);
LinearLayout.LayoutParams params = new
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT,1);
params.setMarginStart(dip2px(MainActivity.this,1f));
params.setMarginEnd(dip2px(MainActivity.this,15f));
child.setLayoutParams(params);
child.invalidate();
}
} catch (Exception e) {

}
}
});

这种情况下,需要指定TabLayout的tabMode为fixed,如果设置为scrollable,那么这段代码是不生效的。

来看看这段代码的运行效果:
这里写图片描述
这种情况下基本可以满足需求,这里的indicator的宽度基本和Tab的文字是等宽的。这种解决方法勉强可以达到这种需求,但是这里有个问题是当Tab的文字不统一的时候,Indicator的宽度会由最大的那个Tab的宽度决定。

可以看到下面这个截图的效果:
这里写图片描述

如上截图,我们发现,当第二个tab的文字变长的时候,第一个tab的Indicator的宽度马上就超过了tab文字的宽度,作为一个追求完美的程序员,这样还是显得很别扭,所以接下来我们再来看看第二种方法。

第二种方法:同样是通过反射,这里的处理方式不是设置每个SlidingTabStrip的margin,而是通过反射改变TabLayout中的每个Tab中的TabView的margin来达到这种效果,这样我们每个Tab的Indicator的宽度不会是由最大的那个Tab文字宽度决定的。

链接地址:方法二

贴上链接地址,这个作者针对第一种方式做了一些改善,我觉得文章写得很不错,不仅针对第一种方式给出了具体的方法,而且分析了为什么采取这种方式。

这里简单总结下,你也可以点击上面的链接看原作者的分析。我们注意到,TabLayout内部对Tab的宽度有个特殊的处理,在onMeasure方法中,如下面的截图,首先循环遍历,取出Tab的最大宽度,然后再循环将最大的宽度设置为每个Tab的宽度,这样tab的宽度总是以所有tab中最大宽度的那个值来决定,而我们每个Tab的Indicator的width是和每个Tab的宽度相关的.
这里写图片描述

我们来看下关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
tabLayoutTwo.post(new Runnable() {
@Override
public void run() {
try {
//了解源码得知 线的宽度是根据 tabView的宽度来设置的
LinearLayout mTabStrip = (LinearLayout) tabLayoutTwo.getChildAt(0);
int dp10 = dip2px(tabLayoutTwo.getContext(), 10);

for (int i = 0; i < mTabStrip.getChildCount(); i++) {
View tabView = mTabStrip.getChildAt(i);

//拿到tabView的mTextView属性 tab的字数不固定一定用反射取mTextView
Field mTextViewField =
tabView.getClass().getDeclaredField("mTextView");
mTextViewField.setAccessible(true);

TextView mTextView = (TextView) mTextViewField.get(tabView);

tabView.setPadding(0, 0, 0, 0);

//因为我想要的效果是 字多宽线就多宽,所以测量mTextView的宽度
int width = 0;
width = mTextView.getWidth();
if (width == 0) {
mTextView.measure(0, 0);
width = mTextView.getMeasuredWidth();
}

//设置tab左右间距为10dp 注意这里不能使用Padding
// 因为源码中线的宽度是根据 tabView的宽度来设置的
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) tabView.getLayoutParams();
params.width = width ;
params.leftMargin = dp10;
params.rightMargin = dp10;
tabView.setLayoutParams(params);

tabView.invalidate();
}

} catch (Exception e) {

}
}
});

通过反射拿到每个TabView的宽度,拿到TabView中每个TextView宽度,将TextView的宽度设置给每个TabView,这样我们的Tab的宽度就被压缩了,同时,每个Tab的Indicator的宽度就能和Tab的文字部分保持同样的宽度了。

这种方式的效果截图如下:
这里写图片描述

这样看,是不是就没有上面的问题了。每个Tab的Indicator的width基本可以合Tab的文字宽度保持一致了。但是这里还有一个问题,就是采用这种方法虽然可以解决每个Tab的Indicator宽度太长的问题,但是,上面两种方法都有一个特点,就是通过反射将每个tab的padding设置为0,这样会导致每个tab的可点击区域变得很小,我么打开开发者模式里面显示布局边距这个选项来看下我们刚才的解决方法运行的截图:
这里写图片描述
通过上面的截图可以清楚地发现每个tab的可点击区域被压缩得很窄。虽然这个在某些方面看来是可以接受的,但是我们来看文章最开始这张图。

这里写图片描述

这里我么如果采取上面两种方式来压缩Tab来改变Indicator的width的话,就会导致每个Tab的边距变得很小,这样,就会导致Tab的可点击区域变小,这对于app的首页来说,肯定是不能接受的。所以,针对这一点来说,上面两种方式都是不符合要求的,所以需要找另一种方法来解决这个问题。

第三种方法:这种方法简单来说就是改源码。因为目前遇到的问题是,通过反射修改原生TabLayout一些属性并不能完全达到所需要的效果,但是自定义选择自定义TabLayout的成本相对比较大,而且我不想很大地去改动TabLayout的一些调用逻辑,因为我想通过很小的改动来达到这种效果,所以我选择这种方式来改。

简单来说,就是将原生TabLayout代码复制一份,作为自己的代码,这样可以很容易地改动里面的代码,在使用的时候直接引用我们自己的这个TabLayout就行。

接下来我们需要改原生TabLayout的一些逻辑,在保留原生TabLayout一些自定义属性的基础上,添加可以自定义Indicator的width的属性,
这里写图片描述

在代码中主要改两个地方,第一个地方如下
这里写图片描述

在updateIndicatorPosition方法中,这里在处理Indicator的宽度的时候不是以Tab的左边坐标来算,将Indicator的left和right进行如下设置。

1
2
left = selectedTitle.getLeft() + (selectedTitle.getWidth() - mSelectedIndicatorWidth) / 2;
right = left + mSelectedIndicatorWidth;

并且,对于Indicator滑动中的状态也做相应的处理,即当mSelectionOffset大于0时对Indicator的left和right做同样的处理。

第二个改动的地方:
这里写图片描述

这里改动的是animateIndicatorToPosition方法,根据名字就可以基本知道这个是对Tab切换时Indicator的动画处理。

如上截图,可以知道这个targetView就是TabLayout的每个Tab,对于targetLeft和targetRight我们做上面同样的处理。

接下来我们可以验证下我们这样的改动方法是否有效,简单看下效果图:
这里写图片描述

可以看到,我们可以让TabLayout的Tab的可点击区域保持不变,并且可以修改Tab下面的Indicator的宽度,这里我们提供了TabLayout的Indicator的width的自定义。

不多说,最后看下统一的效果截图:
这里写图片描述

Demo地址,如果觉得看文章不是特别清楚,可以直接去看Demo中的代码。

最后,感谢大家阅读这篇文章,如果觉得写得还不错的话,点个赞呗,实在不行,留个言也可以接受。

公众号同步更新,欢迎关注😄