基于TabLayout源码实现自定义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存在双向绑定的机制:
基于TabLayout源码实现自定义TabLayout
文章图片
image
绑定流程如下:

基于TabLayout源码实现自定义TabLayout
文章图片
屏幕快照 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地址

    推荐阅读