美文网首页高级UI
全能型GreenTabLayout研发全攻略之联动滑动

全能型GreenTabLayout研发全攻略之联动滑动

作者: 享学课堂 | 来源:发表于2020-05-22 16:42 被阅读0次

    前言

    上一篇文章讲了如何 从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_特效解耦)

    相关文章

      网友评论

        本文标题:全能型GreenTabLayout研发全攻略之联动滑动

        本文链接:https://www.haomeiwen.com/subject/jhzqahtx.html