前言
TabLayout继承自HorizontalScrollView,用作页面切换指示器,因使用简便功能强大而广泛使用在App中。TabLayout通常都和ViewPager结合使用,而且一般情况下本身自带的属性也能满足大部分产品需求,效果如下:
TabLayout可以在相关属性上设置tab选中的指示器和背景图,但在效果上并不是特别完美。在不松开手指情况下,fragment左右滑动,只有指示器跟随移动,tab背景图并不会移动,只有当达到一定的偏移量的时候才会跳到下(上)一个tab。为了弥补这个不足之处,可以通过自定义view(SliderLayout)解决这个缺陷。
自定义View设计思路
1、滑块的绘制,可以读取资源文件里的drawable,也可以自己绘制一个。
2、滑块的移动(1),监听TabLayoutOnPageChangeListener,fragment的左右滑动要同步到到tab滑块的移动,即fragment的移动偏移量相当于tab滑块的偏移量。
3、滑块的移动(2),当TabLayout的tabMode属性改为scrollable时,还需处理TabLayout在横向移动时,滑块也要跟随当前选中的tab移动,可在OnScrollChangeListener监听方法处理。
监听方法相关代码
1、滑块的移动(1)
public static class SliderOnPageChangeListener extends TabLayout.TabLayoutOnPageChangeListener {
private final SoftReference<SliderLayout> mSliderLayoutRef;
public SliderOnPageChangeListener(TabLayout tabLayout, SliderLayout layout) {
super(tabLayout);
mSliderLayoutRef = new SoftReference<>(layout);
layout.setupWithTabLayout(tabLayout);
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
final SliderLayout layout = mSliderLayoutRef.get();
if (layout != null) {
layout.setScrollPosition(position, positionOffset);
}
}
}
/**
* 把滑块滑动到指定的位置
*
* @param position 当前位置
* @param positionOffset 滑动到下一个或上一个位置比例
*/
private void setScrollPosition(int position, float positionOffset) {
final int roundedPosition = Math.round(position + positionOffset);
if (roundedPosition < 0 || roundedPosition >= totalNum) {
return;
}
float scrollX = calculateScrollXForTab(position, positionOffset);
scrollTo((int) scrollX, 0);
}
/**
* 计算滑块需要滑动的距离
*
* @param position 当前选择的位置
* @param positionOffset 滑动位置的百分百
* @return 滑动的距离
*/
private int calculateScrollXForTab(int position, float positionOffset) {
if (mTabLayout == null) return 0;
LinearLayout mTabStrip = getTabStrip();
if (mTabStrip == null) return 0;
//当前选择的View
final View selectedChild = mTabStrip.getChildAt(position);
//下一个View
final View nextChild = position + 1 < mTabStrip.getChildCount()
? mTabStrip.getChildAt(position + 1)
: null;
//当前选择的View的宽度
final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
//下一个View的宽度
final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
//当前选择的View的左边位置,view的方位
final int left = selectedChild != null ? selectedChild.getLeft() : 0;
//计算滑块需要滑动的距离,左 + ,右 - ;
int scrollX = -(left + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)));
//现在是把SliderLayout直接放在TableLayout中的所以就不许要考虑TableLayout本身的滑动
if (mTabLayout.getTabMode() == TabLayout.MODE_SCROLLABLE) {//当为滑动模式的时候TabLayout会有水平方向的滑动
scrollX += mTabLayout.getScrollX();//计算在TabLayout有滑动的时候,滑块相对的滑动距离
}
return scrollX;
}
监听方法(1)作用到ViewPager
mViewPager.clearOnPageChangeListeners();
mViewPager.addOnPageChangeListener(new SliderLayout.SliderOnPageChangeListener(mTabLayout, sliderLayout));
使用监听方法(1)后效果图如下:
由上图可见,fragment左右滑动,tab滑动也能很好的跟随移动了,但当TabLayout横向移动的时候,tab滑块只会停留在当前位置上,并没有紧跟着选中的tab,因此,则需要用到监听方法(2)。
2、滑块的移动(2)
@RequiresApi(api = Build.VERSION_CODES.M)
public static class SlideOnScrollChangeListener implements OnScrollChangeListener {
private final SoftReference<SliderLayout> mSliderLayoutRef;
private TabLayout mTabLayout;
private LinearLayout tabStrip;
public SlideOnScrollChangeListener(TabLayout tabLayout, SliderLayout layout) {
mSliderLayoutRef = new SoftReference<>(layout);
mTabLayout = tabLayout;
tabStrip = (LinearLayout) mTabLayout.getChildAt(0);
}
@Override
public void onScrollChange(View view, int i, int i1, int i2, int i3) {
final SliderLayout layout = mSliderLayoutRef.get();
int selectedTabPosition = mTabLayout.getSelectedTabPosition();
if (layout != null) {
int width = tabStrip.getChildAt(selectedTabPosition).getWidth();
layout.scrollTo(i - width * selectedTabPosition,0);
}
}
}
监听方法(2)作用到TabLayout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mTabLayout.setOnScrollChangeListener(new SliderLayout.SlideOnScrollChangeListener(mTabLayout, sliderLayout));
}
使用监听方法(2)后效果图如下:
由此可见,之前的缺陷已经完美修复。当前的滑块既可以跟随fragment切换滑动,也可以完美跟随TabLayout的横向滑动而移动了。