欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-01 07:50:29
...

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

使用

        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的文字正常显示。

总结

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

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