看了下网上很多的文章来设置下划线宽度的问题,有点杂乱无章,有的博文直接贴代码,无法理解设置的过程和实际的意义,看来只能自己动手才能丰衣足食了。
使用
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的文字正常显示。
最后还有一点就是有的人这么使用会报错,是因为混淆产生的问题,反射mTextView的时候可能会出问题,可以在混淆配置里面设置下TabLayout不被混淆
-keep class android.support.design.widget.TabLayout{*;}
总结
继续看源码,看别人的设计思路
推荐一本最近在看的书《自控力》,人要做到“慎独”、严于律己,慎其独也者,言舍夫五而慎其心之谓也。独然后一,一也者,夫五为一也,然后得之。