看了下網上很多的文章來設定下劃線寬度的問題,有點雜亂無章,有的博文直接貼程式碼,無法理解設定的過程和實際的意義,看來只能自己動手才能豐衣足食了。
使用
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{*;}
總結
繼續看原始碼,看別人的設計思路
推薦一本最近在看的書《自控力》,人要做到“慎獨”、嚴於律己,慎其獨也者,言舍夫五而慎其心之謂也。獨然後一,一也者,夫五為一也,然後得之。