Android的事件分发

1. Touch事件和绘制事件的异同之处

Touch事件和绘制事件很类似,都是由ViewRoot派发下来的,但是不同之处在绘制事件是由应用中的某个View发起请求,一层一层上传到ViewRoot,再有ViewRoot下发绘制,传递canvas给所有子View让其绘制自身,绘制好后,再通知WMS进行画到屏幕上。而Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递。

他们的处理过程都是自上而下的分发,但是绘制多了一层自下往上的请求。

事件存在消耗,事件的处理方法都会返回一个boolean值,如果该值为true,则本次事件下发将会终止。

2. MotionEvent

2.1 MotionEvent对象的产生

系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。

系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。

这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发。

当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象。收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力。目前的手机一般在20毫秒左右。

MotionEventCompat.getActionMasked()

2.2 MotionEvent对象详解

MotionEvent对象包含了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。

MotionEvent常用的Action分为5种:Down 、Up、Move、Cancel、OutSide

MotionEvent中我们常用的方法就是获取点击的坐标,因为这是与我们操作息息相关的。获取坐标有两种方式:

  • getX和getY用于获取以该View左上角为坐标原点的坐标
  • getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标

2.3 5种Touch事件

  • Down:一次触摸事件的第一个MotionEvent对象,即手指初次接触屏幕。
  • Up:通常为一次触摸事件的最后一个MotionEvent对象,即手指离开屏幕。
  • Move:通常多次发生在一次触摸事件之中。表示触摸点发生了移动,我们通常把手指放到屏幕上,实际也会触发该事件,因为人手总是在轻微抖动的。
  • Cancel:常用于取消某个触摸事件,一般是由程序逻辑来指定该事件,用于取消某次触摸事件。
  • OutSide:当触摸点发生在响应事件的View之外时,传递的事件,通常由程序逻辑来指定。

在上面5种事件中,Down为最重要的事件,因为这是一个触摸事件的起始点,程序的很多逻辑判断,都需要根据该事件做处理,例如分发拦截。一次触摸事件必须要有Down事件,这也是MotionEvent对象中都包含了本次触摸事件的Down事件发生的时间点这个属性。其次是Move和Up,通过这3个事件的逻辑处理,就构建出来滑动,点击,长按,双击等多种效果。

2.4 创建一个MotionEvent对象

public static MotionEvent obtain(
        long downTime,    //当用户最初按下开始一连串的位置事件。这必须得到SystemClock.uptimeMillis()
        long eventTime,   //当这个特定的事件是生成的。这必须得到SystemClock.uptimeMillis()            
        int action,       //该次事件的Action                       
        float x,          //该次事件的x坐标        
        float y,          //该次事件的y坐标         
        float pressure,   //该次事件的压力,通常感觉标准压力,从0-1取值     
        float size,       //点击的区域大小,通常根据特定标准范围从0-1取值     
        int metaState,    //一个修饰性的状态,好像一直都是0          
        float xPrecision, //x坐标的精确度           
        float yPrecision, //y坐标的精确度                   
        int deviceId,     //触屏设备id,如果是0,说明这个事件不是来自物理设备      
        int edgeFlags     //系统默认都是返回0,程序在传递时,可以通过逻辑判断加入方向位置 
)

或者一个更简单的方式:

public static MotionEvent obtain(
            long downTime,
            long eventTime,
            int action,
            float x,
            float y,
            int metaState)

也可以通过一个MotionEvent来创建一个新的

public static MotionEvent obtain(MotionEvent event)

通过以上的方式,我们知道,我们也可以通过代码来构建一个虚假的MotionEvent,并分发下去。

view.dispatchTouchEvent(
            MotionEvent.obtain(SystemClock.uptimeMillis(),
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,100,100,0));

然后通过延迟以此往下派发Move和Up时间,形成一个完整的触摸操作。

3. dispatchTouchEvent触摸事件分发

之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正因为如此,才可以从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。

这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View。而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View。

dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数。

我们首先来看看Activity中的dispatchTouchEvent逻辑:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
        //这是一个空实现的方法,以便子类实现,该方法在Key事件和touch事件的dispatch方法中都被调用,
        // 就是方便用户在事件被传递之前做一下自己的处理。
    }
    //这才是事件真正的分发
    if (getWindow().superDispatchTouchEvent(ev)) {
        //superDispatchTouchEvent是一个抽象方法,但是getWindow()获取的对象实际是FrameWork层的
        // PhoneWindow,该对象实现了这个方法,内部是直接调用DecorView的superDispatchTouchEvent
        // 是直接调用dispatchTouchEvent,这样就传递到子View中了   
        return true;
    }
    //如果上面事件没有被消费掉,那么就调用Activity的onTouchEvent事件。
    return onTouchEvent(ev);
}
//PhoneWindow的superDispatchTouchEvent方法直接调用了mDecor的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
//mDecor即为Activity真正的根View,我们通过setContentView所添加的内容就是添加在该View上,
// 它实际上就是一个FrameLayout
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
}

至此我们已经至少明白了以下几点:

1、我们可以重载Activity的onUserInteraction方法,在Down事件触发传递前,实现我们的一些需求,实际上源码中有很多这样的方法,再某个方法体的第一行提供一个空实现的回调方法,在某个方法的最后一行提供一个空实现的回调方法,以便子类去实现自己的逻辑,例如AsyncTask就有类似的方式。这些技巧都能很好的提高我们代码的扩展性。

2、Activity会间接的调用根View的dispatchTouchEvent,并通过if判断返回值,如果为true,即向上层返回true,也就是调用Activity的dispatchTouchEvent的WMS,即操作系统。

3、如果if判断为false,即根View和根View下的所有子View均为消费掉该事件,那么下面的代码就有执行机会,即Activity的onTouchEvent,并把该方法的返回值作为结果返回给上层。

3.1 View的dispatchTouchEvent

View中的处理相当简单明了,因为不涉及到子View,所以只在自身内部进行分发。首先判断是否设置了触摸监听,并且可以响应事件,就交由监听的onTouch处理。如果上述条件不成立,或者监听的onTouch事件没有消费掉该事件,则交由onTouchEvent进行处理,并把返回结果交给上层。

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        //判断mOnTouchListener是否存在,并且控件可点的情况下,执行onTouch,如果onTouch返回true,就消耗该事件
        return true;
    }
    //如果以上条件都不成立,则把事件交给onTouchEvent来处理
    return onTouchEvent(event);
}

3.2 ViewGroup的dispatchTouchEvent

3.3 Down事件

  • 通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle
  • 根据scroll换算后的坐标找出所接受的子View。有动画的子View将不接受触摸事件。
  • 找到能接受的子View后把event中的坐标转换成子View的坐标
  • 调用子View的dispatchTouchEvent把事件传递给子View。
  • 如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
  • 如果子View没有消费,则继续寻找下一个子View。
  • 如果没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理

接下来我们来研究Vi

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值