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

3,776 阅读12分钟

前言

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

Android技能书系列:

Android基础知识

Android技能树 — 动画小结

Android技能树 — View小结

Android技能树 — Activity小结

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

Android技能树 — Android存储路径及IO操作小结

Android技能树 — 多进程相关小结

Android技能树 — Drawable小结

数据结构基础知识

Android技能树 — 数组,链表,散列表基础小结

Android技能树 — 树基础知识小结(一)

Android技能树 — Fragment总体小结

算法基础知识

Android技能树 — 排序算法基础小结

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

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

脑图链接:View事件体系

View事件体系

我们通过具体案例来学习

View相关的基础知识

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

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

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

View的位置参数

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

(!!!!!这里我多画了getRawX和getRawY方法,View是没有这二个方法的,请注意!!!!!)

(!!!!!这里我多画了getRawX和getRawY方法,View是没有这二个方法的,请注意!!!!!)

(!!!!!这里我多画了getRawX和getRawY方法,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()和这里的motionvent.getX/Y()弄混。这里是有差别的。我再画个图来明确下二者的区别。

所以区别是:

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

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

TouchSlop

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

VelocityTracker

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

GestureDetector

我们先来看看英文翻译:

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

ScaleGestureDetector

在GestureDetector前面添加了一个Scale。

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

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

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

View的事件分发机制

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

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

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

举个例子:

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


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

额外提到点:

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

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

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

所以对比下知道是不是发现跟我们的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处再拦截,都会执行不同的:

  1. DOWN事件被传递给ViewGroup的onInterceptTouchEvent()后,该方法返回true,表示拦截该事件,说明ViewGroup自己要处理该事件(事件不再往下传递);调用自身的onTouchEvent()处理事件(DOWN事件将不再往上传递给Activity的onTouchEvent());该事件列的其他事件(Move、Up)将直接传递给ViewGroup 的onTouchEvent()。
  2. 若 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度以外的时候是外部滑动。

  1. 外部拦截法
    默认父元素拦截,然后再适合的条件下,不让父元素拦截。
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;
}
  1. 内部拦截法:
    默认刚开始是不允许父元素做拦截,也就是子元素刚开始就调用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;
    }
}

结语

欢迎大家查看纠正,😉。。。。让吐槽来的更猛烈些吧。