zoukankan      html  css  js  c++  java
  • 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);
        }
  • 相关阅读:
    在Windows .NET平台下使用Memcached
    Windows下配置使用MemCached
    B/S 网站技术选型
    HttpHandler与HttpModule的用处与区别
    TCP长连接与短连接的区别
    页和区 sql server
    聚集索引和非聚集索引的区别
    MicrosoftSQLServer中的锁模式
    我是如何在SQLServer中处理每天四亿三千万记录的
    datetime模块处理时间
  • 原文地址:https://www.cnblogs.com/lsfv/p/11538321.html
Copyright © 2011-2022 走看看