• 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);
        }
  • 相关阅读:
    大数加法、乘法实现的简单版本
    hdu 4027 Can you answer these queries?
    zoj 1610 Count the Colors
    2018 徐州赛区网赛 G. Trace
    1495 中国好区间 尺取法
    LA 3938 动态最大连续区间 线段树
    51nod 1275 连续子段的差异
    caioj 1172 poj 2823 单调队列过渡题
    数据结构和算法题
    一个通用分页类
  • 原文地址:https://www.cnblogs.com/lsfv/p/11538321.html
Copyright © 2020-2023  润新知