在之前的Android開發之TabLayout一文中我已經大篇幅介紹過如何使用TabLayout這個控制元件,今天我們來玩點它的高階用法。通過大量閱讀TabLayout的原始碼,我梳理並摸索出了一條修改tab indicator高階手段。在需要本文之前需要掌握以下知識點:
- 具有閱讀原始碼的能力
- 自定義控制元件基礎
- java反射原理
- 設計模式
首先我們來搞清楚一個問題,那就是TabLayout是如何實現indicator的?要搞清楚這個問題,我們需要進入到TabLayout的原始碼。
注意:我用的design support包版本是27.0.0,由於design support 28.0.0修改了TabLayout部分原始碼,增加了新功能,看到的原始碼可能跟我的不一樣。
進入TabLayout原始碼的世界
TabLayout繼承結構圖:
TabLayout繼承自HorizontalScrollView,HorizontalScrollView是一個可以橫向滾動的控制元件。TabLayout是怎麼實現indicator的?
按照我最初的猜想,我以為indicator是一個View什麼的,給他設定寬度、高度及顏色就可以顯示在文字下方。然而,看了原始碼後才知道其實並不是這樣的。 首先來看看TabLayout是怎麼新增Tab的,我們從構造方法開始閱讀,放出原始碼:
可以看到TabLayout內部新增了一個叫SlidingTabStrip的內部類作為容器,它是繼承LinearLayout,下面是它的定義: 我可以告訴大家indicator是在這個類的draw方法中畫的!!看: 在這個方法中它畫了一個矩形,根據mIndicatorLeft和mIndicatorRight的值來決定indicator的顯示位置。我們的突破點就是從這裡開始。我的想法是通過反射來動態修改這兩個成員變數的值,從而達到修改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);
複製程式碼