前言
默认情况下,TabLayout 中 Indicator 的宽度和该 Tab 的宽度相等。但是有时候我们需要自定义 Indicator 的宽度,所以本文将介绍改变 Indicator 的宽度的几种方法。
一、反射实现
第一种方法是通过反射的方式改变 Indicator 的宽度。
TabLayout 只提供了 app:tabIndicatorHeight
属性来设置 Indicator 的宽度,但没有提供设置宽度的 API。所以只能自己去修改了,先看下源码是怎么实现 Indicator 的。
源码分析
在 TabLayout 中,定义了一个 SlidingTabStrip 对象:1
private final SlidingTabStrip mTabStrip;
SlidingTabStrip 继承自 LinearLayout,负责处理 Tab 滑动的相关操作。在它的 draw 方法中,有绘制 Indicator 相关的代码:1
2
3
4
5
6
7
8
9
public void draw(Canvas canvas) {
super.draw(canvas);
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
其中 mIndicatorLeft 和 mIndicatorRight 决定 Indicator 的宽度,宽度为 mIndicatorRight - mIndicatorLeft。
而 mIndicatorLeft 和 mIndicatorRight 最终是在 SlidingTabStrip 的 updateIndicatorPosition() 方法设置的:1
2
3
4
5
6
7
8private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition); // 当前 Tab
int left, right;
// 最终 left 和 right 会赋给 mIndicatorLeft 和 mIndicatorRight
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
}
可以看到,最终 Indicator 的宽度就是 SlidingTabStrip 中子 View 的宽度。
也就是说,想要改变 Indicator 的宽度,只需设置 SlidingTabStrip 中子 View 的 宽度即可。
代码实现
- 得到 SlidingTabStrip 对象,这里不需要反射,因为 SlidingTabStrip 对象是 TabLayout 的第一个子 View,通过 tabLayout.getChildAt(0) 即可得到 SlidingTabStrip 对象。
- 设置 SlidingTabStrip 中相应子 View 的宽度。这里设置的宽度不能小于 TextView 的宽度,不然文字会显示不全。这时需要通过反射来得到 TextView,因为 TextView 是 TabView 的子 View,而 TabView 是 TabLayout 中的非公有内部类,外部无法直接访问 TabView。
完整代码如下: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/**
* 通过反射的方式改变 Indicator 的宽度
*
* @param tabLayout 要改变的 TabLayout
* @param width Indicator 的宽度
* @param margin Tab 之间的 margin
*/
public static void setIndicatorWidth(final TabLayout tabLayout, final int width, final int margin) {
tabLayout.post(new Runnable() {
public void run() {
try {
LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0);
for (int i = 0; i < mTabStrip.getChildCount(); i++) {
// 得到当前子 View(TabView)
View child = mTabStrip.getChildAt(i);
// 得到 TextView 的宽度
Field textViewField = child.getClass().getDeclaredField("textView");
textViewField.setAccessible(true);
TextView mTextView = (TextView) textViewField.get(child);
child.setPadding(0,0,0,0);
int textViewWidth = mTextView.getWidth();
if (textViewWidth == 0) {
mTextView.measure(0,0);
textViewWidth = mTextView.getMeasuredWidth();
}
// 设置该子 View 的相关参数
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
lp.width = textViewWidth > width ? textViewWidth : width;
lp.leftMargin = margin;
lp.rightMargin = margin;
child.setLayoutParams(lp);
child.invalidate();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
这种方式存在的不足:
- 如果升级 sdk 后,sdk 中获取反射的变量名发生改变,代码也要跟着变,不然会失效。
- 这种方式最小只能把 Indicator 的宽度设置成 TextView 的宽度,如果宽度更小的话,TextView 的文本就会被压缩。
二、SDK 28+ 属性配置
如果你使用的 SDK 版本在 28 以上,并且需要将 Indicator 的宽度设置成和文字宽度一样,那么只需在 xml 中给 TabLayout 加上 app:tabIndicatorFullWidth="false"
即可:1
2
3
4
5<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorFullWidth="false"/>
这样做虽然比反射实现简单,但还是无法实现设置 Indicator 的宽度比文字宽度短的效果。
三、使用 layer-list
Indicator 允许我们设置 drawable 来自定义样式,通过 layer-list 可以在上层设置一个固定宽度的 shape:1
2
3
4
5
6
7
8
9
10
11
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<item android:gravity="center">
<shape>
<size android:width="20dp"
android:height="4dp"/>
<solid android:color="@android:color/holo_red_light"/>
</shape>
</item>
</layer-list>
然后在 xml 中给 TabLayout 设置:1
2
3
4
5<android.support.design.widget.TabLayout
android:id="@+id/tl_style_one_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicator="@drawable/tab_indicator"/>
通过这种方式,就可以设置一个比 TextView 文本宽度更短的 Indicator 了。
这种方式的缺点是各 Indicator 是宽度是一致的,并且在移动时 Indicator 的长度不会改变,不能实现移动时 Indicator 的长度由短变长再变短的动画效果。
四、第三方库
推荐一个很棒的第三方库:MagicIndicator。除了可以改变 Indicator 的宽度外,里面还有很多炫酷的 Indicator 动画。