前言
默认情况下,TabLayout 中 Indicator 的宽度和该 Tab 的宽度相等。但是有时候我们需要自定义 Indicator 的宽度,所以本文将介绍改变 Indicator 的宽度的几种方法。
一、反射实现
第一种方法是通过反射的方式改变 Indicator 的宽度。
TabLayout 只提供了 app:tabIndicatorHeight
属性来设置 Indicator 的宽度,但没有提供设置宽度的 API。所以只能自己去修改了,先看下源码是怎么实现 Indicator 的。
源码分析
在 TabLayout 中,定义了一个 SlidingTabStrip 对象:
1 | private final SlidingTabStrip mTabStrip; |
SlidingTabStrip 继承自 LinearLayout,负责处理 Tab 滑动的相关操作。在它的 draw 方法中,有绘制 Indicator 相关的代码:
1 2 3 4 5 6 7 8 9 | @Override public void draw(Canvas canvas) { super.draw(canvas); if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight(), mSelectedIndicatorPaint); } } |
其中 mIndicatorLeft 和 mIndicatorRight 决定 Indicator 的宽度,宽度为 mIndicatorRight - mIndicatorLeft。
而 mIndicatorLeft 和 mIndicatorRight 最终是在 SlidingTabStrip 的 updateIndicatorPosition() 方法设置的:
1 2 3 4 5 6 7 8 | private void updateIndicatorPosition() { final View selectedTitle = getChildAt(mSelectedPosition); // 当前 Tab int left, right; // 最终 left 和 right 会赋给 mIndicatorLeft 和 mIndicatorRight left = selectedTitle.getLeft(); right = selectedTitle.getRight(); } |
可以看到,最终 Indicator 的宽度就是 SlidingTabStrip 中子 View 的宽度。
也就是说,想要改变 Indicator 的宽度,只需设置 SlidingTabStrip 中子 View 的 宽度即可。
代码实现
- 得到 SlidingTabStrip 对象,这里不需要反射,因为 SlidingTabStrip 对象是 TabLayout 的第一个子 View,通过 tabLayout.getChildAt(0) 即可得到 SlidingTabStrip 对象。
- 设置 SlidingTabStrip 中相应子 View 的宽度。这里设置的宽度不能小于 TextView 的宽度,不然文字会显示不全。这时需要通过反射来得到 TextView,因为 TextView 是 TabView 的子 View,而 TabView 是 TabLayout 中的非公有内部类,外部无法直接访问 TabView。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /** * 通过反射的方式改变 Indicator 的宽度 * * @param tabLayout 要改变的 TabLayout * @param width Indicator 的宽度 * @param margin Tab 之间的 margin */ public static void setIndicatorWidth(final TabLayout tabLayout, final int width, final int margin) { tabLayout.post(new Runnable() { @Override public void run() { try { LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0); for (int i = 0; i < mTabStrip.getChildCount(); i++) { // 得到当前子 View(TabView) View child = mTabStrip.getChildAt(i); // 得到 TextView 的宽度 Field textViewField = child.getClass().getDeclaredField("textView"); textViewField.setAccessible(true); TextView mTextView = (TextView) textViewField.get(child); child.setPadding(0,0,0,0); int textViewWidth = mTextView.getWidth(); if (textViewWidth == 0) { mTextView.measure(0,0); textViewWidth = mTextView.getMeasuredWidth(); } // 设置该子 View 的相关参数 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); lp.width = textViewWidth > width ? textViewWidth : width; lp.leftMargin = margin; lp.rightMargin = margin; child.setLayoutParams(lp); child.invalidate(); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }); } |
这种方式存在的不足:
- 如果升级 sdk 后,sdk 中获取反射的变量名发生改变,代码也要跟着变,不然会失效。
- 这种方式最小只能把 Indicator 的宽度设置成 TextView 的宽度,如果宽度更小的话,TextView 的文本就会被压缩。
二、SDK 28+ 属性配置
如果你使用的 SDK 版本在 28 以上,并且需要将 Indicator 的宽度设置成和文字宽度一样,那么只需在 xml 中给 TabLayout 加上 app:tabIndicatorFullWidth="false"
即可:
1 2 3 4 5 | <android.support.design.widget.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabIndicatorFullWidth="false"/> |
这样做虽然比反射实现简单,但还是无法实现设置 Indicator 的宽度比文字宽度短的效果。
三、使用 layer-list
Indicator 允许我们设置 drawable 来自定义样式,通过 layer-list 可以在上层设置一个固定宽度的 shape:
1 2 3 4 5 6 7 8 9 10 11 | <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <item android:gravity="center"> <shape> <size android:width="20dp" android:height="4dp"/> <solid android:color="@android:color/holo_red_light"/> </shape> </item> </layer-list> |
然后在 xml 中给 TabLayout 设置:
1 2 3 4 5 | <android.support.design.widget.TabLayout android:id="@+id/tl_style_one_tab" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabIndicator="@drawable/tab_indicator"/> |
通过这种方式,就可以设置一个比 TextView 文本宽度更短的 Indicator 了。
这种方式的缺点是各 Indicator 是宽度是一致的,并且在移动时 Indicator 的长度不会改变,不能实现移动时 Indicator 的长度由短变长再变短的动画效果。
四、第三方库
推荐一个很棒的第三方库:MagicIndicator。除了可以改变 Indicator 的宽度外,里面还有很多炫酷的 Indicator 动画。