從原始碼角度來理解TabLayout設定下劃線寬度問題

codelang發表於2018-04-08

看了下網上很多的文章來設定下劃線寬度的問題,有點雜亂無章,有的博文直接貼程式碼,無法理解設定的過程和實際的意義,看來只能自己動手才能豐衣足食了。

使用

        viewPager = (ViewPager) findViewById(R.id.qbdd_viewpager);
        viewPager.setAdapter(new MyViewPagerAdapter(getSupportFragmentManager(), fragmentList));
        tabLayout = (TabLayout) findViewById(R.id.qbdd_tablayout);
        tabLayout.addTab(tabLayout.newTab());
        tabLayout.addTab(tabLayout.newTab());
        tabLayout.addTab(tabLayout.newTab());
        tabLayout.setupWithViewPager(viewPager);
        tabLayout.getTabAt(0).setText("全部");
        tabLayout.getTabAt(1).setText("待付款");
        tabLayout.getTabAt(2).setText("待評論");
複製程式碼

分析

來看tabLayout.newTab()操作

  public Tab newTab() {
        Tab tab = sTabPool.acquire();
        if (tab == null) {
            tab = new Tab();
        }
        tab.mParent = this;
        tab.mView = createTabView(tab);
        return tab;
    }
    
  private TabView createTabView(@NonNull final Tab tab) {
        TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
        if (tabView == null) {
            tabView = new TabView(getContext());
        }
        tabView.setTab(tab);
        tabView.setFocusable(true);
        tabView.setMinimumWidth(getTabMinWidth());
        return tabView;
    }   
複製程式碼

這個步驟的操作不難,就是建立一個TabView,設定一些引數,然後將TabView設定到tab.mView,然後返回Tab。

然後來看tabLayout.addTab(tabLayout.newTab())操作

   //有很多的過載方法,我們直接看最後呼叫的方法
    public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
        if (tab.mParent != this) {
            throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
        }
        configureTab(tab, position);
        addTabView(tab);

        if (setSelected) {
            tab.select();
        }
    }
    //集合儲存下當前的Tab
    private void configureTab(Tab tab, int position) {
        tab.setPosition(position);
        mTabs.add(position, tab);

        final int count = mTabs.size();
        for (int i = position + 1; i < count; i++) {
            mTabs.get(i).setPosition(i);
        }
    }
    //將Tab裡面儲存的TabView新增到mTabStrip佈局裡面
    private void addTabView(Tab tab) {
        final TabView tabView = tab.mView;
        mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
    }
    
複製程式碼

這個部分也比較簡單,就是將之前建立好的Tab裡面的TabView,新增到mTabStrip裡面去。那麼mTabStrip是個什麼東西呢?我們ctrl+F來看看他在TabLayout這個類裡面幹了啥

    private final SlidingTabStrip mTabStrip;
    
    public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ...
        // Add the TabStrip
        mTabStrip = new SlidingTabStrip(context);
        super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
        ...
    }
複製程式碼

在TabLayout的構造方法裡面我們看到了他的應用,將mTabStrip直接add到TabLayout,也就是說SlidingTabStrip是TabLayout的子類,那麼,剛剛新增進來的TabView是SlidingTabStrip的子類。

然後我們來看看SlidingTabStrip這個類是幹什麼的

  private class SlidingTabStrip extends LinearLayout {
        SlidingTabStrip(Context context) {
            super(context);
            setWillNotDraw(false);
            mSelectedIndicatorPaint = new Paint();
        }
  }
複製程式碼

構造方法沒找到設定方向的程式碼,說明當前LineaLayout預設是水平方向,和我們看到TabLayout水平排列的一樣。

然後看看onMeasure方法,這個地方直接看註釋吧,程式碼有點多

  @Override
        protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            ...
            //這種情況只有在設定TabLayout的tabMode為fixed的時候觸發
            if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
                final int count = getChildCount();

                // First we'll find the widest tab
                //for迴圈遍歷所有子TabView的寬度,以最寬的TabView的寬度來為每個子TabView的寬度
                int largestTabWidth = 0;
                for (int i = 0, z = count; i < z; i++) {
                    View child = getChildAt(i);
                    if (child.getVisibility() == VISIBLE) {
                        largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
                    }
                }
                if (largestTabWidth <= 0) {
                    // If we don't have a largest child yet, skip until the next measure pass
                    return;
                }

                final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
                boolean remeasure = false;
                //如果所有子TabView的寬度之和還沒SlidingTabStrip寬度減去一個偏移值寬的話,那麼就給所有的子TabView重新設定寬度為最大寬度的TabView,
               //然後設定weight權重為0,那麼所有的子view就會按比例均分SlidingTabStrip的寬度
                
                if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
                    // If the tabs fit within our width minus gutters, we will set all tabs to have
                    // the same width
                    for (int i = 0; i < count; i++) {
                        final LinearLayout.LayoutParams lp =
                                (LayoutParams) getChildAt(i).getLayoutParams();
                        if (lp.width != largestTabWidth || lp.weight != 0) {
                            lp.width = largestTabWidth;
                            lp.weight = 0;
                            remeasure = true;
                        }
                    }
                } else {
                    // If the tabs will wrap to be larger than the width minus gutters, we need
                    // to switch to GRAVITY_FILL
                    mTabGravity = GRAVITY_FILL;
                    updateTabViews(false);
                    remeasure = true;
                }
                
                if (remeasure) {
                    // Now re-measure after our changes
                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                }
            }
複製程式碼

主要就是計算下子view的寬度,然後設定子view的寬度

然後大致來看看onLayout方法

 @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);

            if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
                // If we're currently running an animation, lets cancel it and start a
                // new animation with the remaining duration
                mIndicatorAnimator.cancel();
                final long duration = mIndicatorAnimator.getDuration();
               //動畫移動指示器在哪個position位置上面,此處就不深入了,主要就是一個ValueAnimatorCompat動畫
               animateIndicatorToPosition(mSelectedPosition,
                        Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
            } else {
                // If we've been layed out, update the indicator position
                updateIndicatorPosition();
            }
        }
複製程式碼

主要就是判斷動畫有麼有被初始化並且是否在做動畫,如果是的話,給TabLayout滑塊做動畫,滑動到指定位置的position上面,如果沒有,則來設定這個滑塊。此處,我們來到了我們想要設定的關鍵點,滑塊的寬度。

我們來看updateIndicatorPosition方法,看註釋吧

        private void updateIndicatorPosition() {
        //根據當前滑塊的位置拿到當前TabView
            final View selectedTitle = getChildAt(mSelectedPosition);
            int left, right;

            if (selectedTitle != null && selectedTitle.getWidth() > 0) {
            //拿到TabView的左、右位置
                left = selectedTitle.getLeft();
                right = selectedTitle.getRight();
                //這句就是在滑塊滑動的時候,如果滑動超過了上一個或是下一個滑塊一半的話,那就說明移動到了上一個或是下一個滑塊,然後取出left和right
                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);
        }
        
        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);
            }
        }
複製程式碼

從程式碼的整體來看,設定滑塊的寬度是根據子TabView的寬度來設定的,也就是說,TabView的寬度是多少,那麼滑塊的寬度就是多少。


停下來想想,按照上面的分析,我們先畫個佈局層圖

從原始碼角度來理解TabLayout設定下劃線寬度問題

最外面是TabLayout,子view是SlidingTabStrip,所有addTab建立的tab都是SlidingTabStrip的子類,並且通過onMeasure重新給Tabview設定了寬度和權重,致使每個tabView都是佔滿佈局。

因為滑塊的寬度跟TabView的寬度有關,那麼我們重新給TabView設定LayoutParams,設定marginLeft和marginRight,這樣,會壓縮TabView的寬度,致使滑塊的寬度也跟著變化,如下思路圖所示

從原始碼角度來理解TabLayout設定下劃線寬度問題

朝上的花括號是margin,黃色的是滑塊,這樣不就可以控制滑塊的寬度了嘛,那麼,我們來試試

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
     //一些TabLayout的addTab操作
    
        tabLayout.post(new Runnable() {
            @Override
            public void run() {
                setTabWidth();
            }
        });
}
public void setTabWidth(){
 //拿到SlidingTabStrip的佈局
  LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0);
  //遍歷SlidingTabStrip的所有TabView子view
  for (int i = 0; i < mTabStrip.getChildCount(); i++) {
        View tabView = mTabStrip.getChildAt(i);
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)tabView.getLayoutParams();
        //給TabView設定leftMargin和rightMargin
        params.leftMargin = dp2px(10);
        params.rightMargin = dp2px(10);
        tabView.setLayoutParams(params);
        //觸發繪製
        tabView.invalidate();
    }
}
複製程式碼

這個地方需要在Tablayout設定完成後操作,並且必須等所有繪製操作結束,使用tabLayout.post拿到屬性引數,然後設定下margin,搞定,來看看效果圖

從原始碼角度來理解TabLayout設定下劃線寬度問題

看起來還行,仔細看的時候會發現,當TabView的文字有2個的時候TextSize特別大,有三個的時候TextSize比較小,這種情況只適合所有TabView的文字長度都是一樣的情況。我們來想想,為啥會導致這種情況呢,在我們設定TabView的寬度的時候,並沒有考慮到TabView子view的wrap情況,萬一TabView的子view寬度有的大,有的小,我只考慮到給每個TabView設定平均的寬度的話,考慮的不是特別周全,所以,我們先來看看TabView裡面有啥,然後重新整理思路。

來看看TabView是個啥玩意

    class TabView extends LinearLayout implements OnLongClickListener {
        private Tab mTab;
        private TextView mTextView;
        private ImageView mIconView;

        private View mCustomView;
        private TextView mCustomTextView;
        private ImageView mCustomIconView;

        private int mDefaultMaxLines = 2;

        public TabView(Context context) {
            super(context);
            if (mTabBackgroundResId != 0) {
                ViewCompat.setBackground(
                        this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
            }
            ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
                    mTabPaddingEnd, mTabPaddingBottom);
            setGravity(Gravity.CENTER);
            setOrientation(VERTICAL);
            setClickable(true);
            ViewCompat.setPointerIcon(this,
                    PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
        }
    }
複製程式碼

是一個垂直方向的LineaLayout,然後我們回到最上面,看一下TabView的建立。呼叫了tabView.setTab(tab)方法

        void setTab(@Nullable final Tab tab) {
            if (tab != mTab) {
                mTab = tab;
                update();
            }
        }
        final void update() {
        ...
        if (mCustomView == null) {
                // If there isn't a custom view, we'll us our own in-built layouts
                if (mIconView == null) {
                    ImageView iconView = (ImageView) LayoutInflater.from(getContext())
                            .inflate(R.layout.design_layout_tab_icon, this, false);
                    //將iconView新增到TabView
                    addView(iconView, 0);
                    mIconView = iconView;
                }
                if (mTextView == null) {
                    TextView textView = (TextView) LayoutInflater.from(getContext())
                            .inflate(R.layout.design_layout_tab_text, this, false);
                    //將TextView新增到TabView
                    addView(textView);
                    mTextView = textView;
                    mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
                }
                TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
                if (mTabTextColors != null) {
                    mTextView.setTextColor(mTabTextColors);
                }
                updateTextAndIcon(mTextView, mIconView);
        } else {
                // Else, we'll see if there is a TextView or ImageView present and update them
                if (mCustomTextView != null || mCustomIconView != null) {
                    updateTextAndIcon(mCustomTextView, mCustomIconView);
                }
        }
複製程式碼

這地方就是初始化mTextView和mIconView,就是我們常見的TabLayout設定帶有icon的tab,然後新增到TabView裡面。

TabView的子view沒多少,總共就5個,自定義的view目前可以不用關心,我們先來關心關心mTextView,因為我們剛剛是因為他出了問題。

我們之前設定的是TabView的寬度,導致了TextView顯示被壓縮,那麼,現在我們換個思路,我們去拿TextView的寬度,然後設定到TabView的寬上面,使TabView的寬度來適應TextView的寬度,不採用之前平均分配寬度的方式。

那麼,來試試吧

 @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
     //一些TabLayout的addTab操作
    
        tabLayout.post(new Runnable() {
            @Override
            public void run() {
                setTabWidth();
            }
        });
}
public void setTabWidth(){
 //拿到SlidingTabStrip的佈局
  LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0);
  //遍歷SlidingTabStrip的所有TabView子view
  for (int i = 0; i < mTabStrip.getChildCount(); i++) {
        View tabView = mTabStrip.getChildAt(i);
        //通過反射拿到TabView的的mTextView
        Field mTextViewField = tabView.getClass().getDeclaredField("mTextView");
        mTextViewField.setAccessible(true);
        //拿到TextView的寬度
        TextView mTextView = (TextView) mTextViewField.get(tabView);
        int txWidth = mTextView.getMeasuredWidth();
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)tabView.getLayoutParams();
        //給TabView設定寬度
        params.width = txWidth;
        //還可以使用tabView.setMinimumWidth(txWidth);給TabView設定最小寬度為textView的寬度
        
        //給TabView設定leftMargin和rightMargin
        params.leftMargin = dp2px(10);
        params.rightMargin = dp2px(10);
        tabView.setLayoutParams(params);
        //觸發繪製
        tabView.invalidate();
    }
}
複製程式碼

效果圖

從原始碼角度來理解TabLayout設定下劃線寬度問題

嗯,很完美,這裡地方主要就是通過mTextView的寬度來控制TabView的寬度,這裡有兩種方式,一種是直接給TabView設定寬度,還有一種是給TabView設定最小寬度為mTextView的寬度,總之就是要保證mTextView的文字正常顯示。

最後還有一點就是有的人這麼使用會報錯,是因為混淆產生的問題,反射mTextView的時候可能會出問題,可以在混淆配置裡面設定下TabLayout不被混淆

-keep class android.support.design.widget.TabLayout{*;}

總結

繼續看原始碼,看別人的設計思路

推薦一本最近在看的書《自控力》,人要做到“慎獨”、嚴於律己,慎其獨也者,言舍夫五而慎其心之謂也。獨然後一,一也者,夫五為一也,然後得之。

相關文章