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