您的位置:首页 > 其它

View工作原理(一)事件传递原理详解

2016-01-13 17:14 435 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。

目录(?)[+]

转载请说明出处:http://blog.csdn.net/ff20081528/article/details/17353869
一、准备知识
1、视图坐标与布局坐标的区别如下图所示:



上图是一个坐标系,这个坐标系是无边无际的。这个无边无际的坐标系即视图坐标。手机屏幕可视范围内的坐标即手机屏幕的布局坐标(坐标原点是屏幕的左上方的(0,0)位置)即A点。屏幕里面的子视图里面可视范围内的坐标即子视图的布局坐标(坐标原点是子视图的左上方的(0,0)位置)即B点。

2、android中布局关系


 

二、例子说明事件分发过程

这里我写了一个布局文件,展示效果如上图。当我点击View1,事件的分发过程是这样的:

1、ViewGroup3的dispatchTouchEvent()方法会被调用。

2、ViewGroup3调用ViewGroup2的dispatchTouchEvent()方法。

3、ViewGroup2调用ViewGroup1的dispatchTouchEvent()方法。

4、ViewGroup1会调用View1的dispatchTouchEvent()方法。

5、View1的dispatchTouchEvent()方法调用自己的onTouchEvent()方法。在onTouchEvent方法中处理点击事件。处理完了后会返回一个true给调用它的dispatchTouchEvent()方法。

6、ViewGroup1的dispatchTouchEvent()方法会返回一个true值给ViewGroup2的dispatchTouchEvent()方法。这样一直将则个true值返回到ViewGroup3的dispatchTouchEvent()方法。ViewGroup3在将这个值返回给调用它的方法。这样一个事件分发过程结束。

转载请说明出处:http://blog.csdn.net/ff20081528/article/details/17353869

 

三、ViewGroup中处理消息的详细过程

通过上面的列子对事件分发的过程有个大概的了解之后,我们通过ViewGroup中的dispatchTouchEvent()方法源码和View中的dispatchTouchEvent()方法及touchEvent方法源码来详细了解android是怎样处理这个过程的。

ViewGroup中的dispatchTouchEvent()方法源码如下:


[java]
view plaincopy





@Override  
    public boolean dispatchTouchEvent(MotionEvent ev) {  
        if (!onFilterTouchEventForSecurity(ev)) {  
            return false;  
        }  
  
        final int action = ev.getAction();  
        final float xf = ev.getX();  
        final float yf = ev.getY();  
        final float scrolledXFloat = xf + mScrollX;  
        final float scrolledYFloat = yf + mScrollY;  
        final Rect frame = mTempRect;  
  
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
  
        if (action == MotionEvent.ACTION_DOWN) {  
            if (mMotionTarget != null) {  
                // this is weird, we got a pen down, but we thought it was  
                // already down!  
                // XXX: We should probably send an ACTION_UP to the current  
                // target.  
                mMotionTarget = null;  
            }  
            // If we're disallowing intercept or if we're allowing and we didn't  
            // intercept  
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
                // reset this event's action (just to protect ourselves)  
                ev.setAction(MotionEvent.ACTION_DOWN);  
                // We know we want to dispatch the event down, find a child  
                // who can handle it, start with the front-most child.  
                final int scrolledXInt = (int) scrolledXFloat;  
                final int scrolledYInt = (int) scrolledYFloat;  
                final View[] children = mChildren;  
                final int count = mChildrenCount;  
  
                for (int i = count - 1; i >= 0; i--) {  
                    final View child = children[i];  
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                            || child.getAnimation() != null) {  
                        child.getHitRect(frame);  
                        if (frame.contains(scrolledXInt, scrolledYInt)) {  
                            // offset the event to the view's coordinate system  
                            final float xc = scrolledXFloat - child.mLeft;  
                            final float yc = scrolledYFloat - child.mTop;  
                            ev.setLocation(xc, yc);  
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                            if (child.dispatchTouchEvent(ev))  {  
                                // Event handled, we have a target now.  
                                mMotionTarget = child;  
                                return true;  
                            }  
                            // The event didn't get handled, try the next view.  
                            // Don't reset the event's location, it's not  
                            // necessary here.  
                        }  
                    }  
                }  
            }  
        }  
  
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                (action == MotionEvent.ACTION_CANCEL);  
  
        if (isUpOrCancel) {  
            // Note, we've already copied the previous state to our local  
            // variable, so this takes effect on the next event  
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
        }  
  
        // The event wasn't an ACTION_DOWN, dispatch it to our target if  
        // we have one.  
        final View target = mMotionTarget;  
        if (target == null) {  
            // We don't have a target, this means we're handling the  
            // event as a regular view.  
            ev.setLocation(xf, yf);  
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
            return super.dispatchTouchEvent(ev);  
        }  
  
        // if have a target, see if we're allowed to and want to intercept its  
        // events  
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
            final float xc = scrolledXFloat - (float) target.mLeft;  
            final float yc = scrolledYFloat - (float) target.mTop;  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            ev.setLocation(xc, yc);  
            if (!target.dispatchTouchEvent(ev)) {  
                // target didn't handle ACTION_CANCEL. not much we can do  
                // but they should have.  
            }  
            // clear the target  
            mMotionTarget = null;  
            // Don't dispatch this event to our own view, because we already  
            // saw it when intercepting; we just want to give the following  
            // event to the normal onTouchEvent().  
            return true;  
        }  
  
        if (isUpOrCancel) {  
            mMotionTarget = null;  
        }  
  
        // finally offset the event to the target's coordinate system and  
        // dispatch the event.  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        ev.setLocation(xc, yc);  
  
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            mMotionTarget = null;  
        }  
  
        return target.dispatchTouchEvent(ev);  
    }  

 

代码说明:

1、(代码2-4行)处理窗口处于模糊显示状态下的消息。所谓的模糊显示是指,应用程序可以设置当前窗口为模糊状态,此时窗口内部的所有视图将显示为模糊效果。这样的目的是为了隐藏窗口中的内容,对于其中的各个视图而言,可以设置改视图的FILTER_TOUCHERS_WHEN_OBSCURED标识,如存在该标识,则意味着用户希望不要处理该消息。

2、(代码6-11行)将ViewGroup的布局坐标转换成视图坐标。

3、(代码16-58行)处理ACTION_DOWN消息,其作用是判断该视图坐标落到了哪个子视图中。首先判断该ViewGroup本身是否被禁止获取TOUCH消息,如果没有禁止,并且回调函数onInterceptTouchEvent中没有消耗该消息,则开始寻找子视图。调用child.getHitRect(frame)方法获取该子视图在父视图中的布局坐标,即ViewGroup将child放在什么位置,这个位置相对于该child来讲是布局坐标,而对于该ViewGroup来讲却是视图坐标,参数frame是执行完毕后的位置输出矩形。得到位置后,就可以调用frame.contain()方法判断该消息位置是否被包含到了该child中,如果包含,并且该child也是一个ViewGroup,则准备递归调用该child的dispatchTouchEvent(),在调用之前,首先需要把坐标重新转换到child的坐标系中。接下来判断该child是否是ViewGroup类。如果是,则诋毁调用ViewGroup的dispatchTouchEvent(),重新从第一步开始执行;如果child不是ViewGroup,而是一个View,则意味着诋毁调用的结束。

4、(代码61-68行)如果是ACTION_UP或者是ACTION_CANCEL消息,则清除mGroupFlags中的FLAG_DISALLOW_INTERCEPT标识,即允许该ViewGroup截获消息。也就是说,常见的情况就是当用户释放手指,下一次按下时,该ViewGroup本身可以重新截获消息,而在按下还没有释放期间,ViewGroup本身是不允许截获消息的。

5、(代码70-82)判断target变量是否为空。空代表了所有子窗口都没有消耗该消息,所以该ViewGroup本事需要处理该消息。然后还原消息的原始位置,将视图坐标重新转换成布局坐标,接着调用super.dispatchTouchEvent(ev),即View类的中的dispatchTouchEvent()方法(该方法会判断是否进行调用touchEvent()方法来处理,到后面的View类的源码中再介绍)。

6、(代码84-121)处理target存在,并且变量disallowIntercept为false,即允许截获,在默认情况下ViewGroup都是允许截获消息的,只有当该ViewGroup的子视图调用父视图的requestDisallowdInterceptTouchEvent()方法时,方可禁止父视图再次截获消息,但每次ACTION_UP消息或者ACTION_CANCEL消息之后,该ViewGroup又会重新截获消息。ViewGroup本身不允许截获消息或者允许截获但是却没有消耗消息,于是调用target.dispatchTouchEvent(ev)方法把该消息继续交给目标视图处理,在调用该方法前需要检查target中是否申明过要取消随后的消息,即mPrivateFlags中包含
CANCEL_NEXT_UP_EVENT,如果是,则把消息action值修改为ACTION_CANCEL,将mMotionTarget变为空,因为target不想处理接下来的消息了,那么就可以认为没有target了。

 

四、View中处理消息的详细过程

dispatchTouchEvent()方法的源码

[java]
view plaincopy





/** 
    * Pass the touch screen motion event down to the target view, or this 
    * view if it is the target. 
    * 
    * @param event The motion event to be dispatched. 
    * @return True if the event was handled by the view, false otherwise. 
    */  
   public boolean dispatchTouchEvent(MotionEvent event) {  
       if (!onFilterTouchEventForSecurity(event)) {  
           return false;  
       }  
  
       if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
               mOnTouchListener.onTouch(this, event)) {  
           return true;  
       }  
       return onTouchEvent(event);  
   }  

处理窗口处于模糊显示状态下的消息。然后回调视图监听着的onTouch()方法,如果监听者消耗了该消息,则直接返回。如果没有则调用View的onTouchEvent()方法。

 

onTouchEvent()方法源码

[java]
view plaincopy





 /** 
     * Implement this method to handle touch screen motion events. 
     * 
     * @param event The motion event. 
     * @return True if the event was handled, false otherwise. 
     */  
    public boolean onTouchEvent(MotionEvent event) {  
        final int viewFlags = mViewFlags;  
  
        if ((viewFlags & ENABLED_MASK) == DISABLED) {  
            // A disabled view that is clickable still consumes the touch  
            // events, it just doesn't respond to them.  
            return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
        }  
  
        if (mTouchDelegate != null) {  
            if (mTouchDelegate.onTouchEvent(event)) {  
                return true;  
            }  
        }  
  
        if (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
            switch (event.getAction()) {  
                case MotionEvent.ACTION_UP:  
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                        // take focus if we don't have it already and we should in  
                        // touch mode.  
                        boolean focusTaken = false;  
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                            focusTaken = requestFocus();  
                        }  
  
                        if (!mHasPerformedLongPress) {  
                            // This is a tap, so remove the longpress check  
                            removeLongPressCallback();  
  
                            // Only perform take click actions if we were in the pressed state  
                            if (!focusTaken) {  
                                // Use a Runnable and post this rather than calling  
                                // performClick directly. This lets other visual state  
                                // of the view update before click actions start.  
                                if (mPerformClick == null) {  
                                    mPerformClick = new PerformClick();  
                                }  
                                if (!post(mPerformClick)) {  
                                    performClick();  
                                }  
                            }  
                        }  
  
                        if (mUnsetPressedState == null) {  
                            mUnsetPressedState = new UnsetPressedState();  
                        }  
  
                        if (prepressed) {  
                            mPrivateFlags |= PRESSED;  
                            refreshDrawableState();  
                            postDelayed(mUnsetPressedState,  
                                    ViewConfiguration.getPressedStateDuration());  
                        } else if (!post(mUnsetPressedState)) {  
                            // If the post failed, unpress right now  
                            mUnsetPressedState.run();  
                        }  
                        removeTapCallback();  
                    }  
                    break;  
  
                case MotionEvent.ACTION_DOWN:  
                    if (mPendingCheckForTap == null) {  
                        mPendingCheckForTap = new CheckForTap();  
                    }  
                    mPrivateFlags |= PREPRESSED;  
                    mHasPerformedLongPress = false;  
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                    break;  
  
                case MotionEvent.ACTION_CANCEL:  
                    mPrivateFlags &= ~PRESSED;  
                    refreshDrawableState();  
                    removeTapCallback();  
                    break;  
  
                case MotionEvent.ACTION_MOVE:  
                    final int x = (int) event.getX();  
                    final int y = (int) event.getY();  
  
                    // Be lenient about moving outside of buttons  
                    int slop = mTouchSlop;  
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                            (y < 0 - slop) || (y >= getHeight() + slop)) {  
                        // Outside button  
                        removeTapCallback();  
                        if ((mPrivateFlags & PRESSED) != 0) {  
                            // Remove any future long press/tap checks  
                            removeLongPressCallback();  
  
                            // Need to switch from pressed to not pressed  
                            mPrivateFlags &= ~PRESSED;  
                            refreshDrawableState();  
                        }  
                    }  
                    break;  
            }  
            return true;  
        }  
  
        return false;  
    }  
  
<p>   </p>  

1、代码10-15)处理该视图是否为disable状态,如果是,什么都不处理,返回true,即消耗该消息。

2、(代码17-21)消息代理处理消息。所谓的消息代理是指,可以给某个View指定一个消息处理代理,当View收到消息时,首先将该消息派发给其代理进行处理。如果代理内部消耗了该消息,则View不需要在进行任何处理;如果代理没有处理,则view继续处理。在这里不用多管,不会影响事件处理的逻辑和结果。

3、(代码22-102)判断该视图是否可以点击,如果不可以点击,则直接返回false,即不处理该消息。否则,真正开始执行触摸消息的默认处理逻辑,该逻辑分别处理了ACTION_DOWN、ACTION_MOVE、ACTION_UP消息.

(26-70)处理ACTION_UP消息,判断该UP消息是否发生在前面所讲的哪一个监测时间段中,并据此进行不同的处理。

(71-78)在ACTION_DOWN消息中,给mPrivateFlags变量添加PRESSED标识,并将变量mHasPerformLongPress设置为false,然后启动tap监测,即发送一个异步延迟消息。

对于触摸消息而言,消息本身没有repeat的属性,一次触摸只有一个ACTION_DOWN消息,按下来就是连续的ACTION_MOVE消息,并最终以处理ACTION_CANCEL消息.

(80-85)ACTION_CANCLE消息处理,这里只需清除PRESSED标识,刷新视图状态,然后再关闭tap监测即可。

(86-105)对ACTION_MOVE消息进行处理。具体逻辑包括,判断是否移动到了视图区域以外,如果是,则删除tap或者longPress的监测,并清除mPrivateFlags中的PRESSED标识,然后调用refreshDrawableState()刷新视图状态,这将导致对背景状态进行重绘。

 

 五、例子程序

通过代码可能完全理解事件的分发过程有点难,下面通过一个例子程序来巩固下事件分发的过程。

例子程序的UI如下:



布局如下:

[java]
view plaincopy





<org.sunday.main.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:orientation="vertical" >  
      
     <org.sunday.main.MyLinearLayout  
        android:id="@+id/ll2"  
        android:layout_width="match_parent"  
        android:layout_height="100dp"  
        android:background="@drawable/ll_selector"  
        android:orientation="vertical" >  
    </org.sunday.main.MyLinearLayout>  
  
    <org.sunday.main.MyLinearLayout  
        android:id="@+id/ll1"  
        android:layout_width="match_parent"  
        android:layout_marginTop="20dp"  
        android:layout_height="100dp"  
        android:background="@drawable/ll_selector"  
        android:orientation="vertical" >  
  
        <org.sunday.main.MyLinearLayout  
            android:layout_width="80dp"  
            android:layout_height="50dp"  
            android:layout_marginTop="20dp"  
            android:clickable="true"  
            android:background="@drawable/ll_selector2"  
            android:orientation="vertical" >  
        </org.sunday.main.MyLinearLayout>  
    </org.sunday.main.MyLinearLayout>  
  
    <org.sunday.main.MyLinearLayout  
        android:id="@+id/ll3"  
        android:layout_width="match_parent"  
        android:layout_height="100dp"  
        android:layout_marginTop="20dp"  
        android:background="@drawable/ll_selector"  
        android:orientation="vertical" >  
  
        <org.sunday.main.MyButton  
            android:layout_width="wrap_content"  
            android:layout_height="60dp"  
            android:layout_margin="20dp"  
            android:background="@drawable/btn_selector"  
            android:text="button1" />  
    </org.sunday.main.MyLinearLayout>  
  
    <org.sunday.main.MyLinearLayout  
        android:id="@+id/ll4"  
        android:layout_width="match_parent"  
        android:layout_height="100dp"  
        android:layout_marginTop="20dp"  
        android:background="@drawable/ll_selector"  
        android:orientation="vertical" >  
  
        <org.sunday.main.MyButtonWithOutEvent  
            android:layout_width="wrap_content"  
            android:layout_height="60dp"  
            android:layout_margin="20dp"  
            android:background="@drawable/btn_selector"  
            android:text="button2" />  
    </org.sunday.main.MyLinearLayout>  
  
</org.sunday.main.MyLinearLayout>  

MyLinearLayout.java

[java]
view plaincopy





package org.sunday.main;  
  
import android.content.Context;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.MotionEvent;  
import android.widget.LinearLayout;  
  
public class MyLinearLayout extends LinearLayout {  
    private final static String TAG = "MyLinearLayout";  
  
    public MyLinearLayout(Context context) {  
        super(context);  
    }  
  
    public MyLinearLayout(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
      
    @Override  
    public boolean dispatchTouchEvent(MotionEvent ev) {  
        Log.e(TAG, "dispatchTouchEvent()----------------" + this.toString());  
        boolean isTrue = super.dispatchTouchEvent(ev);  
        Log.e(TAG, "dispatchTouchEvent()  isTrue = " + isTrue + " --------------- ;"+ this.toString());  
        return isTrue;  
    }  
      
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        Log.e(TAG, "onTouchEvent()----------------" + this.toString());  
        boolean isTrue = super.onTouchEvent(event);  
        Log.e(TAG, "onTouchEvent()  isTrue = " + isTrue+ "-----------------  ;"+ this.toString());  
        return isTrue;  
    }  
      
}  

MyButton.java

[java]
view plaincopy





package org.sunday.main;  
  
import android.content.Context;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.MotionEvent;  
import android.widget.Button;  
  
public class MyButton extends Button {  
    private final static String TAG = "MyButton";  
  
    public MyButton(Context context) {  
        super(context);  
    }  
  
    public MyButton(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    @Override  
    public boolean dispatchTouchEvent(MotionEvent event) {  
        Log.e(TAG, "dispatchTouchEvent()----------------");  
        boolean isTrue = super.dispatchTouchEvent(event);  
        Log.e(TAG, "dispatchTouchEvent  isTrue = " + isTrue);  
        return isTrue;  
    }  
      
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        Log.e(TAG, "onTouchEvent()----------------");  
        boolean isTrue = super.onTouchEvent(event);  
        Log.e(TAG, "onTouchEvent  isTrue = " + isTrue);  
        return isTrue;  
    }  
}  

MyButtonWithOutEvent.java

[java]
view plaincopy





package org.sunday.main;  
  
import android.content.Context;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.MotionEvent;  
import android.widget.Button;  
  
public class MyButtonWithOutEvent extends Button {  
    private final static String TAG = "MyButtonWithOutEvent";  
  
    public MyButtonWithOutEvent(Context context) {  
        super(context);  
    }  
  
    public MyButtonWithOutEvent(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    @Override  
    public boolean dispatchTouchEvent(MotionEvent event) {  
        Log.e(TAG, "dispatchTouchEvent()----------------"+ this.toString());  
//      boolean isTrue = super.dispatchTouchEvent(event);  
//      Log.e(TAG, "isTrue = " + isTrue);  
        return false;  
    }  
      
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        Log.e(TAG, "onTouchEvent()----------------");  
        boolean isTrue = super.onTouchEvent(event);  
        Log.e(TAG, "onTouchEvent  isTrue = " + isTrue);  
        return isTrue;  
    }  
}  

 点击例子一,后台打印的日志如下:



 

解释:例子一的布局中只有两个ViewGroup子类对象,当点击例子一区域,根ViewGroup首先分发touch事件,因为它有child,所以接着会将事件传递到它的孩子ViewGroup。孩子ViewGroup发现自己没有child了,所以它就得自己处理这个touch事件。所以会调用OnTouchEvent()方法来处理这个事件。处理完之后会返回一个true,一直返回给根ViewGroup。

 

点击例子二的绿色区域,后台打印日志如下:



解释:例子二和例子一差不多,只不过多了一个ViewGroup而已。

 

点击例子三得黄色区域,后台打印日志如下:



 解释:例子三相比于例子二只是将最上层的ViewGroup换成了View,原理一样。

 

点击例子四的黄色区域,打印日志如下:



解释:在这个程序中,对于MyLinearLayout和MyButton的dispachTouchEvent()和onTouchEvent()发放的逻辑并没有进行任何的修改。而对于MyButtonWithOutEvent,在dispatchTouchEvent()方法中,直接返回的false(即不进行事件的任何处理),那么事件传递到这里时,它将会把事件返回给调用它的父ViewGroup进行处理,所以这里我们看到的是MyLinearLayout调用了onTouchEvent()。

 

点击例子三得button和点击例子四的button效果图如下:





 例子三中是button响应了这个touch事件,而在例子四中则是LinearLayout响应了这个touch事件。

demo:点击打开链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: