Android技能树 — View事件体系小结

2018-02-11 14:12:48来源:https://juejin.im/post/5a7bb25e5188257a5911b06a作者:稀土掘金人点击

分享

最近年底了,打算把自己的Android知识都整理一下。


Android技能树系列:

Android技能树 — 动画小结 Android技能树 — View小结 Android技能树 — Activity小结 Android技能树 — View事件体系小结


这次是讲View的事件体系。特别是不同情况下的事件分发,我会用很简单的方式教会大家。


还是老样子,先上脑图,然后具体一块块详细说明。





View事件体系





我们通过具体案例来学习
View相关的基础知识

比如我们现在的需求是这样的:界面上有一个按钮,我们的手指点击这个按钮后滑动,这个按钮可以跟着我们的手指一起滑动。(桌面的一些小的清理垃圾的悬浮窗的操作差不多,明白了吧)


具体实现可以看我以前写过的文章,十分简单:小Demo大知识-控制Button移动来学Android坐标





我们来分析,既然按钮可以跟着我们手指滑动,我们肯定是不停告诉按钮,当前你的位置是哪里,既然涉及到一些基本知识点,比如View的位置参数等等。


View的位置参数



这里我配上一张图,更清楚的来说明这些获取各自参数的值的说明:





看了这个图,是不是马上很清楚了。


注意点:



这里要说明一个误区,我面试一些初级水平安卓,我说ViewGroup里面有个View,这个View的 getLeft(),getTop(),getTop(),getBottom() 是什么,让他画给我看下,有些人会给下面这个答案:





错误的回答


这是错误的答案,而且根据正确的描述图,我们可以通过 getLeft(),getTop(),getTop(),getBottom() 来获取相应的View的宽高:


width = getRight() - getLeft();
height = getBottom() - getTop();



View操作相关知识



MotinoEvent



MotionEvent是什么,单独问大家可能有点懵逼,我们来写下我们平常经常写的设置触摸的监听方法:


view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});

有没有发现,里面传递过来的参数就有 MotionEvent 。





我们可以看到,MotionEvent是触屏事件。当用户触摸屏幕时将产生触屏事件,事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。


具体的介绍真的很多,百度一搜一大把。要细讲实在太多了。这里不多介绍了。


特别提示!!!很多人会把上面我们提到过的 view.getX/Y(),view.getRawX/Y() 和这里的 motionvent.getX/Y(),motionevent.getRawX/Y() 弄混。这里是有差别的。我再画个图来明确下二者的区别。





所以区别是:


View的getX/Y()是指自己View的左上角相对于父View左上角的距离。MotionEvent的getX/Y()是指点击处离自己View的左上角的距离。
View的getRawX/Y()是指自己View的左上角相对于屏幕左上角的距离。MotionEvent的getRawX/Y()是指点击处相对于屏幕左上角的距离。

ps:所以面试官问你getX/Y()和getRawX/Y()的时候,一定问清楚是问的哪个。!不然很容易回答错误。





TouchSlop



TouchSlop是系统所能识别出来的被认为滑动的最小的距离。如果你手指在屏幕上滑动的时候小于这个值,系统就认为你不是滑动。


VelocityTracker

滑动时候我们可能还要监听速度,比如说我们的需求就是滑动的快和滑动慢,移动的最终距离不同等。这时候我们一定要知道当前用户在N时间段内的速度到底是什么。这时候我们就需要速度(Velocity)追踪者(Tracker)。





GestureDetector



我们先来看看英文翻译:





没错,既然你在屏幕上操作,你可能是划来划去,可能是单击,可能是双击。很多情况。所以这个类就可以帮我们来监听不同的操作。


ScaleGestureDetector



在GestureDetector前面添加了一个Scale。





那就明显是比例的手势监测,通俗来说就是放大缩小的手势监测。


比如我们的需求是在查看图片的时候,可以二个手指放大缩小图片,那我恩就可以用这个 ScaleGestureDetector 来监测。十分方便。


附上我以前写过的文章: 图片操作系列 —(1)手势缩放图片功能





View的事件分发机制



事件传递三个阶段及事件处理的类






其实这二个算是基础知识。


接下去我会用一个真实的例子带你们更好的理解事件分发,如果讲的不合理,可以提出来哦✧(≖ ◡ ≖✿)



举个例子:

PS:(如果例子不适合,大家可以评论反馈。因为如果例子不适合反而误导了读者,反而是我的问题了。)


好比你们公司是一个软件外包公司,现在有个客户手点了一下鼠标发给你们老板一封邮件,说要开发这么一个APP。你们老板是不是会一层层的分发下去,老板 ——> 主管 ——>开发人员。


额外提到点:


你们老板收到了通知就是把这个任务分下去,不可能说第一反应先想想说我要不要把这个任务拦下来自己做,不要叫手下的人去做了(不然还请你们干嘛,请了你们还要每次想着要不要自己做)。所以他没有拦截功能,默认肯定不会去拦截,肯定第一反应就是直接给手下。


主管都是有权利把任务拦下来的,不给手下的人去做,可以自己处理,毕竟主管不只是就分配下任务就够了,这么简单我也想去做主管,可能因为手下都有任务在做,忙不过来的时候,主管会自己去做一些开发任务。


最底层的开发人员,没有拦截功能,因为任务分到你这里了。你还能再给谁呢,拦了也是你做,不拦你又没有下级可以给背锅,还是你做。


所以对比下知道是不是发现跟我们的 Activity,ViewGroup,View 很像:


PS:当收到触摸事件传递到某个层的时候,这个的dispactchEvent会被调用。(相当于上面接受到通知任务的时候会运行这个方法)

老板 - Activity:有收到通知的能力,所以会调用dispatchTouchEvent(),然后因为他可以去通知主管,所以是


客户通知老板你有项目了。老板的dispatchEvent()会被调用。
老板.dispatchTouchEvent(){
//老板先通知主管去处理,
如果主管给的回复是:老板你不用管接下去的事。我们会处理的。
if(主管.dispatchTouchEvent()){
return true;//就直接结束了。
}
//手下的人说这个app开发不了,只能老板出马做事(跟客户去沟通去)
return 老板.onTouchEvent();
}

所以只有 dispatchEvent() 和 onTouchEvent() 方法。


主管 - ViewGroup
老板通知了主管有个app要你们部门去开发。主管的dispatchTouchEvent()会被调用
主管.dispatchTouchEvent(){
//主管把这个活拦下来准备自己来开发这个app
if(主管.interceptTouchEvent()){
return 主管.onTouchEvent();//主管也有做事能力
}else{
//主管不拦截,主管也可以去通知开发人员,
//如果开发人员回馈说主管你别管了。我们这个app能做好
if(开发人员.dispatchTouchEvent()){
return true; //直接就结束了。
}else{
//如果手下的开发人员也反馈给主管说搞不定。
//就只能主管自己出来做事了。
return 主管.onTouchEvent();
}
}
}

所以有 dispatchTouchEvent()、interceptTouchEvent()、onTouchEvent() 。


开发人员 - View
主管通知了开发人员有个app要开发。开发人员的dispatchTouchEvent()会被调用
开发人员.dispatchTouchEvent(){
return 开发人员.onTouchEvent();
}

所以有 dispatchTouchEvent()、onTouchEvent() 。





不同返回值导致不同的流程

我知道大家一定看到过类似下面的这种图:





很多人都会死记硬背的去记下来,说return true/false/super等不同情况下不同的调用流程。但是这样其实很不好记住的。很多人会问我是怎么记住的,我就是用伪代码来帮忙记住,什么事伪代码,上面那种表达方式就是伪代码。我们现在正是来看具体的伪代码。


Activity的真实代码:
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
/**
调用window的superDispatchTouchEvent方法,
然后再调用下面的ViewGroup(DecorView)的dispatchTouchEvent()方法。
我们就直接这么想,这里就Activity通知了ViewGroup的dispatchTouchEvent方法。
1.如果这里getWindow.superDispatchTouchEvent()返回了true,
这时候就会执行return true语句。
2.如果这里getWindow.superDispatchTouchEvent()返回了false,
这时候就会执行return onTouchEvent(ev);这句,
所以只有当上面的if语句返回false,
才有机会调用Activity自己的onTouchEvent()方法。
*/
if(getWindow.superDispatchTouchEvent()){
return true;
}
return onTouchEvent(ev);
}

所以很多人会所你重写Activity的dispatchTouchEvent()方法,返回true/false,都直接结束了事件。返回super才能正常分发,这个说法是不合理的。实际应该这么描述:


默认重写Activity的 dispatchTouchEvent 方法:


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/**
其实是调用了super.dispatchTouchEvent方法,
才会调用上面我们贴出的Activity的dispatchTouchEvent方法,
才能继续把事件分发下去。
*/
return super.dispatchTouchEvent(ev);
}

而大家通俗上说返回 true/false 就事件结束,是因为没有调用了 super.dispatchTouchEvent(ev); 。所以就不会分发下去,也就事件结束了。


那假如我这么写呢:


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
return true/false;
}

没错,事件也是一样会分发下去。子View的方法也会被调用,而不会说直接结束了。


ViewGroup

因为上面我们已经说过了 getWindow.superDispatchTouchEvent() 可以直接理解为是去调用了ViewGroup的 dispatchTouchEvent() ;


ViewGroup的伪代码:
public boolean dispatchTouchEvent(MotionEvent ev){
/**
如果ViewGroup做了拦截,
则直接返回了ViewGroup的onTouchEvent()事件的结果。
*/
if(onInterceptTouchEvent(ev)){
return onTouchEvent(ev);
}else{
/**
如果ViewGroup不做拦截,则先分发给child,
看他们的反应,他们都不接受,则一定会返回false,
则只能ViewGroup自己去执行自己的onTouchEvent(ev);
*/
if(child.dispatchTouchEvent(ev)){
return true;
}else{
return onTouchEvent(ev);
}
}
}
View的伪代码:
public boolean dispatchTouchEvent(MotionEvent ev){
/**
View 就返回自己的onTouchEvent()
*/
return onTouchEvent();
}

可能很多人还是说我看了这些代码还是不懂啊,我连起来给你看,你就理解了。





这样,在不同情况下,返回不同的false/true,执行顺序就知道了。


额外补充:
《补充1》:

当然其实还有更复杂的情况,我们知道有 ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL 等,比如我们直接ViewGroup拦截Down事件,或者Down事件传递到了View后,我们在MOVE处再拦截,都会执行不同的:


DOWN事件被传递给ViewGroup的onInterceptTouchEvent()后,该方法返回true,表示拦截该事件,说明ViewGroup自己要处理该事件(事件不再往下传递);调用自身的onTouchEvent()处理事件(DOWN事件将不再往上传递给Activity的onTouchEvent());该事件列的其他事件(Move、Up)将直接传递给ViewGroup 的onTouchEvent()。
若 ViewGroup 拦截了一个半路的事件(如MOVE),该事件将会被系统变成一个CANCEL事件 并且 传递给之前处理该事件的子View; 该事件不会再传递给ViewGroup 的onTouchEvent(); 只有再到来的事件才会传递到ViewGroup的onTouchEvent()。
《补充2》:



我们刚记不记得我们的View的伪代码是这样的:


public boolean dispatchTouchEvent(MotionEvent ev){
return onTouchEvent();
}

其实上面是做了简化,其实除了 onTouchEvent ,还有 onTouch 事件和 onClick 事件,我们继续用伪代码来说明规则:


public boolean dispatchTouchEvent(MotionEvent ev){
if(设置了TouchListener){
if(onTouch的返回值){
return true;
}else{
return onTouchEvent();
}
}
return onTouchEvent();
}
public boolean onTouchEvent(){
if(设置了ClickListener){
执行onClick;
}
.......
}



View的滑动

既然我们学会了View的事件体系,很多人说那我学会了能怎么样,最明显的就是我们可以用来解决很多滑动冲突事件。因为我们可以根据实际需求,选择性的拦截,然后做自己的事件处理。


所以我们具体来看View的滑动有关的知识:





View的滑动的基本知识我就不特意提出来了。大家可以分别去搜索。


主要是第二块View的滑动冲突。我们就以最简单的外部左右滑动,内部上下滑动为例子。





外部左右滑动,内部上下滑动


比如我们规定,滑动的角度是N度以内的时候就是说明我们在内部滑动,角度是N度以外的时候是外部滑动。


外部拦截法

默认父元素拦截,然后再适合的条件下,不让父元素拦截。


public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted=false;
int x= (int) event.getX();
int y= (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;
//必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件){
intercepted=true;
}else {
intercepted=false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
default:
break;
}
mLastXIntercept=x;
mLastXIntercept=y;
return intercepted;
}

内部拦截法:

默认刚开始是不允许父元素做拦截,也就是子元素刚开始就调用requestDisallowInterceptTouchEvent(true);方法,禁止父元素做拦截,然后再适合的条件再让父元素拦截。


子元素的dispatchTouchEvent()重写:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;if (父容器需要当前点击事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:{
break;
}
}
mLastXIntercept = x;
mLastYIntercept = y;
return super.dispatchTouchEvent(ev);
}

同时还要修改父容器的onInterceptTouchEvent()方法,不能做拦截,因为如果刚开始DOWN就拦截了,后面的MOVE,UP都没机会到子元素的上面的代码。


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台