一个能让普通View轻松实现嵌套滚动的控件

add0ee47554fbb8f5e90790bdbb97d1a.jpeg

dc57746f616cef0695991d4ab203c1c7.gif

本文字数:2810

预计阅读时间:20分钟

01

背景和现状

在Android的事件分发机制当中,在同一个事件流中,如果由父控件拦截/消费了,那么子控件就没办法再获取到该事件流。这种传统的事件分发机制在嵌套滚动时会有明显不足,就是子控件无法消费父控件没有消费掉的滑动距离(因为起初是父控件接收了滑动事件,那么在同一个事件流中,所有事件都会给到父控件,子控件接收不到事件,也就无法滑动),从而造成嵌套滚动的不连贯(如果想滑动子控件就只能抬起手指结束本次事件流,并在下一次事件中滑动子控件)。使用嵌套滚动机制可以有效的解决上面的问题。它并没有改变事件分发机制,在发生嵌套滚动时,还是先进行事件分发,由父View将事件分发给子View,由子View进行消费。只不过,子View在自己消费之前,会先去询问父View,是否需要处理滑动事件,如果父View需要处理,就先交由父View进行滑动,父View滑完了,子 View 才进行滑动。在嵌套滑动中有两个角色:Child 和 Parent,Child 需要实现 NestedScrollingChild/NestedScrollingChild2/NestedScrollingChild3 接口之一,比如系统控件RecyclerView,而 Parent 需要实现 NestedScrollingParent/NestedScrollingParent2/NestedScrollingParent3 接口之一,比如系统控件CoordinatorLayout。在Android系统中要实现嵌套滚动,子View需实现NestedScrollingChild接口才能和实现了NestedScrollingParent接口的父View进行嵌套滚动。但是,在复杂的业务场景中,子View可能只是普通View(或ViewGroup),其本身并没有实现NestedScrollingChild接口,普通的子View可能是各种各样的控件,而且不止一个。

现有技术方案的缺点

子View要想和实现了NestedScrollingParent的父View进行嵌套滚动,子View就必须自己实现NestedScrollingChild接口,并处理复杂的计算细节,比如判断是否是滑动行为、什么时候需要和合作的父View交互,更重要的是还需要处理比较麻烦的fling惯性。而且在复杂的业务场景下,子View可能是各种各样普通的View(比如FrameLayout、LinearLayout等),它们是没有实现NestedScrollingChild接口的,常规的做法是,这些普通的子View都需要各自实现NestedScrollingChild接口,导致开发和维护成本高,而且有些子View是第三方模块,不方便或不允许修改。

02

全新技术方案

为了解决上述问题,本技术方案实现了一个通用控件NestedScrollFrameLayout,继承自FrameLayout,实现了NestedScrollingChild接口,子View要想和实现了NestedScrollingParent接口的父View进行嵌套滚动,将NestedScrollFrameLayout包裹住子View即可,也就是把NestedScrollFrameLayout作为子View的父View。极大地降低了开发成本,而且不需要对子View进行任何修改,就能简单方便地实现嵌套滚动。

原理简述

NestedScrollFrameLayout同时实现了手指触摸屏幕和fling惯性导致的嵌套滚动。手指触摸屏幕产生一系列Motion Event,NestedScrollFrameLayout会检测判断是否是滑动行为,如果是滑动行为,就拦截后续所有的Motion Event,将滑动过程中不断产生的滑动距离先分发给合作的父View,合作的父View预处理后,将剩余滑动距离返给NestedScrollFrameLayout, NestedScrollFrameLayout不处理剩余的滑动距离,再将剩余的滑动距离分发给合作的父View,当手指抬起时通知合作的父View嵌套滚动结束了。同时,根据一系列Motion Event计算出滑动速度,根据滑动速度,每隔动画时间步长计算一次fling惯性距离,和手指触摸屏幕类似,将不断产生的fling惯性距离先分发给合作的父View,合作的父View自己来决定要不要处理分发过来的fling惯性距离以及消耗多少fling惯性距离,NestedScrollFrameLayout不处理剩余的fling惯性距离,再将剩余的fling惯性距离分发给合作的父View, 直到fling停止通知合作的父View嵌套滚动结束了。

03

技术方案详细阐述

构造一个实现了NestedScrollingChild接口的控件:NestedScrollFrameLayout继承自FrameLayout,并实现了NestedScrollingChild接口。为了增加用户的使用体验,本技术方案不仅实现了手机触摸屏幕导致的嵌套滚动,同时实现了手指离开屏幕后的fling惯性导致的嵌套滚动,让用户操作起来感觉很流畅,没有卡顿感。

1.手指触摸屏幕导致的嵌套滚动

(1)通知合作的父View手指触摸屏幕导致的嵌套滚动开始:

NestedScrollFrameLayout的onInterceptTouchEvent函数开始接收事件流(down、move、move...up),down事件时,通过NestedScrollingChild接口的startNestedScroll函数通知合作的父View手指触摸导致的嵌套滚动开始,并记录初始触摸位置的y值mInitialTouchY(竖向)和上一次触摸位置的y值mLastTouchY,两者在down事件时是相同的;

(2)判断当前的事件流是不是滑动行为:

NestedScrollFrameLayout的onInterceptTouchEvent函数接收一系列的move事件,如果当前触摸位置的y值与mInitialTouchY差值的绝对值大于mTouchSlop,说明用户在屏幕上的操作是滑动行为而不是点击行为,更新mLastTouchY,并返回true,表明NestedScrollFrameLayout要拦截后续的move和up事件 ;

(3)将滑动距离分发给合作的父View:

NestedScrollFrameLayout的onTouchEvent函数接收被拦截的move事件(事件被拦截后,事件流就会从onInterceptTouchEvent函数进入onTouchEvent函数),根据当前触摸位置的y值与mLastTouchY的差值,计算出滑动距离,将滑动距离通过NestedScrollingChild接口的dispatchNestedPreScroll函数分发给合作的父View,NestedScrollFrameLayout不处理合作的父View未消费的滑动距离,直接将未消费的滑动距离通过NestedScrollingChild接口的dispatchNestedScroll函数再分发给合作的父View。重复步骤(3)的操作,直到手指离开屏幕 ;

(4)通知合作的父View手指触摸屏幕导致的嵌套滚动结束:

NestedScrollFrameLayout的onTouchEvent函数接收被拦截的up事件,通过NestedScrollingChild接口的stopNestedScroll函数通知给合作的父View。至此,手指触摸屏幕导致的嵌套滚动结束,开始fling惯性导致的嵌套滚动。

2.fling惯性导致的嵌套滚动

(1)通知合作的父View fling惯性导致的嵌套滚动开始,并计算出滑动速度:

NestedScrollFrameLayout的onTouchEvent函数接收被拦截的up事件,通过NestedScrollingChild接口的startNestedScroll函数通知合作的父View fling惯性导致的嵌套滚动开始。并采用Android系统提供的VelocityTracker来追踪触摸屏幕产生的事件流,up事件即手指离开屏幕时,计算出y轴上的滑动速度yVelocity;

(2)将fling距离分发给合作的父View:

采用Android系统提供的OverScroller根据(1)中计算出来的滑动速度yVelocity,每隔动画时间步长计算一次fling距离(当前的偏移量y值与上一次动画时间步长偏移量y值mLastFlingY的差值),并将fling距离通过NestedScrollingChild接口的dispatchNestedPreScroll函数分发给合作的父View,NestedScrollFrameLayout不处理合作的父View未消费的fling距离,直接将未消费的fling距离通过NestedScrollingChild接口的dispatchNestedScroll函数再分发给合作的父View。重复步骤(2)的操作,直到fling结束;

 (3)通知合作的父View fling惯性导致的嵌套滚动结束:

fling结束(通过OverScroller通过的函数,可以获取到fling是否结束)时,通过NestedScrollingChild接口的stopNestedScroll函数通知给合作的父View。至此,fling惯性导致的嵌套滚动也结束了。

完整流程图

7f81aec7da0579f3ca1aa898afee42c8.png

技术方案的优点

本技术方案实现了一个通用控件NestedScrollFrameLayout,实现了NestedScrollingChild接口。只需要将此控件包裹住子View,就可以让子View具有和实现了NestedScrollingParent接口的父View嵌套滚动的能力,基本上是一键接入,开发和维护成本极低;同时此控件实现了手指触摸屏幕和fling惯性导致的嵌套滚动,交互连贯,提供了很好的用户体验。同时本技术方案可以应用于多种使用场景,如下拉刷新、协调布局(CoordinatorLayout)交互中等场景。

04

术语解释

1.View:是一种界面层的组件的一种抽象,它代表了一个组件,是Android中所有控件的基类;

2.ViewGroup:是View的子类,它也具有View的特性,但它主要用来充当View的容器,将其中的View视作自己的孩子,对它的子View进行管理;

3.FrameLayout:帧布局组件,ViewGroup的子类,用于在屏幕上创建一块空白区域,添加到该区域中的每个子控件占一帧,这些帧会一个一个叠加在一起,后加入的控件会叠加在上一个控件上层;

4.事件流:手指触摸屏幕会产生一系列触摸事件(Motion Event),手指触摸屏幕产生down事件开始和大量的move事件,到手指离开屏幕产生up事件;

5.嵌套滚动(Nested scrolling):手指触摸屏幕或fling惯性能够让父View和子View能够同时滚动的交互就叫做嵌套滚动;

6.NestedScrollingChild、NestedScrollingParent:是Android官方提供的为了实现嵌套滚动的接口,让父View和子View在滚动时互相协调配合,极大的方便了开发者对于嵌套滚动的处理;

7.阈值(mTouchSlop):手指滑动距离大于此阈值就认为用户正在进行滑动行为,可以通过系统ViewConfiguration类来获取;

8.动画时间步长(animation time step):更新屏幕画面帧之间的时长;

9.VelocityTracker:Android系统提供的能追踪手指速度的类,通过一个完整的事件流,当用户手指抬起的时候,就可以计算出手机滑动的速度;

10.OverScroller:Android系统提供的能够计算惯性偏移量的辅助类,根据VelocityTracker计算出的滑动速度,可以获得每个动画时间步长fling惯性的偏移量;

11.fling惯性:手指在屏幕上快速滑动,这种行为叫做fling,手指离开屏幕后,会有惯性。

参考资料:

1.Android 嵌套滑动:

https://blog.csdn.net/tmacfrank/article/details/120402886

2.Android NestedScrolling解决滑动冲突问题(2) - fling问题与NestedScroll++:

https://juejin.cn/post/6844903732539293704

3.安卓实现View的惯性滚动效果(Fling):https://blog.csdn.net/qq_45383789/article/details/126007457

4.Android 基础知识之 VelocityTracker使用(提供手指速度计算,可用于笔迹优化哦):

https://blog.csdn.net/c_he_n/article/details/82890061

5.Android View深入解析基础知识VelocityTracker,GestureDetector,Scroller:

https://it.cha138.com/ios/show-49309.html)

6.Android进阶宝典 -- NestedScroll嵌套滑动机制实现吸顶效果:

https://juejin.cn/post/7191404521916989495

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值