模仿Google News的TabLayout

前景提要

好久以前看到 Google News 的 TabLayout 的樣式挺有意思的,以下圖: java

具體效果你們能夠本身下載 Google News 看一下,截圖上大概看出來一共有兩個要素:

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

因而,我模仿的結果:(截圖來自個人一個小項目裏GeekNewsgit

開始模仿以前,先問個問題,這個控件是 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);
    }
}
複製代碼

這裏畫了一個矩形,從mIndicatorLeftmIndicatorRightmSelectedIndicatorPaint這幾個變量的名字上就很是很是明顯能夠看出,這個矩形就是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

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

從上一步咱們能夠看到,指示器的寬度是由 mIndicatorLeftmIndicatorRight 這兩個變量決定的,那直接在draw方法裏改?顯然不行,想一想,tab切換涉及兩個tab的寬度計算,mIndicatorLeftmIndicatorRight的計算不只跟着位置改變還受到tab自己寬度的影響(其實我偷偷試過了,在這裏改確實不行)。
首先,咱們要找到 mIndicatorLeftmIndicatorRight 被修改的地方,發現一個方法: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

相關文章
相關標籤/搜索