Android 事件分发

1 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=====0
onTouch: view=======android.support.v7.widget.AppCompatButton   action=====1
onClick: view=====android.support.v7.widget.AppCompatButton

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

onTouch: view=======android.widget.RelativeLayout action=====0
onTouch: view=======android.widget.RelativeLayout action=====1
onClick: 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=====0
onTouch: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()。

1.1 总结:
  • 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和其父布局之间事件的传递。

2 事件分发

2.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=0
MyRelativelayout: onInterceptTouchEvent:action=0
MyButton: dispatchTouchEvent:action=0
MyButton: onTouchEvent:action=0


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

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

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

2.2 父控件拦截事件时,事件的处理流程

当在父布局中的onInterceptTouchEvent()方法中返回true时:

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

打印结果为

MyRelativelayout: dispatchTouchEvent:action=0
MyRelativelayout: onInterceptTouchEvent:action=0
MyRelativelayout: onTouchEvent:action=0


MyRelativelayout: dispatchTouchEvent:action=1
MyRelativelayout: onTouchEvent:action=1
onClick: view=====com.example.practice_click.MyRelativelayout
2.3 两个问题

根据打印结果,首先提出两个问题:
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;
                            }
	
	}
2.4 判断是否拦截

从上面这段代码中的这几句:

 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,即按下时才会去拦截。

2.5 判断事件该有谁处理

在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

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()方法,来判断事件该由谁处理。

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

  • 在父布局中重写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);
	}

4 mFirstTarget

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

4.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就已经很清楚了,欢迎大家留言交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值