基于TabLayout源码实现自定义TabLayout
目录
- TabLayout原理
- 具体实现
- 遇到的问题
- 总结
1.1 TabLayout与ViewPager的绑定原理 【基于TabLayout源码实现自定义TabLayout】往往TabLayout都是和ViewPager联动使用,下面就从TabLayout源码进行分析ViewPager和TabLayout如何配合使用。
下面的代码是最简单的一个viewpager+tablayout+fragment的使用场景,那么最开始就从setupWithViewPage()对源码进行分析。
mFragments = new ArrayList<>();
mFragments.add(new NewsTypeFragment());
mFragments.add(new NewsTypeFragment());
mFragments.add(new NewsTypeFragment());
mViewPagerFragmentAdapter = new ViewPagerFragmentAdapter(getChildFragmentManager(), mFragments);
viewpager.setAdapter(mViewPagerFragmentAdapter);
tablayout.setupWithViewPager(viewpager);
viewpager和tablayout存在双向绑定的机制:
文章图片
image
绑定流程如下:
文章图片
屏幕快照 2018-02-04 下午5.13.29.png 通过监听viewpager, 与之绑定的TabLayout也随viewpager更改视图,以下是TabLayoutOnPageChangeListener的源码。
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
private final WeakReference mTabLayoutRef;
private int mPreviousScrollState;
private int mScrollState;
public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
mTabLayoutRef = new WeakReference<>(tabLayout);
}@Override
public void onPageScrollStateChanged(final int state) {
mPreviousScrollState = mScrollState;
mScrollState = state;
}@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {
final TabLayout tabLayout = mTabLayoutRef.get();
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
mPreviousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}@Override
public void onPageSelected(final int position) {
final TabLayout tabLayout = mTabLayoutRef.get();
if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
&& position < tabLayout.getTabCount()) {
// Select the tab, only updating the indicator if we're not being dragged/settled
// (since onPageScrolled will handle that).
final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
|| (mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
}
}void reset() {
mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
}
}
其中onPageScrollStateChanged() 得到viewpager的三种状态,并保存前置状态和当前状态,影响后续页面布局和动画效果。
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
* 表示viewpager的状态为静止状态(无动画、无滑动)
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* Indicates that the pager is currently being dragged by the user.
* 表示viewpager的状态滑动状态
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* Indicates that the pager is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = 2;
public void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels)该方法监听的是Viewpager的位置以及每个page的偏移量(这里解释一下positionOffset,它对应ViewPager当前page的偏移量,其中左划数值从0-1,右滑数值从1-0,后续会根据positionOffset计算整个HorizontalScrollView的位置)、对应的像素位置,onPageScrolled()和下面onPageSelected() 是与TabLayout联动最关键的两个方法 ,在这个方法中,会将position和positionOffset传递给setScrollPosition(),并通过这个方法更新TabLayout视图,其中包括,底部indicater(tab追踪条),text(tab的名称),HorizontalScrollView的偏移位置,运用对偏移量四舍五入的计算方法,设置tab标题颜色。这里要尤其注意,onPageScrolled返回的position会根据滑动方向改变,左滑position保持当前pager的值,而从静止开始往右滑动则变成当前page-1,尤其区分这里的position和onPageSelected返回的position。
void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
boolean updateIndicatorPosition) {
final int roundedPosition = Math.round(position + positionOffset);
if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
return;
}// Set the indicator position, if enabled
if (updateIndicatorPosition) {
mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
}// Now update the scroll position, canceling any running animation
if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
mScrollAnimator.cancel();
}
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
// Update the 'selected state' view as we scroll, if enabled
if (updateSelectedText) {
setSelectedTabView(roundedPosition);
}
}
public void onPageSelected(final int position) 该方法只有在动画完成,页面静止的时候调用,position显示当前page的页数(从0开始)
二、具体实现
2.1 tab底部indicator自定义 原生TabLayout的底部indicator默认是矩形条,并且只能修改其高度,所以它的可定制性非常低,而绘制矩形条的类SlidingTabStrip是私密内部类,所以为了自定义indcator需要将tablayout整体移植到自己的工程项目内,并修改SlidingTabStrip这个类。这里提供简单的三种自定义图形
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
//自定义画圆
//canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
//自定义三角形
Path path = new Path();
path.moveTo((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight - 10);
path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 - mSelectedIndicatorHeight - 10, getHeight());
path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 + mSelectedIndicatorHeight + 10, getHeight());
path.close();
canvas.drawPath(path, mSelectedIndicatorPaint);
//自定义矩形、条形(默认)
//canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
// mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
2.2 tab滑动机制自定义 通常TabLayout与fragment+ViewPager一起使用,不知道大家有没有遇到过这种情况,当设置ViewPager的setCurrentItem方法时,可以选择pager的滑动是否是smooth,true的时候,tablayout也是smooth,false的时候,tablayout的切换也变得生硬,包括现在的网易新闻,今日头条的tablayout就是这种机制。产生这种不协调的原因是因为上述监听ViewPager的onPageScrolled方法,点击tab的时候onPageScrolled方法返回的positionOffset一直为0,每次点击tab时,最后一次调用的是onPageScrolled方法而不是onPageSelected方法,通过debug点击tab时候的log可以看出来:
02-04 08:35:59.965 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1
02-04 08:36:08.591 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageSelected: 2
02-04 08:36:08.597 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1
所以,以最后一次onPageScrolled的监听为主,同时positionOffset为0,就导致没有动画效果,也就是导致点击tab很生硬的主要原因!
那有没有一种机制一能防止viewpager产生过渡动画,又能让tablayout有过渡动画。 其实很简单,就是监听positionOffset,当positionOffset大于0时执行setScrollPosition方法:
@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {
final TabLayout tabLayout = mTabLayoutRef.get();
Log.d("mTabLayoutRef", "onPageScrolled:1 ");
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
mPreviousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
if (positionOffset>0)
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
三、遇到的问题
遇到的最主要的问题就是在tab滑动机制自定义时,由于两个监听用了同一种动画,所以监听结果的顺序就很重要,不然显示的结果差强人意,通过debug发现返回position的顺序是最后返回onPageScrolled方法而不是onPageSelected,才发现问题所在。
四、总结
这次在写自己的demo的时候,本来是想仿写网易新闻和今日头条的顶部滑动菜单栏,然后发现都有这种点击tab时菜单栏无滚动效果的问题,通过看了TabLayout的源码,并改写才完善了这个功能,提高了用户体验,自己也积累了不少知识,总之再小的功能都有不断发掘和革新的价值!
附:Demo地址
推荐阅读
- 基于微信小程序带后端ssm接口小区物业管理平台设计
- 基于|基于 antd 风格的 element-table + pagination 的二次封装
- 基于爱,才会有“愿望”当“要求”。2017.8.12
- Android事件传递源码分析
- Quartz|Quartz 源码解析(四) —— QuartzScheduler和Listener事件监听
- [源码解析]|[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
- ffmpeg源码分析01(结构体)
- Java程序员阅读源码的小技巧,原来大牛都是这样读的,赶紧看看!
- Vue源码分析—响应式原理(二)
- SwiftUI|SwiftUI iOS 瀑布流组件之仿CollectionView不规则图文混合(教程含源码)