从源码角度来理解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的文字正常显示。
总结
继续看源码,看别人的设计思路
推荐一本最近在看的书《自控力》,人要做到“慎独”、严于律己,慎其独也者,言舍夫五而慎其心之谓也。独然后一,一也者,夫五为一也,然后得之。