在之前的文章中我已经大篇幅介绍过如何使用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布局:

<android.support.design.widget.TabLayout
        android:id="@+id/TabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabIndicatorColor="@color/colorAccent">

        <android.support.design.widget.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C语言" />

        <android.support.design.widget.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C++" />

        <android.support.design.widget.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Java" />
    </android.support.design.widget.TabLayout>
复制代码

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设计模式。