前景提要
很久之前看到 Google News 的 TabLayout
的樣式挺有意思的,如下圖:
- 指示器和文字寬度相同
- 指示器的形狀是半個圓角矩形
於是,我模仿的結果:(截圖來自我的一個小專案裡GeekNews)
開始模仿之前,先問個問題,這個控制元件是 TabLayout 嗎?答案:是的,我用 monitor 看過了。
所以可以得到結論:直接魔改原始碼是最簡單最快的方法。
實現思路
魔改系統元件的第一步都是先把原始碼拷出來
就這四個檔案,拷出來改改裡面一些類的引用路徑,試一下能用就行了實現半個圓角矩形
簡單看一下 TabLayout 這個類的結構可以看出, TabLayout 內的指示器是由一個 私有內部類 SlidingTabStrip
來控制的,再看一下其中的 draw
方法實現,
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
複製程式碼
這裡畫了一個矩形,從mIndicatorLeft
、mIndicatorRight
、mSelectedIndicatorPaint
這幾個變數的名字上就非常非常明顯可以看出,這個矩形就是tab下面的那個矩形指示器條 (此處應有️配圖)
這個地方我們可以先實現半個圓角矩形的效果,圓角很簡單,把drawRect
換成 drawRoundRect
即可,半個矩形只要畫一個超過控制元件最底部的rectF,讓控制元件自己裁掉這個rectF的一半高度,圓角則取這個一半高度,就是mSelectedIndicatorHeight
的值
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
RectF rectF = new RectF(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight() + mSelectedIndicatorHeight);
mSelectedIndicatorPaint.setAntiAlias(true);
canvas.drawRoundRect(rectF, mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
}
}
複製程式碼
可以看到這裡我們僅利用已有的變數就能實現半個圓角矩形的效果。
實現指示器寬度與文字等寬
從上一步我們可以看到,指示器的寬度是由 mIndicatorLeft
和 mIndicatorRight
這兩個變數決定的,那直接在draw方法裡改?顯然不行,想想,tab切換涉及兩個tab的寬度計算,mIndicatorLeft
和 mIndicatorRight
的計算不僅跟著位置改變還受到tab本身寬度的影響(其實我偷偷試過了,在這裡改確實不行)。
首先,我們要找到 mIndicatorLeft
和 mIndicatorRight
被修改的地方,發現一個方法:
void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
複製程式碼
我們順著這個方法被呼叫的地方終於找到 left 和 right 計算的地方:
private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() + (1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() + (1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
複製程式碼
終於找到修改的地方了,首先我們要了解一下第一行 getChildAt 返回的 view 是個什麼,這裡就不貼程式碼了,直接說結論:閱讀原始碼可知是個名為 TabView 的類,TabView 是個 LinearLayout,TabLayout 的文字是由其內部的mTextView來顯示的。計算的思路就是下面的靈魂示意圖:
翻譯成虛擬碼就是:spacing = (view.width -view.mTextView.width)/2
newLeft = view.left + spacing
newRight = view.right - spacing
複製程式碼
所以修改之後的 updateIndicatorPosition()
方法如下,要注意需要修改兩組left和right,一個是當前選中的Tab,一個是下一個Tab
private void updateIndicatorPosition() {
final TabView selectedTitle = (TabView) getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
int spacing = (selectedTitle.getWidth() - selectedTitle.mTextView.getMeasuredWidth()) / 2;
left = selectedTitle.getLeft() + spacing;
right = selectedTitle.getRight() - spacing;
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
TabView nextTitle = (TabView) getChildAt(mSelectedPosition + 1);
int nextSpacing = (nextTitle.getWidth() - nextTitle.mTextView.getMeasuredWidth()) / 2;
int nextLeft = nextTitle.getLeft() + nextSpacing;
int nextRight = nextTitle.getRight() - nextSpacing;
left = (int) (mSelectionOffset * nextLeft +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextRight +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
複製程式碼
這就修改完了嗎?不!要知道tab之間切換有兩種方式,一個是 viewPager 划過去,還有一個是點選任意一個tab跳過去,所以還有一個地方要改,不需要找了,setIndicatorPosition 緊跟著的下一個方法就是,void animateIndicatorToPosition(final int position, int duration)
,我們只要改其中的這一段:
final View targetView = getChildAt(position);
...
final int targetLeft = targetView.getLeft();
final int targetRight = targetView.getRight();
複製程式碼
只需要改目標 view 的 left 和 right,因為這個方法裡呼叫了一次 updateIndicatorPosition()
,當前選中的 view 已經被計算過一次了。
修改後:
final TabView targetView = (TabView) getChildAt(position);
...
int targetSpacing = (targetView.getWidth() - targetView.mTextView.getMeasuredWidth()) / 2;
final int targetLeft = targetView.getLeft() + targetSpacing;
final int targetRight = targetView.getRight() - targetSpacing;
複製程式碼
至此,真的就全部完成了。 修改好的原始碼地址:TabLayout.java
終於水完了 2019 年的第一篇 blog