前言
上一篇文章讲了如何 从ViewPager的源码入手,定义自己的ViewPager滑动特效。ViewPager由于有自己的动画接口接口,我们可以直接拿到当前ItemView,以及它的position位置参数,因此可以做出任何我们能够想到的特效。
但是,TabLayout,谷歌貌似就没有那么周到的服务,像是今日头条那样的TabLayout 滑动时的 文字部分颜色变化,还有很多其他app中出现的下方横条indicator长短变化的 特效,还有更多其他特效。如果使用谷歌原生的TabLayout是无法做到的,这个时候就需要我们 自定义TabLayout,但是,说是自定义,前提还是要参照 谷歌的TabLayout源码,然后在其基础上进行再创作。因为,从0开始要制作一个 和谷歌原生同样质量的控件,包括滑动流畅度和边界控制,并且具有良好的扩展性,并没有那么容易,如果存在改造原生TabLayout的可能性,改造的代价要小于 从0创造。所以,优先 阅读源码,探寻这种可能性,如果没有可能性,再去从0创造。
Demo的地址为:https://github.com/18598925736/StudyTabLayout/tree/hank_v1
正文大纲
-
源码分析
-
开发思路
-
开始搬砖
-
一. 尊重原著
-
二. 联动滑动
-
三.特效解耦
正文
二. 联动滑动
下载源码之后,git checkout a132运行看效果
布局层级已经完成,现在需要联动Viewpager的滑动参数,让GreenTabLayout 跟随ViewPager一起滑动。
注册监听
要实现联动,首先要知道,谷歌源码中,TabLayout是如何与ViewPager发生联动的,它们的联结点在哪里,请看代码:
1. `tabLayout.setupWithViewPager(viewpager)`
平时我们用 原生TabLayout,两者唯一发生交集的地方就是这里,进入看源码:

显然他们的交集可能是某个回调监听,顺着这个线索,最终确定,上面的 pageChangeListener
就是 联动滑动的交界点,这里把监听器传给ViewPager,ViewPager则可以把自己的滑动参数传递给TabLayout,TabLayout则做出相应的行为。
监听器的源码为:
1. `privateTabLayoutOnPageChangeListener pageChangeListener;`
3. `publicstaticclassTabLayoutOnPageChangeListenerimplementsViewPager.OnPageChangeListener{`
4. `@Override`
5. `publicvoid onPageScrolled(finalint position, finalfloat positionOffset, finalint positionOffsetPixels) {`
6. `....`
7. `}`
8. `@Override`
9. `publicvoid onPageSelected(finalint position) {`
10. `...`
11. `}`
12. `@Override`
13. `publicvoid onPageScrollStateChanged(finalint state) {`
14. `...`
15. `}`
16. `}`
了解到这里,我们可以给 GreenTabLayuot 直接加上 这个接口实现
1. `classGreenTabLayout: HorizontalScrollView, ViewPager.OnPageChangeListener{`
2. `@Override`
3. `publicvoid onPageScrolled(finalint position, finalfloat positionOffset, finalint positionOffsetPixels) {`
4. `....`
5. `}`
6. `@Override`
7. `publicvoid onPageSelected(finalint position) {`
8. `...`
9. `}`
10. `@Override`
11. `publicvoid onPageScrollStateChanged(finalint state) {`
12. `...`
13. `}`
14. `}`
然后提供一个 相同的 setupWithViewPager(viewpager)
方法, 在内部,给ViewPager绑定监听,同时根据 viewPager的adapter内的 page数目,决定TabView的数目和每一个的标题。
1. `fun setupWithViewPager(viewPager: ViewPager) {`
2. `this.mViewPager = viewPager`
3. `viewPager.addOnPageChangeListener(this)// 注册监听`
4. `val adapter = viewPager.adapter ?: return`
5. `val count = adapter!!.count // 栏目数量`
6. `for(i in0until count) {`
7. `val pageTitle = adapter.getPageTitle(i)`
8. `addTabView(pageTitle.toString())// 根据adapter的item数目,决定TabView的数目和每一个标题`
9. `}`
10. `}`
参数分析
注册监听之后,Viewpager可以把自己的滑动参数的变化告知TabLayout,但是TabLayout如何去处理这个参数变化,还需要从参数的规律上去着手。重点分析 监听的 onPageScrolled
方法, 重点中的重点,则是前两个参数:position(当前page的index) 和 positionOffset(当前page的偏移百分比,小数表示的)
为了研究规律,我们用上面刚刚完成的代码把GreenTabLayout和ViewPager连结上,然后打印日志 onPageScrolled
:

基本得出一个结论:
position为0的,为当前选中的这个page,当慢慢从当前page划走时,它的positionOffset会从0慢慢变成1
并且,如果手指分方向滑动试验,可知:
当手指向左,positionOffset会递增,从0到极限值1,到达极限之后归0,同时 position递加1
反之,手指向右,positionOffset会递减,从1 递减到0,从递减的那一刻开始,position递减1。
基于上面的规律,我们可以调试出 indicator横条动画的代码:
1. `...`
2. `override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {`
3. `scrollTabLayout(position, positionOffset)`
4. `}`
6. `private fun scrollTabLayout(position: Int, positionOffset: Float) {`
7. `// 如果手指向左划,indicator横条应该从当前位置,滑动到 下一个子view的位置上去,position应该+1`
8. `// 如果手指向右滑动,position立即减1,indicator横条应该从当前位置向左滑动`
9. `val currentTabView = indicatorLayout.getChildAt(position) asGreenTabView`
10. `val currentLeft = currentTabView.left`
11. `val currentRight = currentTabView.right`
13. `val nextTabView = indicatorLayout.getChildAt(position + 1)`
14. `if(nextTabView != null) {`
15. `val nextLeft = nextTabView.left`
16. `val nextRight = nextTabView.right`
18. `Log.d("scrollTabLayout","当前index:${position} left:${currentLeft} right:${currentRight} "+" 目标index:${position + 1} left:${nextLeft} right:${nextRight} positionOffset:${positionOffset}")`
20. `val leftDiff = nextLeft - currentLeft`
21. `val rightDiff = nextRight - currentRight`
23. `indicatorLayout.updateIndicatorPosition(`
24. `currentLeft + (leftDiff * positionOffset).toInt(),`
25. `currentRight + (rightDiff * positionOffset).toInt()`
26. `)`
27. `}`
28. `}`
为什么这样就能正确区分滑动的方向?把日志打印出来一看就明白:
这是手指向左划一格:

-
观察positionOffset的变化,从0 变为1,然后归零。
-
而看横条的当前 left = 26,right=170, 以及 目标left=222,right=380 ,随着positionOffset的递增,横条会慢慢向右。
-
而到达最后,positionOffset归零了,当前left 也变成了 目标的left = 222,right=380.
横条向右平移完成。
而手指向右划一格,日志如下:

-
position先直接减1,positionOffset则从1慢慢变成0.
-
横条从 left=26 right=170 的起始位置,向 目标 left=222,righ=380 移动,但是由于positionOffset是递减的,所以,横条的移动方向反而是 向左。一直到positionOffset为0,到达 left=26 right=170.
横条向左平移也完成。
整体平移
横条虽然可以跟着viewPager的滑动而滑动,但是如果TabView已经排满了当前屏幕,横条到达了当前屏幕最右侧,viewPager上右侧还有内容还可以让手指向左滑动。此时,就必须滚动最外层布局,来让TabView显示出来。
通过观察原生TabLayout,它会尽量让 当前选中的tabView位于 控件的横向居中的位置。而随着 ViewPager的当前page的变化,最外层GreenTabLayout也要发生横向滚动。
所以我选择在 回调函数onPageSelected中执行滚动:
1. `classGreenTabLayout: HorizontalScrollView, ViewPager.OnPageChangeListener{`
2. `...`
3. `override fun onPageSelected(position: Int) {`
4. `val tabView = indicatorLayout.getChildAt(position) asGreenTabView`
5. `if(tabView != null) {`
6. `indicatorLayout.updateIndicatorPositionByAnimator(tabView, tabView.left, tabView.right)`
7. `}`
8. `}`
9. `}`
执行滚动的思路为:
-
确定 当前选中的tabView的 矩形范围
tabView.getHitRect(tabViewBounds)
-
确定 确定最外层GreenTbaLayout的矩形范围
getHitRect(parentBounds)
-
计算两个矩形的x轴的中点,然后计算出两个中点的差值,差值就是需要滚动的距离
-
使用属性动画进行平滑滚动
1. `/**`
2. `* 用动画平滑更新indicator的位置`
3. `* @param tabView 当前这个子view`
4. `*/`
5. `fun updateIndicatorPositionByAnimator(`
6. `tabView: GreenTabView,`
7. `targetLeft: Int,`
8. `targetRight: Int) {`
9. `...`
10. `// 处理最外层布局( HankTabLayout )的滑动`
11. `parent.run {`
12. `tabView.getHitRect(tabViewBounds) //确定 当前选中的tabView的 矩形范围`
13. `getHitRect(parentBounds) // 确定最外层GreenTbaLayout的矩形范围`
14. `val scrolledX = scrollX // 已经滑动过的距离`
15. `val tabViewRealLeft = tabViewBounds.left - scrolledX // 真正的left, 要算上scrolledX`
16. `val tabViewRealRight = tabViewBounds.right - scrolledX // 真正的right, 要算上scrolledX`
18. `val tabViewCenterX = (tabViewRealLeft + tabViewRealRight) / 2`
19. `val parentCenterX = (parentBounds.left + parentBounds.right) / 2`
20. `val needToScrollX = -parentCenterX + tabViewCenterX // 差值就是需要滚动的距离`
22. `startScrollAnimator(this, scrolledX, scrolledX + needToScrollX)`
23. `}`
24. `}`
26. `/**`
27. `* 用动画效果平滑滚动过去`
28. `*/`
29. `private fun startScrollAnimator(tabLayout: GreenTabLayout, from: Int, to: Int) {`
30. `if(scrollAnimator != null&& scrollAnimator.isRunning) scrollAnimator.cancel()`
31. `scrollAnimator.duration = 200`
32. `scrollAnimator.interpolator = FastOutSlowInInterpolator()`
33. `scrollAnimator.addUpdateListener {`
34. `val progress = it.animatedValue asFloat`
35. `val diff = to - from`
36. `val currentDif = (diff * progress).toInt()`
37. `tabLayout.scrollTo(from+ currentDif, 0)`
38. `}`
39. `scrollAnimator.start()`
40. `}`
二阶效果
完成到这里,就能达成下图中的效果:
上半部分为原生TabLayout效果,下半部分为刚刚完成的效果,几乎没有差别了。
当然,我们这是把TabLayout本体化,完成这些,仅仅用了kotlin 300多行代码。可见Kotlin在节省代码方面,确实是一绝,比java简洁很多。
请期待下一篇:全能型GreenTabLayout研发全攻略(3_特效解耦)
网友评论