在之前的文章中我已经大篇幅介绍过如何使用TabLayout这个控件,今天我们来玩点它的高级用法。通过大量阅读TabLayout的源码,我梳理并摸索出了一条修改tab indicator高级手段。在需要本文之前需要掌握以下知识点:
- 具有阅读源码的能力
- 自定义控件基础
- java反射原理
- 设计模式
首先我们来搞清楚一个问题,那就是TabLayout是如何实现indicator的?要搞清楚这个问题,我们需要进入到TabLayout的源代码。
注意:我用的design support包版本是27.0.0,由于design support 28.0.0修改了TabLayout部分源码,增加了新功能,看到的源码可能跟我的不一样。
进入TabLayout源码的世界
TabLayout继承结构图:
TabLayout继承自HorizontalScrollView,HorizontalScrollView是一个可以横向滚动的控件。indicator是怎么实现的?
按照我最初的猜想,我以为indicator是一个View什么的,给他设置宽度、高度及颜色就可以显示在文字下方。然而,看了源码后才知道其实并不是这样的。 首先来看看TabLayout是怎么添加Tab的,我们从构造方法开始阅读,放出源码:
可以看到TabLayout内部添加了一个叫SlidingTabStrip的内部类作为容器,它是继承LinearLayout,下面是它的定义: 我可以告诉大家indicator是在这个类的draw方法中画的!!看: 在这个方法中它画了一个矩形,根据mIndicatorLeft和mIndicatorRight的值来决定indicator的显示位置。我们的突破点就是从这里开始。我的想法是通过反射来动态修改这两个成员变量的值,从而达到修改indicator的显示宽度。我的思路
通过阅读TabLayout的源码得知indicator的宽度是由SlidingTabStrip这个内部类中的两个成员变量来决定的,如下:
所以我们只要通过动态修改这两个变量的值,就可以达到修改indicator的宽度目的。实操指北
首先我们来拿到这两个成员变量的值。下面是它们的定义:
一看是private修饰的,二话不说,上反射先拿到再说: try {
Field field = TabLayout.class.getDeclaredField("mTabStrip");
Log.d(TAG, "mTabStrip field = " + field);
field.setAccessible(true);
Object tabStrip = field.get(tabLayout);
if (tabStrip != null) {
Field leftField = tabStrip.getClass()
.getDeclaredField("mIndicatorLeft");
Log.d(TAG, "mIndicatorLeft field = " + leftField);
leftField.setAccessible(true);
Log.d(TAG, "mIndicatorLeft value = " + leftField.get(tabStrip));
Log.d(TAG, "----------------------------------------------------");
Field rightField = tabStrip.getClass()
.getDeclaredField("mIndicatorRight");
Log.d(TAG, "mIndicatorRight field = " + rightField);
rightField.setAccessible(true);
Log.d(TAG, "mIndicatorRight value = " + rightField.get(tabStrip));
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
复制代码
先来看看实际需要的效果:
我们希望indicator由我们自己指定宽度,而不是系统默认的样式。下一步是根据计算来修改这两个成员变量的值,如下: try {
Field field = TabLayout.class.getDeclaredField("mTabStrip");
field.setAccessible(true);
Object tabStrip = field.get(tabLayout);
if (tabStrip != null) {
Field leftField = tabStrip.getClass()
.getDeclaredField("mIndicatorLeft");
leftField.setAccessible(true);
int leftValue = (int) leftField.get(tabStrip);
Log.d(TAG, "mIndicatorLeft field before update value = " + leftValue);
Field rightField = tabStrip.getClass()
.getDeclaredField("mIndicatorRight");
rightField.setAccessible(true);
int rightValue = (int) rightField.get(tabStrip);
Log.d(TAG, "mIndicatorRight field before update value = " + rightValue);
// indicator实际宽度
int realWidth = rightValue - leftValue;
int currentSelectedTabPosition = tabLayout.getSelectedTabPosition();
Log.d(TAG, "TabLayout tab indicator real width = " + realWidth);
Log.d(TAG, "TabLayout tab indicator show width = " + builder.getIndicatorWidth());
if (width > 0) {
int indicatorLeft = leftValue + (realWidth - width) / 2;
leftField.set(tabStrip, indicatorLeft);
Log.d(TAG, "currentSelectedTab = " + currentSelectedTabPosition
+ ",mIndicatorLeft field after update value = " + indicatorLeft);
int indicatorRight = indicatorLeft + width;
rightField.set(tabStrip, indicatorRight);
Log.d(TAG, "currentSelectedTab = " + currentSelectedTabPosition
+ ",mIndicatorRight field after update value = " + indicatorRight);
} else {
// 设置indicator高度为0,即不显示
tabLayout.setSelectedTabIndicatorHeight(0);
}
// 刷新UI
ViewCompat.postInvalidateOnAnimation((LinearLayout) tabStrip);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
复制代码
这行代码是我参考的它源码里的写法:
// 刷新UI
ViewCompat.postInvalidateOnAnimation((LinearLayout) tabStrip);
复制代码
实现重点
上面只是找到了修改indicator绘制宽度突破点,并没有解决实际问题。通过测试发现,切换tab中和切换tab后indicator的宽度会恢复到系统默认效果。经过测试和调试系统源码,通过监听tab切换回调来动态修改indicator的宽度,以达到我们想要的效果,如下:
切换后解决方案:
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
Log.d(TAG, "OnTabSelectedListener -> onTabSelected.");
// 调试源码得知,TabLayout$SlidingTabStrip的draw方法会调用两次,需要延时获取,否则返回的不是最后修改的值
tabLayout.postDelayed(new Runnable() {
@Override
public void run() {
getDeclaredFieldValue(tabLayout);
setDeclaredFieldValue(tabLayout, 30);
}
}, 420);
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
Log.d(TAG, "OnTabSelectedListener -> onTabUnselected.");
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
Log.d(TAG, "OnTabSelectedListener -> onTabReselected.");
tabLayout.postDelayed(new Runnable() {
@Override
public void run() {
setDeclaredFieldValue(tabLayout, 30);
}
}, 420);
}
});
复制代码
注意:这里的延时不宜过长或过短,250-450毫秒左右,时间间隔太短可能无效果,太长界面看起来像停顿。
如何食用
用法跟正常TabLayout是一样,不需要增加额外属性。
xml布局:
"@+id/TabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="@color/colorAccent">
"wrap_content"
android:layout_height="wrap_content"
android:text="C语言" />
"wrap_content"
android:layout_height="wrap_content"
android:text="C++" />
"wrap_content"
android:layout_height="wrap_content"
android:text="Java" />
复制代码
Java代码:
private TabLayout tabLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tabLayout = findViewById(R.id.TabLayout);
setTabLayout();
}
private void setTabLayout() {
new TabLayoutIndicatorHelper.Builder(tabLayout)
.setIndicatorColor(R.color.colorPrimary)
.setIndicatorHeight(10) // 10dp
.setIndicatorWidth(30) // 30dp
.build();
}
复制代码
更优雅的食用
为了简化调用和适应不同项目而不用拷贝来拷贝去的,我们需要用一种设计模式来简化食用流程。它就是Builder设计模式。
终极方案
别费劲了,用第三方库! SmartTabLayout