Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Android事件分发浅谈

$
0
0
Android事件分发机制浅谈
前言:可能Android的事件分发对于刚学Android的童鞋来说接触得不多,这样不奇怪。因为刚学的时候,一般人很难注意到或是会选择主动去了解。那么究竟什么是Android的事件分发呢?

或许刚说出来,有点觉悟的新手会想到就是那些按钮的点击事件、或是说监听。而这些也确实是Android事件分发的其中一部分。由于Android的事件分发其实是可以有很多变化的,特别是当你需要自定义View的时候,很多情况都需要具体分析,所以大体上它都不容易精确的掌握。但如果是主流的,大概的事件分发机制其实也没那么难理解,说到底这可以说是一个浅入深出的问题吧。那么接下来我们来浅谈一下这次的主题Android事件分发机制。

1.比喻
打个比方,Android的事件分发中的事件(用户的触屏)就像一块饼。假若有一家3代的人在饥荒的年代里,如果爷爷有一块饼,那么他会先给谁?那当然会是孙子。但爷爷年纪大视力不好,所以他把饼传递给了儿子,而儿子又把这块饼传给了孙子,最终由孙子吃下了那块饼。而像这样的父穿子,子传孙的方法,就如同我们Android中的事件分发一样。

接下来我们新建一个工程并写好如上图的布局,这是一个最外层包着RelativeLayout(爷爷),中间是LinearLayout(儿子),最里面是Button(孙子)。


图画得不是很好,大家就凑合这看吧。上面就是我们布局的上个View。而触摸事件(饼)就是通过RelativeLayout--->LinearLayout--->Button,这样一层层的传递的。而事件的分发就是这样通过disppatchTouchEvent(),OnInterceptTouchEvent(),OnTouchEvent()这三个方法去判断事件是否继续向下传递。

而当其中有一层View自己先截获了事件消费掉了(可理解为自己用掉了点击事件),那么事件则不会向下传递,而在OnTouchEvent中返回False则不会自己消费并返回到上一层去处理。

看到这里大家肯定还是不明白,接着到java代码里面,按着 Ctrl+ Shift+ T,查找一下View的API,这里我选API23的,其实API多少都没太大变化,我就选一个最新的。



3.理解dispatchTouchEvent(),onInterceptTouchEvent(),OnTouchEvent()三个事件

这三个事件理解起来,首先要区分View与ViewGroup两种情况:
* View:dispatchTouchEvent,OnTouchEvent
* ViewGroup:dispatchTouchEvent,onInterceptTouchEvent,OnTouchEvent

其中View是没有onInterceptTouchEvent方法的。在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法,所以我们会找View的API,而不是找Button的API,因为这个方法一般在具体的控件类中是找不到的,他是父View的方法。
下面我贴出找到的dispatchTouchEvent源码,看看官方是怎么解析的。

理解dispatchTouchEvent()
/**
     * 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 the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

看上面的官方解析,大概意思是:这个View接收的是从屏幕传递过来的事件并传给目标的View,或是这个View就是从屏幕传达过来的事件的目标。

这句话怎么理解呢?其实也就对应我上面画的图,两种情况:要么该View就是目标的View(自己消费掉事件),要么向下传递事件。而这个方法默认是调用super.dispatchTouchEvent(event)的,需传入super.dispatchTouchEvent(true),需要注意的是他如果直接返回true或flase而不是进过调用supper的都不会向下传递。这里可能有点难理解,不用急,下面会继续解析。

接着我们看看onInterceptTouchEvent()方法。

理解onInterceptTouchEvent()



同样的快捷键方法我们来到ViewGroup的源码:

/**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    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;
    }


这里他的代码不是很多,但官方的注释却又很多,我们只看第一段,大概意思是:这个方法他拦截了所有的屏幕事件。他允许用户监测这些事件,并且可以分发到他的子View里面,作为子View的一个手势(或是说任何点)的手势。

所以从上面我们着重的可以知道,onInterceptTouchEvent()这个方法可以拦截事件的分发。而当onInterceptTouchEvent()返回的是false的时候就说明不拦截这个View的子View,那么子View就可以获取到这个事件了。

OnTouchEvent()方法在View跟ViewGroup都有,所以要分开讨论。我们现在可以理解为,若是OnTouchEvent()返回true则代表这一层的View自己消费掉事件,而返回false,那么事件会重新返回到该View的父View,也就是上一层的View中。




当RelativeLayout在onInterceptTouchEvent()里面不拦截子View的时候,事件就会传递到LinearLayout的dispatchTouchEvent()事件里面。而同样LinearLayout也是继承ViewGroup的,所以他也有onInterceptTouchEvent方法。

而LinearLayout的OnTouchEvent()里面,如果返回true则代表自己消费掉事件,而如果返回false则表示不作处理并返回给上层父View处理

4.结合例子理解

这个是我编写的上图的布局,里面三个都是简单的自定义View,作用是方便打印出信息

<org.dispatchtouchevent002.CustomRelativieLayout 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"
    tools:context="org.heima.dispatchtouchevent002.MainActivity" >
    <org.dispatchtouchevent002.CustomLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
        <org.dispatchtouchevent002.CustomButton
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="click Me" />
    </org.dispatchtouchevent002.CustomLinearLayout>
</org.dispatchtouchevent002.CustomRelativieLayout>

接下来我们在这些View里面重写dispatchTouchEvent()和OnTouchEvent()两个方法,分别在里面打印Log,查看结果。

RelativeLayout
public class CustomRelativieLayout extends RelativeLayout {
	public CustomRelativieLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomRelativieLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomRelativieLayout(Context context) {
		super(context);
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

LinearLayout
public class CustomLinearLayout extends LinearLayout{
	public CustomLinearLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomLinearLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomLinearLayout(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

Button
public class CustomButton extends Button{
	public CustomButton(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomButton(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

1)首先我们来写三个View中的三个方法里的Log日志
RelativeLayout
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}
LinearLayout
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:onInterceptTouchEvent");
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomLinearLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}
Button
@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:dispatchTouchEvent");
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:onTouchEvent");
		return super.onTouchEvent(event);
	}

这里需要注意的是,在Activity中其实也含有onInterceptTouchEvent()和OnTouchEvent()这两个方法,所以我们也需要重写这两个方法。
MainActivity
public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Button btn=(Button) findViewById(R.id.btn);
		btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "MainActivity:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "MainActivity:onTouchEvent");
		return super.onTouchEvent(event);
	}
}

之后我们按下按钮。因为点击事件是由按下和抬起两部分组成的,所以上述的Log日志会打印两次。在Android里面,按下和抬起是分别处理的两个不同的事件,可以看到打印结果如下:
按下
09-27 17:27:41.222: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:onTouchEvent
抬起
09-27 17:27:41.321: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:onTouchEvent
09-27 17:27:41.331: D/TouchEvent(1493): MainActivity:onClick

总结1:
配合我画的图,结合上面的Log日志可以看出,点击事件最终被Button的onClick事件所消费。
MainActivity:
dispatchTouchEvent()为默认,向下传递。
Relative:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
LinearLayout:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
Button:
dispatchTouchEvent()为默认true,向下传递。
OnTouchEvent():为默认false,默认不处理

事件到了Button的OnTouchEvent()里面后,由于默认是false,OnTouchEvent()方法不处理。,又向最上层的父View返回了,

看到这里的Log日志再配合上面的图,是不是应该能稍微理解了一些呢?若是不了解的话,那还得自己多看几遍,或是自己也试试打印Log测试一下了。


2)接着我们来讨论上面理解dispatchTouchEvent()时出现的一种情况:dispatchTouchEvent()返回true或false時不向下传递事件,当只有调用super.dispatchTouchEvent()的时候才会。

在这里我试着修改RelativeLayout里面的dispatchTouchEvent(),其余布局不变
RelativeLayout:dispatchTouchEvent()返回true
 @Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return true;
	}
Log日志:
09-28 01:23:09.349: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.349: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent

RelativeLayout:dispatchTouchEvent()返回false
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return false;
	}
Log日志:
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): CustomRelativieLayout:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:onTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:onTouchEvent

总结2:
当dispatchTouchEvent()返回true或者flase的时候,事件不向下传递,只有返回的是 super.dispatchTouchEvent()才会进行传递。并且返回false的时候事件会返回到上层View的onTouchEvent(),但如果父View的onTouchEvent为默认(默认不处理)。那么最终这个事件会没有响应,不被任何层的View所消费掉。

3)接下来我们调用super.dispatchTouchEvent(),但不把他返回,而是选择返回true或者false,观察情况如何:

RelativeLayout:调用super.dispatchTouchEvent(),返回true:
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return true;
	}
Log日志:

09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:onClick


RelativeLayout:调用super.dispatchTouchEvent(),返回false:
 @Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return false;
	}
Log日志:
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:onTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): Touch:true
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:onTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:onTouchEvent

总结3:
从日志来看,当有调用super.dispatchTouchEvent()并且返回true的情况下,跟我们结论1里,直接返回super.dispatchTouchEvent()的结果是一样的。
但当super.dispatchTouchEvent()并且返回false时,事件走到一半就停止了,看到日志的第8行后ANCTION_DOWN(点下去)已经执行完了,而抬起来的事件只走到Activity里面的dispatchTouchEvent()就再也没有分发下去。

4)接着onInterceptTouchEvent()返回true,则为拦截事件的分发。
RelativeLayout:onInterceptTouchEvent()返回ture:
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		boolean touch = super.onTouchEvent(event);
		Log.d("TouchEvent", "onToucheEvent:"+touch);
		return super.onTouchEvent(event);
	}
Log日志:
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onInterceptTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): onToucheEvent:false
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:onTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:onTouchEvent

结4:
这里,我在RelativeLayout里面的OnTouchEvent()方法打印了super.onTouchEvent的值,可以看到当为false。当onInterceptTouchEvent()为true时,事件不分发给下一层的子View,而选择走自己的onToucheEvent()方法,但又是默认的false不处理。导致事件回到上一层的父View中,最终父View的onTouchEvent()也是默认为false,不处理事件。最后导致事件没有任何View响应,也就没有消费。

5)在Activity里,给Button设置OnTouchListener,在onTouch()中返回false,默认不拦截。
MainActivity:onTouch()返回false:默认不拦截
	btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
		
		btn.setOnTouchListener(new OnTouchListener() {
			
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return false;
			}
		});
RelativeLayout:onInterceptTouchEvent()返回false:不拦截事件
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
 
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
 
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");s
		return super.onTouchEvent(event);
	}
Log日志:
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onClick

总结5:
从上面的结果来看,事件在传递到Button的disptchTouchEvent(),然后回到Activity调用自己的onTouch(),dispatchTouchEvent结束后调用Button自己的onTouchEvent()(第8行),到此按下去的事件已经完成。
紧接着到抬起来的up事件。而执行的Log结果大致上也跟按下去的down事件差不多,但不同的是,up事件最终会在Button自己的OnTouchEvent()中响应,而onTouchEvent()会调用Buttton自己的onClick()方法,最后由onClick方法消费。

6)Button的onTouch()方法返回true
MainActivity:onTouchEvent返回true
btn.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return true;
			}
		});
Log日志:
09-28 04:23:15.712: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.722: D/TouchEvent(2223): MainActivity:onTouch
09-28 04:23:15.882: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.892: D/TouchEvent(2223): MainActivity:onTouch

总结6:
可以看到onTouch()设置为true,事件到最后也没传递到onClick()里面,而是由Button的onTouch()给消费了。所以onTouch()方法应该是先于onClick执行的,事件到了onTouch()(返回true)已经被消费了,那么整个方法都已经返回了,事件就不会进一步传递,自然没有onClcik的事。

到这里我们差不多可以结合源码来解析一下我们锁看到的现象了。

7.dispathTouchEvent()和onTouch()源码理解

找到View中dispathTouchEvent()的源码:
public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

从上面的代码24行开始可以看出dispathToychEvent()若能返回true的话都是要在,第24行或是第38行的判断中返回true,dispathTouchEvent才会为true。在这里其实可以看到前面的第24行的判断正是onTouch()方法的响应。而我们onTouch()方法的返回值就是写在Acivity中的代码,所以当onTouch()返回true的时候,事件在这里就已经消费了,而dispathToychEvent()也马上返回false。

而如果onTouch()返回false则事件会去到View自己的OnTouchEvent()方法里,那么onClick方法是怎么才调用到的呢?接着我们再看源码。

找到View中dispathTouchEvent()的源码:
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_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 (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }
                        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();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }
        return false;
    }

代码很多,我们看到第24行,程序进过一些列的判断后会进入一个 switch()语句判断,而里面的case事件就是我们熟悉的ACTION_DOWN,ACTION_UP,ACTION_MOVE等事件的处理。

而我们的setOnClickListener在哪?Ctrl+F搜索一下,发现下面两段代码:
 public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
其中有一个变量mOnClickListener.于是再搜索:
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;
    }

到这里我们可以知道performClick()方法是在OnTouchEvent()里面的ACTION_UP调用的。所以Android的事件分发都是这样进过一层层的View,再通过每个View中的dispatchTouchEvent()和onTouchEvent()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。

这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~












作者:u011070603 发表于2016/9/28 23:26:40 原文链接
阅读:98 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>