开发文章

深入理解Android事件分发机制

一、事件分发机制介绍

关于Android事件分发,我们主要分ViewGroup和View两个事件处理部分进行介绍,主要研究在处理事件过程中关注最多的三个方法dispatchTouchEventonInterceptTouchEventonTouchEvent,在ViewGroup和View对三个方法的支持如下图所示:

事件种类 ViewGroup View
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

在Android中,当用户触摸界面时系统会把产生一系列的MotionEvent,通过ViewGroup 的dispatchTouchEvent方法开始向下分发事件,在dispatchTouchEvent方法中,会调用onInterceptTouchEvent方法,如果该方法返回true,表明当前控件拦截了该事件,此后事件交由该控件处理并不再调用该控件的onInterceptTouchEvent方法。最后交由该控件的onTouchEvent方法对事件进行处理。如果当前控件在onInterceptTouchEvent方法中返回false,表示不拦截该控件,之后交由其子控件进行判断是否对事件进行拦截处理。可以用如下伪代码来对其进行处理:

复制内容到剪贴板
  1. public boolean dispatchTouchEvent(MotionEvent event) {  
  2.         boolean consume = false;  
  3.         if (onInterceptTouchEvent(event)) {  
  4.             consume = onTouchEvent(event);  
  5.         } else {  
  6.             consume = child.dispatchTouchEvent(event);  
  7.         }  
  8.         return consume;  
  9.     }  

Android事件分发机制.png

先说结论再细分析:

  1. 事件是由其父视图向子视图传递,如图为A->B->C
  2. 如果当前控件需要拦截该事件,则在onInterceptTouchEvent方法中返回true,但真正决定是否处理事件是在onTouchEvent方法中,也就是说如果此时onTouchEvent方法返回了false,则此控件也表示不处理该事件,交由父控件的onTouchEvent方法来判断处理。如图:当事件由A分发至B,B在其onInterceptTouchEvent方法中返回true表示要拦截该事件,此时事件将不会再传给C,但在B的onTouchEvent方法中返回了false,表示不处理该事件,则事件以此向上传递交由A控件的onTouchEvent方法处理。即onInterceptTouchEvent负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent返回true的那个view进行处理。
  3. 一旦控件确定处理该事件,则后续事件序列也会交由该控件处理,同时该控件的onInterceptTouchEvent方法将不再调用。
  4. 由于View没有onInterceptTouchEvent方法,在其dispatchTouchEvent方法中调用onTouchEvent方法处理事件,如果返回false则表示事件不作处理。同时其ACTION_MOVE、ACTION_UP不会得到响应。
  5. View的OnTouchListener优先于onTouchEvent方法执行,如果OnTouchListener方法返回true,那么View的dispatchTouchEvent方法就返回true。而后则onTouchEvent方法得不到执行,同时因为onClick方法在onTouchEvent方法的ACTION_UP中调用,onClick方法也得不到执行。

情况一、A\B\C onInterceptTouchEvent onTouchEvent均返回false

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false false
onTouchEvent false false false

 

 

情况一.png

当A、B、C同时返回false时,事件传递为A(onInterceptTouchEvent) –>B(onInterceptTouchEvent) –>C(onTouchEvent)–>B(onTouchEvent) –>A(onTouchEvent),也就是事件从A传至C时,都没有拦截和处理事件,则事件再次向上传递调用B和A的onTouchEvent方法。

看下打印的结果:

打印的结果.png

情况二、B onInterceptTouchEvent 方法返回true

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false true
onTouchEvent false false false

当BonInterceptTouchEvent返回true时表示拦截了事件,C控件就无法响应该事件。

情况二.png

情况二.png

情况三、B onInterceptTouchEventonTouchEvent方法返回true

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false true
onTouchEvent false true false

当BonInterceptTouchEventonTouchEvent返回true时表示拦截处理了事件,C控件就无法响应该事件,同时事件在B的onTouchEvent之后将不再向上传递,随后事件将不再调用其onInterceptTouchEvent方法。

情况三.png

情况三.png

情况四、C onTouchEvent方法返回true

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false false
onTouchEvent false false true

当ConTouchEvent返回true时表示处理了该事件,之后事件就交由C控件处理,同时事件在C的onTouchEvent之后将不再向上传递。

情况四.png

 

情况四.png

情况五、A onInterceptTouchEvent方法返回true

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent true false
onTouchEvent false false false

当AonInterceptTouchEvent返回true时表示拦截了事件,之后事件就交由A的onTouchEvent方法处理,B、C就无法响应该事件。如果AonTouchEvent方法返回false,其ACTION_MOVE、ACTION_UP事件不会得到响应。

情况五.png

复制内容到剪贴板
  1. @Override  
  2.     public boolean onTouchEvent(MotionEvent event) {  
  3.         Log.e(TAG, "A --- onTouchEvent");  
  4.         switch (event.getAction()){  
  5.             case MotionEvent.ACTION_MOVE:  
  6.                 Log.e(TAG, "A --- onTouchEvent :ACTION_MOVE");  
  7.                 break;  
  8.             case MotionEvent.ACTION_UP:  
  9.                 Log.e(TAG, "A --- onTouchEvent :ACTION_UP");  
  10.                 break;  
  11.         }  
  12.         return false;//super.onTouchEvent(event);  
  13.     }  

返回false.png

二、实现侧滑删除效果

运用上面的知识学习,我们来实现一下简单的侧滑删除效果吧~

实现侧滑删除效果.gif

其核心代码主要在于对事件的拦截和处理上:

复制内容到剪贴板
  1.  @Override  
  2.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3. //        boolean intercepter = false;  
  4.         Log.e("TAG""onInterceptTouchEvent: "+ev.getAction());  
  5.   
  6.         boolean intercepter = false;  
  7.         if (isMoving)  
  8.             intercepter = true;  
  9.         switch (ev.getAction()){  
  10.             case MotionEvent.ACTION_DOWN:  
  11.                 downX = (int) ev.getX();  
  12.                 downY = (int) ev.getY();  
  13.   
  14.                 if (mVelocityTracker == null)  
  15.                     mVelocityTracker = VelocityTracker.obtain();  
  16.   
  17.                 mVelocityTracker.clear();  
  18.                 break;  
  19.             case MotionEvent.ACTION_MOVE:  
  20.   
  21.                 moveX = (int) ev.getX();  
  22.                 moveY = (int) ev.getY();  
  23.   
  24.   
  25.                 Log.e("TAG""getScrollX: "+getScrollX() );  
  26.                 if (Math.abs(moveX - downX) > 0){  
  27.                     intercepter = true;  
  28.   
  29.                     //Log.e("TAG","onInterceptTouchEvent: ");  
  30.                     //scrollBy(moveX - downX,0);  
  31.   
  32.                 }else {  
  33.                     intercepter = false;  
  34.                 }  
  35.   
  36.                 downX = moveX;  
  37.                 downY = moveY;  
  38.                 break;  
  39.             case MotionEvent.ACTION_UP:  
  40.             case MotionEvent.ACTION_CANCEL:  
  41.   
  42.                 intercepter = false;  
  43.   
  44.                 break;  
  45.         }  
  46.   
  47.         //scrollBy(45,0);  
  48.         return intercepter;//  
  49.         //super.onInterceptTouchEvent(ev);  
  50.   
  51.     }  
  52.     @Override  
  53.     public boolean onTouchEvent(MotionEvent ev) {  
  54.   
  55.         Log.e("TAG""onTouchEvent: "+ev.getAction() );  
  56.   
  57.         mVelocityTracker.addMovement(ev);  
  58.         switch (ev.getAction()){  
  59.   
  60.             case MotionEvent.ACTION_MOVE:  
  61.   
  62.                 moveX = (int) ev.getX();  
  63.                 moveY = (int) ev.getY();  
  64.   
  65.                 mVelocityTracker.computeCurrentVelocity(1000);  
  66.                 Log.e("TAG""getScrollX: "+getScrollX() );  
  67.   
  68.                 if (getScrollX()+downX - moveX>=0 && getScrollX()+downX - moveX <= view1.getMeasuredWidth()){  
  69.   
  70.                     scrollBy(downX - moveX,0);  
  71.                  }  
  72.   
  73.                 isMoving = true;  
  74.                 downX = moveX;  
  75.                 downY = moveY;  
  76.                 break;  
  77.             case MotionEvent.ACTION_UP:  
  78.             case MotionEvent.ACTION_CANCEL:  
  79.   
  80.                 Log.e("TAG1""getXVelocity: "+mVelocityTracker.getXVelocity() );  
  81.                 Log.e("TAG1""getYVelocity: "+mVelocityTracker.getYVelocity() );  
  82.                 //  
  83.                 if (getScrollX()>=view1.getMeasuredWidth()/2 || mVelocityTracker.getXVelocity() < -ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity()){  
  84.                     //scrollTo(view1.getMeasuredWidth(),0);  
  85.                     open();  
  86.                 }else {  
  87.                     //scrollTo(0,0);  
  88.                    close();  
  89.                 }  
  90.   
  91.                 mVelocityTracker.clear();  
  92.                 mVelocityTracker.recycle();  
  93.                 mVelocityTracker = null;  
  94.                 break;  
  95.         }  
  96.         return true;//super.onTouchEvent(ev);  
  97.     }  

这里整个父布局继承自ViewGroup,在onMeasure中测量子控件大小:

复制内容到剪贴板
  1. @Override  
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.   
  4.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  5.         measureChildren(widthMeasureSpec, heightMeasureSpec);  
  6.         setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());  
  7.     }  

onFinishInflate方法中获取各个子控件:

复制内容到剪贴板
  1. @Override  
  2.     protected void onFinishInflate() {  
  3.         super.onFinishInflate();  
  4.          view = getChildAt(0);  
  5.          view1 = getChildAt(1);  
  6.         if (mScroller == null)  
  7.             mScroller = new Scroller(getContext());  
  8.   
  9.         view.setOnTouchListener(new OnTouchListener() {  
  10.             @Override  
  11.             public boolean onTouch(View mViewm, MotionEvent mMotionEventm) {  
  12.                 if (mMotionEventm.getAction() == MotionEvent.ACTION_UP  
  13.                         && isOpen){  
  14.                     close();  
  15.                 }  
  16.                 if (mMotionEventm.getAction() == MotionEvent.ACTION_DOWN){  
  17.                     if (mOnChangeMenuListener!=null){  
  18.                         mOnChangeMenuListener.onStartTouch();  
  19.                     }  
  20.                 }  
  21.                 return false;  
  22.             }  
  23.         });  
  24.     }  

并在onLayout方法中布局子控件:

复制内容到剪贴板
  1. @Override  
  2.     protected void onLayout(boolean mBm, int mIm, int mIm1, int mIm2, int mIm3) {  
  3.         if (getChildCount()!=2){  
  4.             throw new IllegalArgumentException("必须包含两个子控件");  
  5.         }  
  6.         Log.e("TAG""onLayout:getWidth "+view.getWidth() );  
  7.             view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());  
  8.             view1.layout(view.getMeasuredWidth(),0,view.getMeasuredWidth()+view1.getMeasuredWidth(),view1.getMeasuredHeight());  
  9.   
  10.     }  

重点在对onInterceptTouchEventonTouchEvent方法的处理,我们在onInterceptTouchEvent中处理是否拦截该事件。如果手指是向左滑动,则表示用户在进行侧滑删除操作,则拦截该事件,需要注意的是,一旦拦截了该事件,之后事件将不调用该控件的onInterceptTouchEvent方法,所以我们将具体的处理逻辑放在onTouchEvent方法中,该方法返回true表示处理该事件,此后事件都由dispatchTouchEvent方法交由onTouchEvent方法处理。在onTouchEvent方法中调用scrollBy方法实现控件左右滑动,从而实现类似侧滑删除效果。

复制内容到剪贴板
  1. @Override  
  2.     public void computeScroll() {  
  3.         if (mScroller.computeScrollOffset()){  
  4.             scrollTo(mScroller.getCurrX(),mScroller.getCurrY());  
  5.             invalidate();  
  6.         }else {  
  7.             isMoving = false;  
  8.         }  
  9.     }  

为使滑动效果更自然,用Scroller在手指抬起的时候控制控件打开或者闭合,Scroller的使用也很简单,抬起时调用其startScroll方法并刷新界面,在控件computeScroll方法中判断是否滑动完毕并刷新界面,在invalidate方法中会调用computeScroll从而直到滑动结束。

好了,总的实现就这么多,希望可以加深对事件分发机制的理解~

Demo下载地址

http://download.csdn.net/detail/zhangke3016/9752543

感谢 zhangke3016 支持 磐实编程网 原文地址:
blog.csdn.net/zhangke3016/article/details/54959569

文章信息

发布时间:2017-02-11

作者:zhangke3016

发布者:aquwcw

浏览次数: