Android開發之無侵入式修改TabLayout tabIndicator寬度

mex發表於2019-03-28

在之前的Android開發之TabLayout一文中我已經大篇幅介紹過如何使用TabLayout這個控制元件,今天我們來玩點它的高階用法。通過大量閱讀TabLayout的原始碼,我梳理並摸索出了一條修改tab indicator高階手段。在需要本文之前需要掌握以下知識點:

  • 具有閱讀原始碼的能力
  • 自定義控制元件基礎
  • java反射原理
  • 設計模式

首先我們來搞清楚一個問題,那就是TabLayout是如何實現indicator的?要搞清楚這個問題,我們需要進入到TabLayout的原始碼。

注意:我用的design support包版本是27.0.0,由於design support 28.0.0修改了TabLayout部分原始碼,增加了新功能,看到的原始碼可能跟我的不一樣。

進入TabLayout原始碼的世界

TabLayout繼承結構圖:

Android開發之無侵入式修改TabLayout tabIndicator寬度
TabLayout繼承自HorizontalScrollView,HorizontalScrollView是一個可以橫向滾動的控制元件。

TabLayout是怎麼實現indicator的?

按照我最初的猜想,我以為indicator是一個View什麼的,給他設定寬度、高度及顏色就可以顯示在文字下方。然而,看了原始碼後才知道其實並不是這樣的。 首先來看看TabLayout是怎麼新增Tab的,我們從構造方法開始閱讀,放出原始碼:

Android開發之無侵入式修改TabLayout tabIndicator寬度
可以看到TabLayout內部新增了一個叫SlidingTabStrip的內部類作為容器,它是繼承LinearLayout,下面是它的定義:
Android開發之無侵入式修改TabLayout tabIndicator寬度
我可以告訴大家indicator是在這個類的draw方法中畫的!!看:
Android開發之無侵入式修改TabLayout tabIndicator寬度
在這個方法中它畫了一個矩形,根據mIndicatorLeft和mIndicatorRight的值來決定indicator的顯示位置。我們的突破點就是從這裡開始。我的想法是通過反射來動態修改這兩個成員變數的值,從而達到修改indicator的顯示寬度。

實操指北

首先我們來拿到這兩個成員變數的值。下面是它們的定義:

Android開發之無侵入式修改TabLayout tabIndicator寬度
一看是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();
        }
複製程式碼

先來看看實際需要的效果:

Android開發之無侵入式修改TabLayout tabIndicator寬度
我們希望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);
複製程式碼

相關文章