模仿Google News的TabLayout

Howshea發表於2019-01-08

前景提要

很久之前看到 Google News 的 TabLayout 的樣式挺有意思的,如下圖:

模仿Google News的TabLayout
具體效果大家可以自己下載 Google News 看一下,截圖上大概看出來一共有兩個要素:

  1. 指示器和文字寬度相同
  2. 指示器的形狀是半個圓角矩形

於是,我模仿的結果:(截圖來自我的一個小專案裡GeekNews

模仿Google News的TabLayout

開始模仿之前,先問個問題,這個控制元件是 TabLayout 嗎?答案:是的,我用 monitor 看過了。
所以可以得到結論:直接魔改原始碼是最簡單最快的方法。

實現思路

魔改系統元件的第一步都是先把原始碼拷出來

模仿Google News的TabLayout
就這四個檔案,拷出來改改裡面一些類的引用路徑,試一下能用就行了

實現半個圓角矩形

簡單看一下 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);
    }
}
複製程式碼

這裡畫了一個矩形,從mIndicatorLeftmIndicatorRightmSelectedIndicatorPaint這幾個變數的名字上就非常非常明顯可以看出,這個矩形就是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);
    }
}
複製程式碼

可以看到這裡我們僅利用已有的變數就能實現半個圓角矩形的效果。

實現指示器寬度與文字等寬

從上一步我們可以看到,指示器的寬度是由 mIndicatorLeftmIndicatorRight 這兩個變數決定的,那直接在draw方法裡改?顯然不行,想想,tab切換涉及兩個tab的寬度計算,mIndicatorLeftmIndicatorRight的計算不僅跟著位置改變還受到tab本身寬度的影響(其實我偷偷試過了,在這裡改確實不行)。
首先,我們要找到 mIndicatorLeftmIndicatorRight 被修改的地方,發現一個方法:

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來顯示的。計算的思路就是下面的靈魂示意圖:

模仿Google News的TabLayout
翻譯成虛擬碼就是:

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

模仿Google News的TabLayout

相關文章