好久以前看到 Google News 的 TabLayout
的樣式挺有意思的,以下圖: java
因而,我模仿的結果:(截圖來自個人一個小項目裏GeekNews) git
開始模仿以前,先問個問題,這個控件是 TabLayout 嗎?答案:是的,我用 monitor 看過了。
因此能夠獲得結論:直接魔改源碼是最簡單最快的方法。github
簡單看一下 TabLayout 這個類的結構能夠看出, TabLayout 內的指示器是由一個 私有內部類 SlidingTabStrip
來控制的,再看一下其中的 draw
方法實現,canvas
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下面的那個矩形指示器條 (此處應有️配圖)bash
這個地方咱們能夠先實現半個圓角矩形的效果,圓角很簡單,把drawRect
換成 drawRoundRect
便可,半個矩形只要畫一個超過控件最底部的rectF,讓控件本身裁掉這個rectF的一半高度,圓角則取這個一半高度,就是mSelectedIndicatorHeight
的值post
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);
}
}
複製代碼
能夠看到這裏咱們僅利用已有的變量就能實現半個圓角矩形的效果。this
從上一步咱們能夠看到,指示器的寬度是由 mIndicatorLeft
和 mIndicatorRight
這兩個變量決定的,那直接在draw方法裏改?顯然不行,想一想,tab切換涉及兩個tab的寬度計算,mIndicatorLeft
和 mIndicatorRight
的計算不只跟着位置改變還受到tab自己寬度的影響(其實我偷偷試過了,在這裏改確實不行)。
首先,咱們要找到 mIndicatorLeft
和 mIndicatorRight
被修改的地方,發現一個方法:spa
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來顯示的。計算的思路就是下面的靈魂示意圖:3d
翻譯成僞代碼就是: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