从源码角度来理解TabLayout设置下划线宽度问题

看了下网上很多的文章来设置下划线宽度的问题,有点杂乱无章,有的博文直接贴代码,无法理解设置的过程和实际的意义,看来只能自己动手才能丰衣足食了。

使用

        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,子view是SlidingTabStrip,所有addTab创建的tab都是SlidingTabStrip的子类,并且通过onMeasure重新给Tabview设置了宽度和权重,致使每个tabView都是占满布局。

因为滑块的宽度跟TabView的宽度有关,那么我们重新给TabView设置LayoutParams,设置marginLeft和marginRight,这样,会压缩TabView的宽度,致使滑块的宽度也跟着变化,如下思路图所示

朝上的花括号是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,搞定,来看看效果图

看起来还行,仔细看的时候会发现,当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();
    }
}

效果图

嗯,很完美,这里地方主要就是通过mTextView的宽度来控制TabView的宽度,这里有两种方式,一种是直接给TabView设置宽度,还有一种是给TabView设置最小宽度为mTextView的宽度,总之就是要保证mTextView的文字正常显示。

总结

继续看源码,看别人的设计思路

推荐一本最近在看的书《自控力》,人要做到“慎独”、严于律己,慎其独也者,言舍夫五而慎其心之谓也。独然后一,一也者,夫五为一也,然后得之。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值