TabLayout我们再熟悉不过了,在开发中,像这种tab切换的需求都会用到TabLayout,它是由官方提供的一个控件,在support design 包中。使用起来非常简单方便,交互效果也很不错,能满足我们开发中95%的需求。但是它有一个缺陷:不能改变Tab下划线(Indicator)的宽度。本篇文章给你带来改变Tab下划线宽度的几种方式:
1 . 通过反射设置Tab下划线的宽度
2 . 通过TabLayout setCustomView 的方式
3 . 使用第三方开源库。
一、通过反射的方式,改变TabLayout下划线的宽度
首先我们看一下原生的TabLayout的效果(没有任何修改):
gif演示:
上图第一个固定模式(tabMode:fixed),下面是滚动模式(tabMode:scrollable),可以看到,所有Tab下方的线(即Indicator)是一样长的,不管Tab的内容是长还是短。Tab indicator的长度与最长的Tab保持一致。
TabLayout提供了tabIndicatorHeight
属性来设置indicator的高度,但是没有提供设置宽度的的api,要想改变indicator的宽度,就得去看看源码indicator是怎么实现的。简单的看一下源码:
如上思维导图,其中有两个重点的东西, TabView
和 SlidingTabStrip
,TabView
就是我们所看到的Tab,SlidingTabStrip
是TabView
的父容器,继承自LinearLayout
,用来处理Tab滑动相关操作,如动画,绘制Indicator等。
我们要研究indicator是怎么添加的,重点就在SlidingTabStrip
里了,这里我们看到了mSelectedIndicatorHeight
,这就是我们设置Indicator的高度,在draw
方法里有如下代码:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
这就是绘制的选中Tab的Indicator,高度是mSelectedIndicatorHeight
,宽是mIndicatorRight - mIndicatorLeft 。那么者两个值是从哪儿来的呢?在updateIndicatorPosition
方法中:
private void updateIndicatorPosition() {
// 选中的TabView
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
// left 和right 的值
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
// 设置mIndicatorLeft和mIndicatorRight
setIndicatorPosition(left, right);
}
void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
**从上面的代码就可以看出,Indicator(Tab选中下划线)的宽度其实就是TabView的宽度,那么TabView的宽度是多少呢?在SlidingTabStrip
的onMeasure
方法中,为TabView设置了宽度。**请看代码:
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
//以上省略
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
final int count = getChildCount();
// First we'll find the widest tab
//google的工程师注释写的非常清楚:第一步,找出宽度最长的Tab
int largestTabWidth = 0;
for (int i = 0, z = count; i < z; i++) {
View child = getChildAt(i);
if (child.getVisibility() == VISIBLE) {
largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
}
}
if (largestTabWidth <= 0) {
// If we don't have a largest child yet, skip until the next measure pass
return;
}
final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
boolean remeasure = false;
if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
// If the tabs fit within our width minus gutters, we will set all tabs to have
// the same width
// 第二步:将所有Tab的宽度都设置为largestTabWidth
for (int i = 0; i < count; i++) {
final LinearLayout.LayoutParams lp =
(LayoutParams) getChildAt(i).getLayoutParams();
if (lp.width != largestTabWidth || lp.weight != 0) {
lp.width = largestTabWidth;
lp.weight = 0;
remeasure = true;
}
}
} else {
// If the tabs will wrap to be larger than the width minus gutters, we need
// to switch to GRAVITY_FILL
mTabGravity = GRAVITY_FILL;
updateTabViews(fals