接触工作与滑动,一直是Android知识库中的一个不行避免被触及的一个大的方向。

由此,几个比较中心的论题需求咱们知道:

  1. 接触工作的传递模型;

  2. 怎样经过手指去滑动视图;

  3. 假如有多个可滑动视图的情况下,怎样合理地安排滑动的先后顺序;

  4. 怎样做到嵌套滑动

其间的一和二都是陈词滥调的论题,无非便是工作冒泡模型和滑动偏移的核算,3和4则会在一些事务中才会具体地被使用到,例如3,假如ViewPager嵌套RecylerView,那么此刻的工作应该交给谁以避免滑动冲突带来的不友爱的用户体验。再比如4,怎样在两个嵌套的可滑动View中,得到友爱的滑动体验,换句话说,ScrollerView和ScrollerView嵌套和NestedScrollerView、NestedScrollerView嵌套又有哪些差异?

咱们从一个最常见的问题开端说起,假如两个ScrollerView套在一起会发生什么?很简单,套在里边的ScrollerView直接无法滑动了,即便它的内容展现不全:

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

那么咱们要怎样处理这个问题呢?

网上很简单收到,把ScrollerView换成NestedScrollerView就能够了,所以,咱们再次测验,将外层的ScrollerView换成NestedScrollerView,你会发现,还是不行。再试,保持外面的ScrollerView不变,将里边的换成NestedScrollerView,你会发现,成功了,咱们完成了咱们的需求:

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

背后的原因,正是咱们今天所要探究的论题:Android接触与工作的传递

1. 工作冒泡模型

咱们知道,Android设备大多数是经过接触屏来进行外界工作输入的,当咱们输入的时分,接触屏会进行高密度的工作采样,将手指接触在屏幕上的某一个方位信息传递给咱们的操作体系,再由操作体系派发给具体的某一个Window,对应的Activity,终究经过View Hierarchy,到达咱们的View上,咱们就能够在View的代码中处理这一个接触工作。

实际上咱们手指点按下去的时分,高密度的采样,会触发一系列的接触工作:按下、很多个滑动采样和抬起,所以点按屏幕的时分,其间的第一个采样工作会经过如下的进程,从中心的「点击」开端:

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

首要有两个部分,一是下方的硬件层和体系代码层,这一块首要是屏幕感知触控,发生相应的工作。其次是软件层,也便是App层,Window会负责接纳从体系层面宣布的MotionEvent工作,也便是咱们的点击工作。而工作在体系层面的传递是向下的,先由屏幕传递给驱动,再由驱动传递给体系,体系内部处理完后吗,再向上派发给App。

而App层面的传递是向上的,从最底层的Window开端, 传递给DecorView,再是ViewGroup,终究找到方针View。

向上向下是怎样规则的?

你能够想象一下,现在最上方有一双眼睛在看着屏幕,它首要看到的一定是最顶部的View,再是ViewGroup,由于默许情况下,View Hierarchy是堆叠而成的。

虽然咱们点击了屏幕,动作是向下的,最底层处理完接触信号之后,终究点击工作却是往上浮的,就想往上冒泡一样,这便是咱们的工作冒泡模型。

2. View中和工作上浮的相关办法

咱们知道,工作是不断向上冒泡的,可是工作不断上浮的进程中,经过的一切View、ViewGroup或者其它的节点,都会面临一件工作:要不要消费该工作和要不要向下传递

2.1 工作的阻拦办法

例如上述的Activity、DecorView、ViewGroup和View2构成的一个View Hierarchy,它们四个都有权去消费一个工作,也有权利去决议一个工作是否要继续下发。假如你写了一个不讲武德的ViewGroup,重写它的办法:onInterceptTouchEvent,里边回来了true,那工作在ViewGroup这一层就被阻拦了,不会再上浮。

假定此刻工作现已上浮到View了,由于View并没有子View,他现已在View Hierarchy构成的树中是一个 叶子节点了,他就不需求再去考虑是否需求阻拦工作,所以View是没有 onInterceptTouchEvent的。

此外,默许情况下只会在dispatchTouchEvent中接纳ACTION_DOWN工作的时分调用onInterceptTOuchEvent。

2.2 工作的派发办法

onInterceptTouchEvent算是一个比较独立的办法,它只决议工作是否被ViewGroup阻拦。而工作上浮的进程中,最最首要的办法有两个,onTouchEventdispatchTouchEvent。前者是怎样去消费这个工作,后者是决议是否派发这个工作。

你可能会觉得奇怪,既然View没有子View,为什么要决议怎样去派发工作。

默许情况下,ViewGroup的dispatchTouchEvent被ViewGroup重写了,他会去遍历它一切的子View,依据方位检查是否是当时子View是否在接触点上,假如ViewGroup有多个,这个进程会被重复多次。

而View的dispatchTouchEvent则是去调用onTouchEvent,该办法的作用便是去消费工作,假如确定要消费回来true,不然回来false。

无论是View还是ViewGroup,dispatchTouchEvent的回来值的意义都是该工作是否被本View(ViewGroup)消费,View好了解,由于它在最顶端,不会再下发,它的回来值其实便是onTouchEvent的回来值。

ViewGroup要考虑向上派发,所以它会先向上派发工作,假如该工作被子View消费了,子View的dispatchTouchEvent会回来true,此刻该ViewGroup的dispatchTouchEvent将不会再去消费工作了,回来true,将一路向下传递,回来给操作体系;假如子View的dispatchTouchEvent回来了false,此刻ViewGroup就会调用自己的onTouchEvent去测验消费工作,于是乎ViewGroup在此刻成为了可能消费工作的“子View”。

所以对于1中图片描述的的工作冒泡模型只要一半,假如工作没有被子View消费,工作冒泡到顶端之后,工作会再次下沉,反着问View Hierarchy上的各个ViewGroup是否要消费工作,如绿色箭头;假如被子View消费了,工作同样会下沉,只不过余下的ViewGroup是没有机会再去消费这个工作了。

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

所以,但从工作传递上来看,View其实能够不要dispatchTouchEvent的,可是有一些其它的逻辑,比如NestedScroll,即嵌套滑动这类的办法严格来说不是自己消费工作,会在dispatchTouchEvent中做一些处理,将额外的数据派发给其他的工作,更多的意义还是将对外消费工作对自己消费工作区别开来。

此外,只要工作经过了ViewGroup,ViewGroup就能看见这个工作,只不过ViewGroup会依据子View的dispatchTouchEvent的回来值来决议是否去调用自己的onTouchEvent()。

2.3 工作的消费办法

便是在onTouchEvent去处理一个接纳的到的工作。

假如是叶子节点,通常是嵌套在ViewGroup中的一个View,它要接纳工作,就会接受第一个手指按下的工作,尔后的手指移动的工作会下发它来处理,也便是调用它的onTouchEvent来让View决议怎样处理这个工作。

假如在View的onTouchEvent对手指的「按下工作」回来了false,则阐明View将不会处理这一系列的工作,包含后续的手指移动(MOVE)、手指抬起(UP)等等,都将不会继续下发给它。

假如在View的onTouchEvent对手指的「滑动工作」回来了false,则阐明View不处理这次的滑动工作,按照冒泡机制,这个滑动工作本应该从头回到ViewGroup的dispatchTouchEvnet,可是此刻的ViewGroup也不会再去消费和这个工作了。

下一个滑动工作依然会派发给View,并不会由于MOVE工作没有被消费而越过该View。

换句话说,除非ViewGroup自动阻拦,不然一系列的工作,被View消费了一半,忽然View不用费了(onTouchEvent 回来false),ViewGroup也不会再去消费了。

3. 工作的分类

现在的工作机制,指的是不重写上述的 onInterceptTouchEvent dispatchTouchEvent 办法的情况下,由 View.class和ViewGroup.class 的默许的机制去处理工作。

3.1 首要工作

首要工作的分类其实首要就四种:

  1. ACTION_DOWN
  2. ACTION_MOVE
  3. ACTION_UP
  4. ACTION_CANCEL

别离对应手指的工作:按下、滑动和抬起,终究一个CANCEL则对应着工作的撤销,在根底的冒泡模型中,咱们先重视前三个。

理所应当地,工作在上浮的进程中,现在的工作机制会确保对应方位上的一切的View都能收到ACTION_DOWN工作。

为什么是ACTION_DOWN工作呢?

由于ACTION_DOWN工作是一次接触行为的起点,一次接触必定是以按下为起点,中心链接了多个移动,终究手指抬起,这儿就催生出一个定论:

  • 假如一个控件不接受ACTION_DOWN工作,那后续就不会得到ACTION_MOVE和ACTION_UP。

假如两个同归于ViewGroup的View叠在一起呢?

一样能收到,ViewGroup会优先遍历视图的上层View,由于假如上层View不用费工作,在ViewGroup遍历子View时,会接着遍历下一层View。

详见:再谈接触、滑动与嵌套滑动(二)第3部分

3.2 ACTION_CANCEL

ACTION_CANCEL并不归于首要工作,由于它直译为撤销。假如是手指在屏幕上滑动,无论怎样也不行能忽然导致工作的撤销,由于手指的动作无非便是按下、滑动和抬起,并没有撤销。所以,撤销的场景天然应该存在于冒泡模型之上,换句话说它不是由用户手指触发的,而应该是由一些特别的UI逻辑触发的。

那ACTION_CANCEL究竟是何时触发的?答案其实就在名字上,正是撤销

何谓撤销?

上文提到了,一切的View都会遭到ACTION_DOWN,即能够感知手指按下的动作。假如此刻有这么个场景:ScrollView里边有一个Button,咱们手指按在Button上,可是没有当即抬起,而是向上滑动,此刻的工作会怎样样呢?

首要Button必定会收到一个ACTION_DOWN,它宣布,它会消费这个ACTION_DOWN,作为一个Button,它肯定在等待ACTION_UP,以构成一个Click工作。

在此之后,ScrollView依然能收到这一串工作的后续工作,由于工作的冒泡一定会再经过ScrollView传递到Button。

假如你的手指当即抬起,此刻的Button就认为自己被点击了,于是调用performClick,触发点击监听办法;

可是假如你手指没有当即抬起,而是向上滑了一小段间隔(间隔 > TouchSlope)。

TouchSlope是体系所能识别出的能够被认为是滑动的最小间隔,小于TouchSlope的滑动不认为是滑动。

此刻工作经过ScrollView的时分,ScrollView发现现已上下滑动,而且超过了滑动阈值了,不能不管了,为了ScrollView中的内容能够正常上下翻滚,于是乎便阻拦该ACTION_MOVE工作,此刻便会派发给接纳ACTION_DOWN的Button一个ACTION_CANCEL。同时响应下一个ACTION_MOVE。这儿就衍生出另两个定论:

  1. 即便一个控件消费了ACTION_DOWN工作,也不意味着它就能收到本次接触后续的一切工作。
  2. 即便一个控件没有消费ACTION_DOWN工作,也可能会经过阻拦工作的方法消费后续的ACTION_MOVE工作。
  1. 这是很好观察的,由于Android 的Button自带水波纹作用,该工作被CANCEL之前水波纹一直都在;一旦收到CANCEL工作,水波纹就消失了,同时(收到下一个ACTION_MOVE之后)ScrollView开端滑动。
  2. 一次Click是由ACTION_DOWN和ACTION_UP构成的。
  3. 对于Button来说,收到ACTION_DOWN之后,收到几个ACTION_MOVE,再收到ACTION_CANCEL就没有后续了,这便是撤销的意义;
  4. 对于ScrollView来说,在Button收到ACTION_CANCEL之后,他会直接纳到滑动的ACTION_MOVE,直到手指抬起,再收到ACTION_DOWN,他并不会从头收到ACTION_DOWN。

上述现象的具体的流程是:

  1. 当外层的ScrollView探测到纵向的滑动符合自己的滑动条件,于是将mIsBeingDragged置为true,并在onInterceptTouchEvent中回来mIsBeingDragged,也便是true。
  2. 然后ScrollView会将原有的ACTION_MOVE替换成ACTION_CANCEL,到Button上。
  3. 尔后ScrollView便开端接收ACTION_MOVE工作:
2023-01-30 14:12:54.854 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), dispatchTouchEvent:ACTION_MOVE
2023-01-30 14:12:54.854 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), onInterceptTouchEvent:ACTION_MOVE
/**此刻ScrollView在onInterceptorTouchEvent中回来了true,阻拦工作,而且把MotionEvent的Action换成了ACTION_CANCEL**/
2023-01-30 14:12:54.854 18990-18990/com.example.actions D/rEd: CustomButton(267738707), dispatchTouchEvent:ACTION_CANCEL
2023-01-30 14:12:54.854 18990-18990/com.example.actions D/rEd: CustomButton(267738707), onTouchEvent:ACTION_CANCEL,consumed:true
/**这儿的MotionEvent和上面ACTION_MOVE的Event其实是一样的,不信你能够打印一下它们的,只不过类型被ScrollView替换掉了。**/
2023-01-30 14:12:54.905 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), dispatchTouchEvent:ACTION_MOVE
2023-01-30 14:12:54.905 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), onTouchEvent:ACTION_MOVE,consumed:true
2023-01-30 14:12:54.920 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), dispatchTouchEvent:ACTION_MOVE
2023-01-30 14:12:54.920 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), onTouchEvent:ACTION_MOVE,consumed:true
2023-01-30 14:12:54.940 18990-18990/com.example.actions D/rEd: CustomScrollView(165088834,), dispatchTouchEvent:ACTION_MOVE

ScrollView的ACTION_MOVE和Button的ACTION_CANCEL,对应的MotionEvent实际上是同一个目标,只不过ScrollView阻拦工作冒泡的时分,会将ACTION_MOVE换成ACTION_CANCEL,继续下发:

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

[Android]触摸、滑动与嵌套滑动(一)事件与滚动

此外,View忽然不行见(isVisible = false)和手指滑动到View之外都不会触发ACTION_CANCEL工作,可是ScrollView下方的容器LinearLayout忽然调用了removeView,导致对应的Button视图被删除的话也会触发ACTION_CANCEL工作。

4. View是怎样滑动的

咱们知道,在一次滑动的进程中,会有一个ACTION_DOWN,无数个ACTION_MOVE(具体的数量取决于采样次数)和一个ACTION_UP,滑动的实质,无非便是在监控手指的移动方位,所以咱们视图翻滚的中心,便是依据滑动工作来设置View内容的偏移量

一般来说,咱们在onTouchEvent接纳到ACTION_DOWN的时分去记录下初始数据,mLastX和mLastY用于标记上一次工作的滑动坐标点:

mLastX = event.x;
mLastY = evnet.y;

然后再ACTION_DOWN中,核算新的event的x和y与Last的差值:

val deltaX = mLastX - event.x;
val deltaY = mLastY - event.y;
mLastX = event.x;
mLastY = event.y;

这样一来,newX和newY便是滑动的偏移量,也便是接纳到本ACTION_MOVE之后的视图内容偏移量。

终究咱们需求让View动起来:

scrollBy(deltaX,deltaY);

所以,滑动的实质是:多个工作中手指方位的差值驱动View经过scrollBy办法做内容的偏移

5. 总结

咱们介绍了滑动的工作冒泡模型、工作派发的办法和细节以及工作的分类。

工作的派发首要遵从几个规律:

  1. ACTION_DOWN工作优先派发给叶子节点的View,只要子View不需求的情况下,父ViewGroup才有机会去消费这个ACTION_DOWN。

  2. 假如一个控件不接受ACTION_DOWN工作,那后续就不会得到ACTION_MOVE和ACTION_UP,所以父ViewGroup一般不会阻拦ACTION_DOWN工作,不然本次触控行为下的其他的ACTION_MOVE和ACTION_UP都将无法流到子View上。

  3. 即便一个控件消费了ACTION_DOWN工作,也不意味着它就能收到本次接触后续的一切工作。 ACTION_MOVE工作在父View滑动和子View点击发生冲突的时分,可能会被父View阻拦,并向子View派发ACTION_CANCEL工作,尔后的ACTION_MOVE工作由父View来消费。

  4. 即便一个控件没有消费ACTION_DOWN工作,也可能会经过阻拦工作的方法消费后续的ACTION_MOVE工作。

这些内容都是比较重要的,也是了解滑动冲突和嵌套滑动的根底。

~End