Android 事件分发

来源:互联网 发布:淘宝动漫壁纸店铺推荐 编辑:程序博客网 时间:2024/06/10 21:17

onClick()和onTouch()方法的关系

首先来看一个示例:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/relativelayout"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.example.practice_09_click.MainActivity">    <Button        android:id="@+id/button"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="button"        android:layout_alignParentTop="true"        android:layout_centerHorizontal="true"        android:layout_marginTop="180dp"></Button></RelativeLayout>
 @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        relativeLayout = (RelativeLayout) findViewById(R.id.relativelayout);        button = (Button)findViewById(R.id.button);        relativeLayout.setOnTouchListener(this);        button.setOnTouchListener(this);        relativeLayout.setOnClickListener(this);        button.setOnClickListener(this);    }    @Override    public boolean onTouch(View view, MotionEvent motionEvent) {        Log.i("onTouch","view======="+view+"action====="+motionEvent.getAction());        return false;    }    @Override    public void onClick(View view) {        Log.i("onClick","view====="+view);    }

这段代码很简单,就是让Button和Button的父布局分别实现onTouch()和onClick()方法。
当点击Button时,

onTouch: view=======android.support.v7.widget.AppCompatButton   action=====0onTouch: view=======android.support.v7.widget.AppCompatButton   action=====1onClick: view=====android.support.v7.widget.AppCompatButton

可以看出先执行的是Button中的onTouch()方法,action==0表示按下,action==1表示抬起。再执行onClick()方法。
当点击Button的父布局时,

onTouch: view=======android.widget.RelativeLayout action=====0onTouch: view=======android.widget.RelativeLayout action=====1onClick: view=====android.widget.RelativeLayout

和之前的Button一样,都是先执行onTouch()方法,再执行onClick()方法。

当在onTouch()方法的返回值改为true时,

@Override    public boolean onTouch(View view, MotionEvent motionEvent) {        Log.i("onTouch","view======="+view+"action====="+motionEvent.getAction());        return true;    }

再次执行,结果为:

onTouch:view=======android.support.v7.widget.AppCompatButton action=====0onTouch:view=======android.support.v7.widget.AppCompatButton action=====1

为什么button并没有回调onClick()方法呢?这里我先剧透一下,来看onTouch的源码:

 /**     * Interface definition for a callback to be invoked when a touch event is     * dispatched to this view. The callback will be invoked before the touch     * event is given to the view.     */    public interface OnTouchListener {        /**         * Called when a touch event is dispatched to a view. This allows listeners to         * get a chance to respond before the target view.         *         * @param v The view the touch event has been dispatched to.         * @param event The MotionEvent object containing full information about         *        the event.         * @return True if the listener has consumed the event, false otherwise.         */        boolean onTouch(View v, MotionEvent event);    }

注释好TM多,代码就两行。呵呵。那么这个接口是什么意思呢?其实就是一个回调。当一个事件分发到当前的View后,将会回调onTouch()方法。那么为什么写成了true以后RelativeLayout里面的Button就不会再响应onTouch()方法了呢?我们在上面的注释中看到了这一句

@return True if the listener has consumed the event, false otherwise.

也就是说返回true表示事件被消耗了,比如你吃了一口面包,面包被你消耗了,那别人还怎么吃?同样的道理。
概括了来说onTouch()方法表示事件是否被消费。

首先来看dispatchTouchEvent()方法。

  public boolean dispatchTouchEvent(MotionEvent event) {   if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }   if (!result && onTouchEvent(event)) {                result = true;            }    }

这段代码中的这一句

li.mOnTouchListener.onTouch(this, event))

首先会根据onTouch()方法来判断,当onTouch()方法返回true时,result=true。如果result=true那么下面这段代码

 if (!result && onTouchEvent(event)) {                result = true;            }

!result将会不满足,onTouchEvent方法将不会执行。

当onTouch()方法返回false时,此时会调用onTouchEvent()方法。在onTouchEvent()方法中

 public boolean onTouchEvent(MotionEvent event) {  switch (action) {                //onClick只有在ACTION_UP中才会被触发                case MotionEvent.ACTION_UP: if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                            // 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();                                }                            }                        } }

调用了performClick()方法,在performClick方法中

public boolean performClick() {        final boolean result;        final ListenerInfo li = mListenerInfo;        if (li != null && li.mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(this);            result = true;        } else {            result = false;        }        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        return result;    }

可以看出其中这一句

  li.mOnClickListener.onClick(this);

到这里才回真正调用onClick()方法。由此可见,onTouch()的返回值true或者false将会决定是否会调用onTouchEvent()方法。而onTouchEvent()方法最终会调用onClick()方法。

强调一点,就是这个onTouch()方法是OnTouchListener接口中的方法,并不是view中的方法。当给view设置setOnTouchListener时,会回调此方法。此方法的返回值将会影响到事件是否传递到onTouchEvent()。

总结:

  • onClick()方法被调用的流程是:dispatchTouchEvent()——>onTouch() 如果此方法返回false——>onTouchEvent()——>performClick() 如果点击事件是ACTION_UP——>onClick()

  • onTouch方法是OnTouchListener接口中的抽象方法,在onTouch()方法中,如果返回true,将不会再调用onTouEvent.因此onClick()方法也不会调用。

  • onTouchEvent()中的ACTION_UP会触发onClick()事件,所以触发onClick()事件要满足两点,一是在onTouch()方法中返回false。二是event为ACTION_UP。也就是当手抬起屏幕的时候。

上面的示例中,只是说明了onTouch()和onClick()方法之间的关系。并没有涉及到Button和Button的父布局的关系。接下来将会说明view和其父布局之间事件的传递。

事件分发

1 父控件不拦截事件时,事件的处理流程
首先把Button和其父布局RelativeLayout改为自定义的控件

public class MyRelativelayout extends RelativeLayout {    public MyRelativelayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        Log.i("MyRelativelayout","dispatchTouchEvent"+ev.getAction());        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.i("MyRelativelayout","onInterceptTouchEvent"+ev.getAction());        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.i("MyRelativelayout","onTouchEvent"+event.getAction());        return super.onTouchEvent(event);    }}
public class MyButton extends Button {    public MyButton(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        Log.i("MyButton","dispatchTouchEvent"+event.getAction());        return super.dispatchTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.i("MyButton","onTouchEvent"+event.getAction());        return super.onTouchEvent(event);    }}

之前的activity中的代码没变。

 @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        relativeLayout = (RelativeLayout) findViewById(R.id.relativelayout);        button = (Button)findViewById(R.id.button);        relativeLayout.setOnTouchListener(this);        button.setOnTouchListener(this);        relativeLayout.setOnClickListener(this);        button.setOnClickListener(this);    }    @Override    public boolean onTouch(View view, MotionEvent motionEvent) {        //Log.i("onTouch","view======="+view+"action====="+motionEvent.getAction());        return false;    }    @Override    public void onClick(View view) {        Log.i("onClick","view====="+view);    }

当点击Button按钮时,打印结果为:

MyRelativelayout: dispatchTouchEvent:action=0MyRelativelayout: onInterceptTouchEvent:action=0MyButton: dispatchTouchEvent:action=0MyButton: onTouchEvent:action=0MyRelativelayout: dispatchTouchEvent:action=1MyRelativelayout: onInterceptTouchEvent:action=1MyButton: dispatchTouchEvent:action=1MyButton: onTouchEvent:action=1onClick: view=====com.example.practice_click.MyButton

从打印结果可以看出,当action=0,即按下时,事件的传递过程为:
(父布局)dispatchTouchEvent()—>(父布局)onInterceptTouchEvent()—>(子控件)dispatchTouchEvent()—>(子控件)onTouchEvent()。

当action==1,即抬起时,事件的传递过程和上述过程一样,只是最后会多调用onClick()方法。
(子控件)onTouchEvent()—>(子控件)onClick()。

2 父控件拦截事件时,事件的处理流程
当在父布局中的onInterceptTouchEvent()方法中返回true时:

  @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.i("MyRelativelayout","onInterceptTouchEvent:"+"action="+ev.getAction());        return true;    }

打印结果为

MyRelativelayout: dispatchTouchEvent:action=0MyRelativelayout: onInterceptTouchEvent:action=0MyRelativelayout: onTouchEvent:action=0MyRelativelayout: dispatchTouchEvent:action=1MyRelativelayout: onTouchEvent:action=1onClick: view=====com.example.practice_click.MyRelativelayout

根据打印结果,首先提出两个问题:
1 为什么在onInterceptTouchEvent()方法中返回true,即拦截时,事件没有在传递到子View?
2 为什么当action==1,即抬起时,没有再去调用onInterceptTouchEvent()方法?

下面来看父控件拦截事件后,事件的处理流程,下面是一段截取代码:

  public boolean dispatchTouchEvent(MotionEvent ev) {    //1 先要判断事件是否要拦截    final boolean intercepted;            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {                    //调用onInterceptTouchEvent(),判断是否要拦截事件                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action);  it was changed                } else {                    intercepted = false;                }            } else {                         intercepted = true;            }    //2 在判断事件该有谁来处理    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }    }

判断是否拦截
从上面这段代码中的这几句:

 if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); // restore action in case it was changed                } else {                    intercepted = false;                }            }

可以得出,只有当按下时,才会调用onInterceptTouchEvent()方法判断是否要拦截事件。所以一个事件只能又一个控件来处理。

在onInterceptTouchEvent()方法中

public boolean onInterceptTouchEvent(MotionEvent ev) {        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)                && ev.getAction() == MotionEvent.ACTION_DOWN                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)                && isOnScrollbarThumb(ev.getX(), ev.getY())) {            return true;        }        return false;    }

默认是返回false,即不拦截事件。并且只有当ACTION_DOWN,即按下时才会去拦截。

判断事件该有谁处理
在dispatchTransformedTouchEvent()方法中,有这样一段代码:

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,            View child, int desiredPointerIdBits) {  if (child == null) {            //1 child为空 直接调用父类的dispatchTouchEvent()。即事件由自己处理            handled = super.dispatchTouchEvent(transformedEvent);        } else {            final float offsetX = mScrollX - child.mLeft;            final float offsetY = mScrollY - child.mTop;            transformedEvent.offsetLocation(offsetX, offsetY);            if (! child.hasIdentityMatrix()) {                transformedEvent.transform(child.getInverseMatrix());            }            //2 当child不为空,将会直接调用child的dispathTouchEvent()方法,即事件由子控件处理。            handled = child.dispatchTouchEvent(transformedEvent);        }        }

下面再来看之前提过的两个问题:
1 为什么在onInterceptTouchEvent()方法中返回true,即拦截时,事件没有在传递到子View?
当重写onInterceptTouchEvent()方法并返回true时:
在dispatchTouchEvent()中:

 if (!disallowIntercept) {                    //1此时的intercepted值为true。                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action);                 }

在dispatchTransformedTouchEvent()中:

final boolean cancelChild = resetCancelNextUpFlag(target.child)                                || intercepted;                        //2 因为intercepted值为true,所以cancelChild也为true                        if (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = true;                        }
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {            event.setAction(MotionEvent.ACTION_CANCEL);            if (child == null) {                //3 拦截事件后,自己处理                handled = super.dispatchTouchEvent(event);            } else {                handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            return handled;        }

由于cancel=true,所以当child==null时将会直接调用dispatchTouchEvent()方法直接去处理此事件。我这里分析的不好,因为cancel=true.表示拦截,所以即使child不为空也不能将事件交给child去处理。但是主要的思路已经明确,就是onInterceptTouchEvent()返回true方法会拦截事件。

2 为什么当action==1,即抬起时,没有再去调用onInterceptTouchEvent()方法?

 if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); // restore action in case it was changed                } else {                    intercepted = false;                }            }

从上面这段代码中可以看出,只有当ACTION_DOWN时,才会调用onInterceptTouchEvent()方法。

综上所述,dispatchTouchEvent()方法是通过onInterceptTouchEvent()方法来判断是不是要拦截事件,在通过dispatchTransformedTouchEvent()方法,来判断事件该由谁处理。

不拦截子控件事件的二种方法

  • 在父布局中重写onInterceptTouchEvent()方法,并返回false。
@Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        return false;    }
  • 在父布局中调用requestDisallowInterceptTouchEvent()方法,并 传入true。
@Override    public boolean dispatchTouchEvent(MotionEvent ev) {        // 不要拦截  一定要在super之前调用        requestDisallowInterceptTouchEvent(true);        return super.dispatchTouchEvent(ev);    }

mFirstTarget
在dispatchTouchEvent()方法中,可以看到有这样一个变量:mFirstTarget.可以说这个变量贯穿整个dispathTouchEvent()方法,所以理解这个变量对分析事件分发也至关重要。介绍这个变量主要分两步:

1 mFirstTarget的作用。
首先来概括下mFirstTarget的作用,通过此变量的作用再来介绍此变量会容易理解很多。
先提出一个问题,前面已经介绍过,一个事件是由顶级的ViewGroup最先拿到事件,然后此事件一级一级的向下传递直到传到消耗事件的view。那么对于一个事件来说,down以后的后续事件是会由同一个消耗事件的view来处理的,那么这个view怎么确定呢?每个事件的后续事件都是一级一级的去找view吗?其实这个问题就已经说明了mFirstTarget的作用。mFirstTarge就是用来存储已经拿到事件的view,当一个事件的后续事件被触发时,会直接根据mFirstTarget来找到这个view。这样可以直达病灶,快速高效。

2 mFirstTarget的执行流程。
mFirstTarget是TouchTarget对象,先来看此对象的结构。在此对象中其中一个变量是View。

public View child;

此child表示触发事件的view。当一个事件传递到child时,那么mFirstTarget将会持有此child。

再来看next变量,

 public TouchTarget next;

当一个事件由ViewGroup1—>ViewGroup2—>child时,那么mFirstTarget将会以链式结构来存储这些View。如:
mFirstTarget(持有child).next(持有ViewGroup2).next(持有ViewGroup1)
而mFirstTarget持有的child始终是最顶端。从下面这个方法中可以看出

  private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);        target.next = mFirstTouchTarget;        mFirstTouchTarget = target;        return target;    }

mFirst主要出现在dispatchTouchEvent()方法中,我这里把关于mFirstTarget的所有相关代码贴出来。

 @Override    public boolean dispatchTouchEvent(MotionEvent ev) {            if (actionMasked == MotionEvent.ACTION_DOWN) {                //1 每个按下的事件都会作为一个新事件,                //新事件就要清空所有的之前的事件。此方法会把mFirstTarget置空                cancelAndClearTouchTargets(ev);                resetTouchState();            }        //当有任何的后续事件时:             if (actionMasked == MotionEvent.ACTION_DOWN                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //首先遍历所有的子child                     for (int i = childrenCount - 1; i >= 0; i--) {     final int childIndex=getAndVerifyPreorderedIndex(childrenCount, i, customOrder);      final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex);        //找到获取焦点的view            if (childWithAccessibilityFocus != null) {                                if (childWithAccessibilityFocus != child) {                                    continue;                                }                                childWithAccessibilityFocus = null;                                i = childrenCount - 1;                            }        //找到获取焦点的view以后,通过dispathTransformedTouchEvent来让此view去处理事件        //当事件被消耗时,返回true。        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();       //2 如果已经消耗了事件,就把此消耗事件的view赋给mFirstTarget                                                         newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;  //3 如果mFirstTouchTarget为空 说明没有消耗此事件,  //从调用dispathTransformedTouchEvent()的第三个参数传null,这个参数表示用来处理此事件的view。可以说明 由viewGroup自己来处理此事件。                         }  if (mFirstTouchTarget == null) {                // No touch targets so treat this as an ordinary view.                handled = dispatchTransformedTouchEvent(ev, canceled, null,                        TouchTarget.ALL_POINTER_IDS);            } else {                      TouchTarget predecessor = null;                //把mFirstTouchTarget赋给target                TouchTarget target = mFirstTouchTarget;                while (target != null) {                    //这个next最后再看,这个是当mFirstTouchTarget持有的view没有消耗事件时,再去向上找mFirstTouchTarget的父ViewGroup,然后把事件分给此ViewGroup来处理。                    final TouchTarget next = target.next;                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                        handled = true;                    } else {                        final boolean cancelChild = resetCancelNextUpFlag(target.child)                                || intercepted;                        //4 看到了吗?哈哈哈!当mFirstTouchTarget不为空时,直接把事件交给由mFirstTouchTarget持有的view来处理。                                if (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = true;                        }                        if (cancelChild) {                            if (predecessor == null) {                                mFirstTouchTarget = next;                            } else {                                predecessor.next = next;                            }                            target.recycle();                            target = next;                            continue;                        }                    }                    predecessor = target;                    target = next;                }            }   }}

关于解释都在注释中了,到这里mFirstTarget就已经很清楚了,欢迎大家留言交流。

0 0