手把手教你如何写事件处理的代码

通过事件分发之View事件处理ViewGroup事件分发和处理源码分析这两篇的的理论知识分析,咱们已经大体的了解了事件的分发处理机制,可是这并不表明你就必定能写好事件处理的代码。java

既然咱们有了基本功,那么本文就经过一个案例来逐步分析事件处理的代码如何写,事件冲突如何解决。git

剖析事件分发的过程

为了模拟实际状况,我特地搞了一幅画View各类嵌套的图github

View嵌套图

图中有一个MyViewGroup,它能够左右滑动,本文就用它来说解事件处理的代码如何写。markdown

后面的分析须要你们有前面两篇文章的基础,请务必理解清楚,不然你可能会以为我在讲天书。框架

ACTION_DOWN

因为咱们操做的目标是MyViewGroup,所以我会把手指在MyViewGroup内容区域内按下,至于按在哪里,其实无所谓,甚至在TextView上也行。此时系统会把ACTION_DOWN事件通过Activity传递给ViewGroup0,那么问题来了ViewGroup0会不会截断事件呢?ide

若是ViewGroup0截断了ACTION_DOWN事件,那么它的全部子View在这个事件序列结束前,将没法接收到任何事件,包括ACTION_DOWN事件。MyViewGroup就是ViewGroup0的子View,很显然咱们并不但愿这样的事情发生。若是真的发生从一开始就截断ACTION_DOWN这样的事情,那父View控件的代码写的绝壁有问题。oop

事件序列是由ACTION_DOWN开始,由ACTION_UP或者ACTION_CANCEL结束,而且中间有0个或者多个ACTION_MOVE组成。源码分析

那么有没有截断ACTION_DOWN事件的状况呢?固然有,ViewGroup必须处于一个合理的状态,而且有理由截断ACTION_DOWN事件。例如ViewPager,当手指在屏幕快速划事后,页面还处于滑动状态,此时若是手指再次按下,ViewPager把这个ACTION_DOWN事件当作是中止滑动当前滑动而且从新开始滑动的指示,所以它有理由截断这个ACTION_DOWN事件。post

那么,ViewGroup0在没有任何合理状态,而且尚未任何合理理由的状况下,是毫不会截断ACTION_DOWN事件的,所以它会把这个事件传递给MyViewGroup测试

MyViewGroup很高兴接收到了第一个事件ACTION_DOWN,按照刚才讲的规则,常规状态下,是不截断ACTION_DOWN事件的,可是若是MyViewGroup在滑动状态中,而且手指已经离开屏幕,当再次按下手指的时候,我但愿MyViewGroup截断ACTION_DOWN事件的,所以onInterceptTouchEvent()方法的事件处理的框架代码应该这样写

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 1. 若是处于无状态,默认不截断
                // 2. 若是处于滑动状态,截断事件
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

如今讨论的是事件处理的框架代码如何写,所以没有具体的代码。

你确定觉得ACTION_DOWN事件就这样处理完了是吧,机智的我早已看穿一切

图样图森破

MyViewGroup是须要实现滑动特性的,那么它就必需要能接收到ACTION_MOVE事件。那么ACTION_DOWN事件要如何处理,才能确保这个事情呢?必须知足下面的一个条件

  1. MyViewGroup有一个子View处理了ACTION_DOWN事件。
  2. MyViewGroup本身处理ACTION_DOWN事件。

第一个条件呢,是最理想的状况,由于MyViewGroup在这种状况下,不用处理ACTION_DOWN事件就能够接收到ACTION_MOVE事件。

然而第一个条件,是不可控的,所以咱们要作好最坏的打算,那就是MyViewGroup本身处理ACTION_DOWN。所以,在onTouchEvent()中处理ACTION_DOWN事件要返回true

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                // 本身处理ACTIOND_DOWN,必须返回true
                return true;
        }
        return super.onTouchEvent(event);
    }
复制代码

ACTION_MOVE

前面处理ACTION_DOWN已经确保了ACTION_MOVE能够顺利接收,根据前面列出的2个保证条件,那么接收ACTION_MOVE的状况以下

  1. MyViewGroup有一个子View处理了ACTION_DOWN,那么ACTION_MOVE将会在onInterceptTouchEvent()中被接收。
  2. MyViewGroup本身处理了ACTION_DOWN,那么ACTION_MOVE将会在onTouchEvent()中接收到。

对于第一种状况,其实有个限制条件,那就是子View必须容许MyViewGroup截断事件,不然MyViewGroup将收不到ACTION_MOVE事件。若是出现这种状况,那你得检查子控件的代码了是否写的合理了。

首先讨论第二种状况,若是ACTION_MOVEonTouchEvent()中接收到,那就表明MyViewGroup要本身处理事件来滑动,所以返回true

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:

                // 本身处理ACTION_MOVE,返回true
                return true;
        }
        return super.onTouchEvent(event);
    }
复制代码

如今来继续看第一种状况,ACTION_MOVE在发送给处理了ACTION_DOWN的子View前,须要经过MyViewGrouponInterceptTouchEvent()方法,那么MyViewGroup要不要截断ACTION_MOVE事件呢?其实有不少种状况,咱们来逐一分析可行性。

有人说,既然onInterceptTouchEvent()会一直接收ACTION_MOVE事件,那能够不截断就直接执行滑动。表面上看MyViewGroup实现了滑动,可是在实际中可能遇到问题。假如子View也是一个滑动的控件,那么在MyViewGroup滑动的时候,因为没有截断事件,所以子View同时也会根据本身的意愿去滑动,这岂不是瞎搞吗?又或者说子View在接收ACTION_MOVE事件后,请求父View不容许截断后续的事件,那么MyViewGroup后续就处理不了ACTION_MOVE事件了。

通过上面的分析,有人可能会说,一不作二不休,那就直接截断得了。我只能说,这位施主你太冲动!

反思

若是直接粗暴的截断,万一赶上了不是彻底垂直滑动的手势,MyViewGroup却在水平滑动,那岂不是尴尬了。

这时候,确定有人忍不了了,截断也不是,不截断也不是,你想闹哪样!咱们能够变通下嘛,咱们要有条件的截断,避免刚才的尴尬状况嘛,举两个经常使用的条件

  1. 达到滑动的临界点
  2. 判断手势是水平滑动仍是垂直滑动

那么,在onInterceptTouchEvent()方法中关因而否截断ACTION_MOVE的框架代码能够这样写

public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 达到滑动标准就截断,不然不截断
                // 滑动标准以下
                // 1. 达到滑动的临界距离
                // 2. 判断手势是水平滑动
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

ACTION_UP

咱们先来讨论下,ACTION_UP会在哪里接收到

  1. MyViewGroup处理了ACTION_DOWNACTION_UP将会在onTouchEvent()中接收到。
  2. MyViewGroup在截断ACTION_MOVE以前,ACTION_UP将会在onInterceptTouchEvent()中接收到。
  3. MyViewGroup截断ACTION_MOVE后,ACTION_UP将会在onTouchEvent()中接收到。

第一种状况,返回true吧,由于毕竟是MyViewGroup本身处理了ACTION_UP事件。

第二种状况,返回false吧,由于此时MyViewGroup尚未处理滑动事件呢。

第三种状况,返回true吧,由于毕竟是MyViewGroup本身处理了ACTION_UP事件。

从源码角度看,对于ACTION_UP事件的处理的返回值,好像并不过重要。 可是返回true仍是false实际上是向父View代表一个种态度,那就是我究竟是不是处理了ACTION_UP事件。

ACTION_CANCEL

从前面文章分析可知,ACTION_CANCEL是在MyViewGroup的父View截断了MyViewGroupACTION_MOVE事件后收到的,ACTION_CANCEL接收的地方其实和ACTION_UP是同样,至因而处理仍是不处理,根据实际中有没有作实质的动做来相应的返回true或者false

完成案例代码

前面咱们已经对每一个事件到底处不处理进行了分析,而且写出了事件处理的框架,那么接下来,咱们就能够在这个框架之下,很放心地完成MyViewGroup滑动特性的代码了。

ACTION_DOWN

在处理ACTION_DOWN的时候要作啥呢?固然是记录手指按下时的坐标。因为ACTION_DOWN必定会通过onInterceptTouchEvent(),因此在这里记录按下坐标

public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 记录手指按下的坐标
                mLastX = mStartX = x;
                mLastY = mStartY = y;
                // 1. 若是处于无状态,默认不截断
                // 2. 若是处于滑动状态,截断事件
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

mStartXmStartY表示手指按下的坐标,mLastXmLastY表示最近一次事件的坐标。

ACTION_MOVE

根据前面的分析,处理ACTION_MOVE有状况有以下几种

  1. 若是MyViewGroup存在一个子View处理了ACTION_DOWN

    1. MyViewGroup截断ACTION_MOVE以前,ACTION_MOVE将会在onInterceptTouchEvent()中接收。
    2. MyViewGroup截断ACTION_MOVE以后,ACTION_MOVE将会在onTouchEvent()中接收。
  2. 若是MyViewGroup处理了ACTION_DOWN,那么ACTION_MOVE将会在onTouchEvent()中接收。

第一种状况,根据前面的分析,咱们将在onInterceptTouchEvent()根据条件来截断ACTION_MOVE事件。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 计算从手指按下时滑动的距离
                float distanceX = Math.abs(x - mStartX);
                float distanceY = Math.abs(y - mStartY);
                if (distanceX > mScaledTouchSlop && distanceX > 2 * distanceY) {
                    // 设置拖拽状态
                    setState(SCROLLING_STATE_DRAGGING);
                    // 不容许父View截断后续事件
                    requestDisallowIntercept(true);
                    // 执行一次拖拽的滑动
                    performDrag(x);
                    // 更新最新事件坐标
                    mLastX = x;
                    mLastY = y;
                    // 截断后续的事件
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

根据咱们的分析,要达到截断ACTION_MOVE的标准才截断后续的ACTION_MOVE事件,从代码中能够看出这个标准有两条

  1. 水平滑动的距离要大于一个临界值。
  2. 水平滑动的距离要大于两倍的垂直滑动距离,这样就排除了一些不标准的手势。

当咱们认为这是一次有效的滑动的时候,就要截断后续的ACTION_MOVE事件,这就是代码中看到的return true的缘由。

然而事情尚未完,咱们还作了一些优化动做

第一步,设置拖拽状态。这是由于在截断后续的ACTION_MOVE后,后续的ACTION_MOVE事件就会分发给MyViewGrouponTouchEvent(),而onTouchEvent()也要处理其余状况的拖拽,所以须要这个状态判断值。

第二步,请求父View不容许截断后续ACTION_MOVE事件。由于MyViewGroup立刻要执行以系列的滑动动做,若是父View此时截断了事件那确定是不合适的,所以要通知父View不要搞事情。

第三步,执行一次滑动。可能不少人不理解为什么要在onInterceptTouchEvent()中执行滑动动做,这个方法名义上只是用来判断是否截断事件的。

其实这里是有缘由的,因为要截断后续的ACTION_MOVE事件,那么此次的ACTION_MOVE事件是不会发送到MyViewGrouponTouchEvent()中的,而是把这个ACTION_MOVE事件变为ACTION_CANCEL事件发给处理了ACTION_DOWN事件的子View。所以当前的ACTION_MOVE若是不在onInterceptTouchEvent()处理,那么就会丢失这一次滑动处理。

截断后续的ACTION_MOVE后,MyViewGrouponTouchEvent()会接收后续的ACTION_MOVE,那么在这里要继续执行滑动

public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                if (mState == SCROLLING_STATE_DRAGGING) {
                    // 处于滑动状态就继续执行滑动
                    performDrag(x);
                    mLastX = x;
                }
                return true;
        }
        return super.onTouchEvent(event);
    }
复制代码

至此,处理ACTION_MOVE的第一种状况已经处理完毕,咱们如今来看下第二种状况,那就是MyViewGroup处理了ACTION_DOWN,全部的ACTION_MOVE事件都将交给MyViewGrouponTouchEvent()处理。那么此时MyViewGroup尚未滑动,所以须要再次判断是否达到滑动标准

@Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                if (mState == SCROLLING_STATE_DRAGGING) {
                    // 处于滑动状态就继续执行滑动
                    performDrag(x);
                    // 更新最新坐标点
                    mLastX = x;
                } else {
                    // 不处于滑动状态,就再次检测是否达滑动标准
                    float distanceX = Math.abs(x - mLastX);
                    float distanceY = Math.abs(y - mLastY);
                    if (distanceX > mScaledTouchSlop && distanceX > 2 * distanceY) {
                        setState(SCROLLING_STATE_DRAGGING);
                        requestDisallowIntercept(true);
                        performDrag(x);
                        mLastX = x;
                    }
                }
                return true;
        }
        return super.onTouchEvent(event);
    }
复制代码

ACTION_UP

对于ACTION_UP事件,咱们先来预想下发生的状况

  1. 没有截断ACTION_MOVE事件以前,ACTION_UP事件会先由onInterceptTouchEvent()处理。
  2. 截断ACTION_MOVE事件以后,ACTION_UP事件会由onTouchEvent()处理。
  3. MyViewGroup处理了ACTION_DOWN事件,ACTION_UP事件所有会由onTouchEvent()处理。

第一种状况,因为MyViewGroup尚未产生滑动,所以不须要处理此种状况下手指抬起事件。

第二种状况,MyViewGroup已经产生滑动,若是MyViewGroup是一个像ViewPager同样的页面式的滑动,那么当手指抬起时,它须要进行一些页面定位操做,也就是决定滑动到哪一个页面。

第三种状况,其实就是第一种状况和第二种状况的综合版而已。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_UP:
                if (mState == SCROLLING_STATE_DRAGGING) {
                    setState(SCROLLING_STATE_SETTING);
                    // 使用Scroller进行定位操做
                    int contentWidth = getWidth() - getHorizontalPadding();
                    int scrollX = getScrollX();
                    int targetIndex = (scrollX + contentWidth / 2) / contentWidth;
                    mScroller.startScroll(scrollX, 0, targetIndex * contentWidth - scrollX, 0);
                    invalidate();
                }
                return true;
        }
        return super.onTouchEvent(event);
    }
复制代码

ACTION_CANCEL

ACTION_CANCEL这个事件比较特殊,按照正常流程看,是因为父View截断了MyViewGroupACTION_MOVE事件后,把ACTION_MOVE变为了ACTION_CANCEL,而后发送给MyViewGroup

若是MyViewGroup在进行滑动以前,会先请求父View不容许截断它的事件,也就是说以后父View不可能截断ACTION_MOVE事件,也就是不可能发送ACTION_CANCEL事件。

若是MyViewGroup还没开始滑动,那么MyViewGroup就可能会收到ACTION_CANCEL事件,然而此时不用作任何处理动做,由于MyViewGroup尚未滑动产生状态呢。

这是一种正常状况下的纯理论分析,不排除异常状况。

截断ACTION_DOWN

如今,咱们回过头来处理MyViewGroup截断ACTION_DOWN的状况,前面咱们说过,若是手指抬起,MyViewGroup仍是处于滑动状态,在咱们这个例子中叫作定位状态,那么当手指按下时,就须要截断事件,由于MyViewGroup认为这个时候的按下动做是为了中止当前滑动,并用手指控制滑动

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 重置状态
                setState(SCROLLING_STATE_IDLE);
                // 记录手指按下的坐标
                mLastX = mStartX = x;
                mLastY = mStartY = y;
                // 1. 若是处于无状态,默认不截断
                // 2. 若是处于滑动状态,截断事件
                if (!mScroller.isFinished()) {
                    // 中止定位动做
                    mScroller.abortAnimation();
                    // 设置拖拽状态
                    setState(SCROLLING_STATE_DRAGGING);
                    // 不容许父View截断后续事件
                    requestDisallowIntercept(true);
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

MyViewGroup截断ACTION_DOWN事件后,那么后续的的ACTION_MOVE事件就由onTouchEvent()来进行滑动处理,这个过程在前面已经实现。

结束

本文先从理论上搭建了事件处理的框架,而后用一个简单的例子实现了这个框架。若是你们在看本文的时候有任何疑问,请先参考前面两篇文章的分析,若是仍是有疑问,欢迎在评论里留言讨论。

详细源码请参考github,实现的效果以下

触摸事件

为了测试,我在第一个页面放置了一个Button,而后点击Button开始滑动,能够看到Button并无相应点击事件。而后在第二个页面返回第一个页面时,只有滑动超过了一半的宽度,才会自动滑动到第一页面。

相关文章
相关标签/搜索