android 触摸事件详解(完结)


冲突的原因
    电脑本来设计好了最简单的规则,down事件碰到哪个控件,哪个控件就接收全部事件,是原始的以人为本!
       但人偏偏喜欢打破规则。或者是偷懒,便捷缘故,如scrollView.不需要设计旁边下拉条。人就想往中间拉,管你碰到中间什么控件,我就要滑动的事件。
    为什么电脑端没有这么多冲突?因为电脑端时代,是键盘,和鼠标,人还算守规则,要滑动,用滚动条啊,有了触摸后。拉什么拉,我要流的滑。
    所以出现了截断,截断就像是潘多拉之盒,虽然有了截断的便利,也带来了世界的混乱。
因为事件的处理是从控件树的上层到下层,截断后,上下之间顺序优先独占,虽然对于scorllview,大部分是ok的。
但有时候这又和我们人类感觉的不匹配。为什么下滑里面的不让下滑,因为下滑不知道里面还有下滑啊,下滑必须掌控下滑动作。不行,下滑碰到下滑,要给里面下滑。 等等。
所以才会产生控件间事件该如何分配的问题。
    所以总结
电脑的设计(无冲突,有时候不那么方便):down碰到谁给谁,铁一般的纪律。要滑动,用滚动条。

    人类的感觉(全部截断):0.有时候,手碰到了子控件,还是要把事件给上层,所以就产生了截断,打开了混乱的源头。如scorllview:在子控件上滑动,事件还要归scrollview。
    人类的感觉(外部截断)
1.有时候,上层截断后,需要把上层不需要的动作,分配给下层。 所以需要上层,精确控制自己的截取,放行自己不需要的。

人类的感觉(内部截断)2.有时候,上层截断后,需要把下层需要的动作, 分配给下层。所以需要上层,先放行down, 让下层执行getparent().disallowinterxxxxx. 这样让下层掌握控制权。

我的感觉:优先碰到谁给谁,如果一定要截断,那么只截取自己的。如果子和我要抢同一个事件,那么优先看看是否可以避免这种设计。所以尽量不用getparent().disallowinter.
尽量用外部截断法来分配滑动,因为简洁,容易理解和定位bug,只有一种情况是必须使用内部截断法的。就是内外需要分配的事件是同一个,从业务上无法区分的动作,而且此动作应该给子控件。那么就必须内部截断法。

所以本来,从下往上一个listener.touch就可以工作。一切的起源都是上层想要截断事件。所以才有onintercetp+ontouch. 为了解决截断的特殊情况又出现了disallowflag. click感觉是一个动作语法糖而已。

个人名词修正

滑动冲突,因为修改为滑动分配。这样更容易理解本质。
因为本质上就是如何分配事件。不管和外部截断和内部截断。
截断的目的就是分配。

diapatchEvent:个人感觉应该翻译为下发,而不是分发。
有3个苹果,都给一个小朋友,是下发。给3个才叫分发。很明显,事件最终是一个人处理。只是看看给谁而已。
直译很多情况下,都会发生意思偏差。

触摸设计的推导假设

从直接触碰的控件往上传播所有事件,包括down和move,up。
这样同一个枝的控件都可以知道所有事件。设置一个listener就可以工作。
设置一个字段,isHandle,是否掌控。一但为真,那么就不再往上传。
这个设计很简单,从下往上符合人的感知和经验。
随时可以触发自己的动作。触发了自己的,设置下ishandle.
为什么这么简单的流程不用。要搞的这么复杂?
因为有特例要要上层截断,所以才有截断判断,还可以设置截断条件。
截断又搭配一个ontouch,放在listener.touch之后,比较符合常理。
又想优化下每次事件的传播效率,才有down作为判断消费者的设计。不必要每次都传到最底层。
截断后,又想要特例,所有又有了 disallow.
触摸事件的伪代码
首先<<android 开发艺术探索>>和网上的伪代码是一样的,估计大家都是抄书的,但是个人感觉有非常明显的失误。都是一抄全错。

书上的伪代码,看来是down的伪代码,但是尾递归之后又少一个很重要的,兜底处理。

自己理解的伪代码,分为down和其他事件。因为差别挺大,分为2个部分更容易理解。

down 伪代码

down event
public boolean dispatchTouchEvent(MotionEvent ev) 
{
    boolean consume = false;
    if (onInterceptTouchEvent(ev))
    {
        consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev);
    } 
    else 
    {
        consume = child.dispatchTouchEvent (ev) ;
        if(consume==false)
        {
            consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev);
        }
    }
    return consume;
}

move:伪代码

public boolean dispatchTouchEvent(MotionEvent ev) 
{
    boolean consume = false;
    if (target==null)//没有下发目标,自己处理.  有2种情况  1.最早截断过down. 2.上次截断过move
    {
        consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev);
    } 
    else 
    {
        if (onInterceptTouchEvent(ev))
        {
            ev=cancel;
            consume = child.dispatchTouchEvent (ev) ;
            target=null;
        }
        else
        {
            consume = child.dispatchTouchEvent (ev) ;
        }
    }
    return consume;
}

详细流程图,

分为down事件和非down事件。

down 事件

非down事件

典型事件图


   


分析过程

一。自己的总结。
从大的说,其实就是一个递归。
1.down的目的就是找到谁来处理事件,循环所有子控件,一直往下(dispatchTouchEvent)问(onintercepevent),只要有控件截断,那么之后所有事件的终点站就是它了。
都不处理,那么递归出来时再沿往回问(touchListener + clickListener)。这样,通过down事件找到了谁来处理,
2.那么其他事件就不需要循环所有子控件了,直接走处理链的那一条路
(dispatchTouchEvent),一直到目标,调用它的touchListener + clickListener
中途,有截断的话,那么就把处理者由原处理者更改为截断者。特殊情况down的时候发现没有处理的view,那么交给activity处理。
3.调用touchListener + clickListener,一般说成Listener.Ontouch 和 onTouchEvent. 因为onTouchEvent的基类实现就是调用view.onclick.我们重写onTouchEvent就是覆盖view.onclick
细致点就是先 touchlistener,如果返回flase,再onclickListener.


网上都是任务下派来作为比喻,很好。只不过大部分没有详细点明一些细节。自己详细比喻下。
假如某公司有多级部门。总公司中心处是activity。activity
总公司中心处 不记住任何东西。只派发任务,并处理大家都不处理的任务。而group会存储是否有我的分部门处理这件事。而分部分又会记载分分部分。直到分分分分分记录了某个人。
从总公司中心处,派发任务,当派发了某个任务,这里就比喻为点击了某处。那么就把这个任务给相关部门,此部门,一层一层的下放到最小的部门的某个人。 当然如果是好差事,中间会有截取。
如果下放到某个人,或者被中间某人截取,但是他后来才发现他没有能力处理(也就是某个控件,触摸事件点到它了,但是它没有消费down)。那么就一层一层沿来路往上,看看谁能处理。
这里就比喻 down下发时候的ontouchEvent都返回false的回归逻辑。最终有人处理,或者真的无人处理。这里
ontouchEvent包括我们的click和自定义的ontoucheventlister。down返回了true。那么和截断一样。就确定了处理人。下次会逐层传递到这里为止,也就是还是会从上往下询问是否需要中断,但是不会再像down一样往回问处不处理。因为已经有人处理了。
之后如果有这个任务的后续处理事件,就比喻为move,up事件。 那么还是从总公司中心处,一层一层过来(很多文章都是说交给某人处理,没有强调是从上往下一层一层的),直到交给处理这个任务的人,就不再往下了。
这里就是比喻其他事件,也是
递归进去,并比较是否是当初存储的那个处理的view。是,就停止递归。
当然中间也可以再截取这个任务,然后再一层一层的通知原来处理这个事情的人,这个任务作废了。也就是比喻为中间截取了move或up信号,并一层一层发送cancel事件到down处理者。只发送一次cancel。以后截取的view就成为了新的处理者,截断所有事件。
如果当初是无人处理。那么后续事项,总公司中心处,还是需要先发给大部门,大部知道无法处理。就直接说无法处理。总公司才自己处理。Activity是不存储谁处理事情的,只有group才存储。所以就算没有处理。activity还是要先问下顶级group。

 

最佳实践

1.最方便是只写 listener.
2.如果需要上层覆盖下层。那么最好是只用外部截断法。套用固定套路。
2.1 down,up,cancel 都放行。
2.2 对于move,只截断自己需要的,尽量吧范围缩小。
3.实在是无法区分上下事件,无法区分也就是无法下放下层事件,那么就用内部截断法。也是固定套路。
3.1 ondispatchEvent中。down事件,就告诉上级不要截断事件。
3.2 必要的话,可以放弃通过down事件获得的事件接收权。

4.要注意分辨,onIntercept和listener消费的区别和含义。
4.1 onIntercept的目的是截取我要的动作。获得控制权。 所以一般对于down是要放行,以便让down走到最接近人触摸点的位置,以便符合人的感觉。 而对于move动作,需要就必须截断。以符合人的最早的动作意图就是我的本意的习惯。
4.2 listener+touch的目的是是否消费这个动作。有2种情况,进入此函数。
1.没有任何子空间消费down,那么down会进入此函数问我是否消费。
2.如果截断了事件。那么进入此函数会问我是否消费。 所以listener必须覆盖这2中情况。这2中情况的余集,就是对于情况1对于down的处理。
所以一般listen是必须消费down和up.正常处理move。

固定套路

外部截断法。

public MotionEvent mDownEvent=null;//down 动作。 因为down是不会被截断的。所以不会进入listener+touch。所以最好保存下,给listener+ontouch使用。
private MotionEvent mLastInterceptEvent=null;//最新的move动作。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if(ev.getAction()==MotionEvent.ACTION_DOWN)
{
mDownEvent=MotionEvent.obtain(ev);//必须copy。因为ev是一个被所有事件共同使用的变量,随时会被更新,而不是new。
return false;
}
else if(ev.getAction()==MotionEvent.ACTION_MOVE)
{
boolean res=false;
if(需要)//只截断左右滑动。
{
res=true;
}
mLastInterceptEvent=MotionEvent.obtain(ev);
return res;
}
else if(ev.getAction()==MotionEvent.ACTION_UP)
{
return false;
}
else//cancel 应该只有下级的cancel才会经过这里。如果是自己cancel。是会直接进入listener+ontouch.所以必须放行。
{
return false;
}
}




内部截断法
内部截断法,对于我看来。就是外部截断法的补充。所以内部截断法中的上层的代码包括外部截断法的ontercept.

@Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
if(getParent()!=null && ev.getAction()==MotionEvent.ACTION_DOWN)
{
getParent().requestDisallowInterceptTouchEvent(true);
}
if(getParent()!=null&& ev.getAction()==MotionEvent.ACTION_MOVE && 上层需要)
{
getParent().requestDisallowInterceptTouchEvent(false);
}
return super.dispatchTouchEvent(ev);
}

 

 一个实际例子

内部控件

public class MyHorizontalScrollViewEx extends HorizontalScrollView
{
    public MyHorizontalScrollViewEx(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
        if(getParent()!=null && ev.getAction()==MotionEvent.ACTION_DOWN)
        {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.dispatchTouchEvent(ev);
    }
}

外部控件

public class MyConstrainLayoutEx extends ConstraintLayout
{
    public MotionEvent mDownEvent=null;//down 动作。 因为down是不会被截断的。所以不会进入listener+touch。所以最好保存下,给listener+ontouch使用。
    private MotionEvent mLastInterceptEvent=null;//最新的move动作。

    private PointF mdisInterceptStart=null;
    private PointF mdisInterceptEnd=null;

    public MyConstrainLayoutEx(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    //v1.分配事件。放行click,一旦有move,那么之后就全部要。要注意,up放行。前提是没有触发move,move触发后,表示截断,那么onInterceptTouchEvent是不会再执行的。之后的move和up是会直接给listener+ontouch
    //v2.改动就在于截断move的时候加了一个条件判断。其他基本没动。
    //v3.如果同向,可以提供一个方法,用于告诉group,再那个区域的不要截断。好像这样和内部截断的功效一样,内部也是告诉group。别截断,但是本质是不一样的。内部法是内部从此掌握了所有事件。
    //如果上层还想要。必须内部放行。而我们画蛇添足的加入一个方法让外部调用。本质上还是上层控制主动。好处是耦合低,如果内部法有一个方法,可以让上层重新掌握主动。而不是靠内部来判读,那才算是耦合度合理。
    //但是不可能有,因为外部法,就是由于无法通过已有的方法,分辨出何时该放。何时该收。但是google为什么不多提供一个接口呢,而不是只能用内部这种不完美的方案。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        if(ev.getAction()==MotionEvent.ACTION_DOWN)
        {
            mDownEvent=MotionEvent.obtain(ev);//必须copy。因为ev是一个被所有事件共同使用的变量,随时会被更新,而不是new。
            return false;
        }
        else if(ev.getAction()==MotionEvent.ACTION_MOVE)
        {
            boolean res=false;
            if(mDownEvent!=null && ev!=null)//只截断左右滑动。
            {
                LSTouch.scrollDirection direction=LSTouch.getscrollDirection(mDownEvent, ev);
                if(direction==LSTouch.scrollDirection.LEFT || direction==LSTouch.scrollDirection.RIGHT)
                {
//                    if(ev.getRawX()>=0 && ev.getRawY()>=200)//这里做一个假设,可以提供一个方法,传递某个控件的位置,这样当触摸点在这个位置,那么不能截断。也是可以的。
//                    {
//                        res=false;
//                    }
//                    else
//                    {
//                        res = true;
//                    }
                    res=true;
                }
            }
            mLastInterceptEvent=MotionEvent.obtain(ev);
            return res;
        }
        else if(ev.getAction()==MotionEvent.ACTION_UP)
        {
            return false;
        }
        else//cancel 应该只有下级的cancel才会经过这里。如果是自己cancel。是会直接进入listener+ontouch.所以必须放行。
        {
            return false;
        }
    }

未解决的疑点

1.当有匹配的事件发生,只给下面说你的事件取消了,但是不告诉自己去触发事件? 这样不是浪费了一个事件了不?虽然很多情况下是无关紧要,但是逻辑上还是错误啊。万一下一个事件就是up事件呢?所以截取一定不能截取up?否则不会触发自己的touch事件!!!
解决:en .可以在onintercept,设置一个变量,来告诉事情已经发生了。如果最后一个是up。那么就直接触发动作。不需要touch事件。否则,根据定义好的变量,在touch中直接做动作,后面的事件直接消费就好了,不作为事件是否发生的标志。
2.如果截断后产生了新的事件消费者控件,事件都已经触发了,假设它上层某个控件有个事件,又匹配上了用户的后续动作呢?,又要截断? 那要触发2个动作。不符合人的常识啊。
解决:可以在截断后,设置 disallow为true。这样保证上层不会再截止动作了。只有我们自己一个动作执行者。

补充 activity ,window, dector的处理分析

C:androidsdksourcesandroid-28androidappactivity.java
/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    else
     {
return onTouchEvent(ev);
      }
}
private Window mWindow;
public Window getWindow() {
    return mWindow;
}
mWindow = new PhoneWindow(this, window, activityConfigCallback);


C:androidsdksourcesandroid-28androidviewwindow.java
/**
 * Used by custom windows, such as Dialog, to pass the touch screen event
 * further down the view hierarchy. Application developers should
 * not need to implement or call this.
 *
 */
public abstract boolean superDispatchTouchEvent(MotionEvent event);




C:androidsdksourcesandroid-28comandroidinternalpolicyPhoneWindow.java
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor = (DecorView) preservedWindow.getDecorView();
mDecor = generateDecor(-1);

DecorView就是Window的顶级View,它派生于FrameLayout,而FrameLayout又派生于groupview。所以我们可以最后追到ViewGroup.java
所以最终看ViewGroup.java的dispatchTouchEvent就可以。但是需要配合下面这幅图。其中contentViews是我们的布局xml文件的内容。







C:androidsdksourcesandroid-28comandroidinternalpolicyDecorView.java

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
原文地址:https://www.cnblogs.com/lsfv/p/11538321.html