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就已经很清楚了,欢迎大家留言交流。
- android事件分发
- android 事件分发
- android事件分发
- android事件分发机制
- Android 事件分发
- Android事件分发机制
- Android 事件分发机制
- Android事件分发机制
- android事件分发
- Android事件的分发
- Android 事件分发机制
- android事件的分发
- Android 事件分发机制
- android 事件分发机制
- Android事件分发机制
- android 事件分发机制
- android事件分发机制
- Android 事件分发
- iOS 出现内存泄漏的几种原因
- 使用import关键字导入静态成员
- [leetcode] 560. Subarray Sum Equals K
- python项目练习五:虚拟茶话会
- easyrms 接口文档
- Android 事件分发
- caffe入门 从vgg16结构学习caffe
- Nginx使用brotli代替gzip
- JENKINS 项目集成部署
- Android 6权限
- 堆和栈的区别(内存和数据结构)(转)
- Nexus3.x.x上传第三方jar
- Android ScrollView中的ListView不能滑动
- 测试某个tty设备是否可用