在之前的文章中我已经大篇幅介绍过如何使用TabLayout这个控件,今天我们来玩点它的高级用法。通过大量阅读TabLayout的源码,我梳理并摸索出了一条修改tab indicator高级手段。在需要本文之前需要掌握以下知识点:
- 具有阅读源码的能力
- 自定义控件基础
- java反射原理
- 设计模式
首先我们来搞清楚一个问题,那就是TabLayout是如何实现indicator的?要搞清楚这个问题,我们需要进入到TabLayout的源代码。
注意:我用的design support包版本是27.0.0,由于design support 28.0.0修改了TabLayout部分源码,增加了新功能,看到的源码可能跟我的不一样。
进入TabLayout源码的世界
TabLayout继承结构图:
indicator是怎么实现的?
按照我最初的猜想,我以为indicator是一个View什么的,给他设置宽度、高度及颜色就可以显示在文字下方。然而,看了源码后才知道其实并不是这样的。 首先来看看TabLayout是怎么添加Tab的,我们从构造方法开始阅读,放出源码:
我的思路
通过阅读TabLayout的源码得知indicator的宽度是由SlidingTabStrip这个内部类中的两个成员变量来决定的,如下:
实操指北
首先我们来拿到这两个成员变量的值。下面是它们的定义:
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(); }复制代码
先来看看实际需要的效果:
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布局:
复制代码
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设计模式。
终极方案
别费劲了,用第三方库!