概述
本篇主要分析的是touch事件的分发机制,网上关于这个知识点的分析文章非常多。但是还是想通过结合自身的总结,来加深自己的理解。对于事件分发机制,我将使用两篇文章对其进行分析,一篇是针对View的事件分发机制解析,一篇是针对ViewGroup的事件分发机制解析。本片是对View的事件分发机制进行解析,主要采用案例结合源码的方式来进行分析。
前言
在分析事件分发机制之前,我们先来学习一下基本的知识点,以便后面的理解。
View中有两个关键方法参与到Touch事件分发
dispatchTouchEvent(MotionEvent event) 和 onTouchEvent(MotionEvent event)
所有Touch事件类型都被封装在对象MotionEvent中,包括ACTION_DOWN,ACTION_MOVE,ACTION_UP等等。
每个执行动作必须执行完一个完整的流程,再继续进行下一个动作。比如:ACTION_DOWN事件发生时,必须等这个事件的分发流程执行完(包括该事件被提前消费),才会继续执行ACTION_MOVE或者ACTION_UP的事件。
案例分析
为了能够清楚的监视事件的分发过程,我们采用自定义View的形式,查看内部的方法执行过程。
上代码:
package com.yuminfeng.touch;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("yumf", "MyButton=====dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.i("yumf", "MyButton=====dispatchTouchEvent ACTION_UP");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("yumf", "MyButton=====onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.i("yumf", "MyButton=====onTouchEvent ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
在XML布局中引用该控件,非常简单。
<RelativeLayout 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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.yuminfeng.myviewpager.FirstActivity" >
<com.yuminfeng.touch.MyButton
android:id="@+id/mybutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
</RelativeLayout>
以上代码都非常简单,没有什么逻辑,就是重写Button的dispatchTouchEvent和onTouchEvent的方法,然后引用该控件即可。
然后执行代码,查看日志打印,如下:
由此看到,当点击控件时,首先执行的是dispatchTouchEvent方法,然后再执行onTouchEvent的方法。
如果此时我们修改dispatchTouchEvent的返回值为true时(默认为false),那么onTouchEvent方法便不再执行,如下:
流程示意图如下:
接着我们恢复之前的返回值false,继续让mybutton设置一个setOnTouchListener监听事件,关键代码如下:
mybutton = (Button) findViewById(R.id.mybutton);
mybutton.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("yumf", "Activity=====onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.i("yumf", "Activity=====onTouch ACTION_UP");
break;
}
return false;
}
});
执行后,日志打印如下:
由此我们可以看到,首先执行方法dispatchTouchEvent,然后再执行OnTouchListener中onTouch方法,最后执行onTouchEvent方法。
同上,如果我们继续修改dispatchTouchEvent的返回值为true时,那么后面的方法onTouch,onTouchEvent均不执行。
如果我们修改onTouch的返回值为true,那么后面的onTouchEvent事件就不会执行了。
流程示意图如下:
如上,恢复默认返回值false,然后在button上设置一个监听点击事件,代码如下:
mybutton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.i("yumf", "Activity=====onClick");
}
});
执行后,查看日志打印信息,如下:
由此我们可以知道,在完整的事件结束之后(从ACTION_DOWN开始,到ACTION_UP结束),这时才会去执行button的onClick方法。
综合以上所述,View在处理Touch事件时,都是从dispatchTouchEvent方法开始的,因此我们在分析源码时,可以从该方法入手。
源码阅读
我们当前的MyButton是继承自Button,而Button又是继承自TextView,TextView继承自View,逐步往上查看,可以发现父类的dispatchTouchEvent方法,就是View的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) {
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;
}
如上代码中,我们来逐一进行分析,首先是
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
通过查看mInputEventConsistencyVerifier,得知这段代码主要是用来调试的,可以不用关注。接着继续查看下一段代码
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
当执行ACTION_DOWN事件时,进入方法stopNestedScroll()中,进入该方法中
/**
* Stop a nested scroll in progress.
*
* <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
*
* @see #startNestedScroll(int)
*/
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
mNestedScrollingParent.onStopNestedScroll(this);
mNestedScrollingParent = null;
}
}
该方法主要是用来停止View的滑动,当一个滚动的view不是当前进行接收事件的View时不会受到影响。下面的一段代码是关键的代码,我们来看看
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;
}
}
上面的代码中,首先根据安全策略过滤event,来确定是否响应这个事件,返回true表示响应。响应该事件后,将mListenerInfo赋值给ListenerInfo对象。那么这个mListenerInfo到底是什么呢,我们现在来分析一下mListenerInfo的初始化
首先,我们可以在View的属性中,能看到该对象的引用:
ListenerInfo mListenerInfo;
接着,在getListenerInfo()方法中初始化:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
最后,在为该View的对象设置监听器时,会将对应的监听器对象返回赋值给mListenerInfo对象,如下:
/**
* Register a callback to be invoked when focus of this view changed.
*
* @param l The callback that will run.
*/
public void setOnFocusChangeListener(OnFocusChangeListener l) {
getListenerInfo().mOnFocusChangeListener = l;
}
/**
* Returns the focus-change callback registered for this view.
*
* @return The callback, or null if one is not registered.
*/
public OnFocusChangeListener getOnFocusChangeListener() {
ListenerInfo li = mListenerInfo;
return li != null ? li.mOnFocusChangeListener : null;
}
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
/**
* Register a callback to be invoked when this view is clicked and held. If this view is not
* long clickable, it becomes long clickable.
*
* @param l The callback that will run
*
* @see #setLongClickable(boolean)
*/
public void setOnLongClickListener(OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
如上,其实里面涉及的方法非常多,我只抽出了几个常见的方法,如:setOnClickListener,setOnTouchListener等。
所以说当我们给View的对象设置监听器时,通过回调的方式,最后都会赋值到mListenerInfo对象中。mListenerInfo类里面包含了许多的监听器类型,如下:
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}
完成了监听器类型的赋值后,我们分析继续下面的代码逻辑:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
这里在if条件里面我们看到了一个属性的方法li.mOnTouchListener.onTouch(this, event),这就是我们在Activity中设置的setOnTouchListener中,重写的onTouch方法。当返回为true时,result = true,这时便不执行下面代码中的onTouchEvent(event)方法。result 为false时,才执行onTouchEvent(event)方法。这段关键性的代码中,对应了我之前所做的实验结果。
下面,我们继续分析方法View的onTouchEvent(MotionEvent event)的内部执行。
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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) {
// 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();
}
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();
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;
}
首先看这个方法的说明,实现这个方法来处理触摸屏幕的动作事件。如果这个方法被用来检测点击动作,它是建议执行和调用的操作。如果这个事件被处理,返回true,否则返回false。
现在我们来看逐一代码,第一个if语句块中,判断View的状态是否可用,如果不可用则设置为不可按压,否则为设置为可点击和可长按。然后下面在可点击和可长按的条件下,进行touch事件的逻辑处理。在这个if语句内部有switch条件判断,将分别对不同的事件进行处理,如MotionEvent.ACTION_UP,MotionEvent.ACTION_DOWN,MotionEvent.ACTION_CANCEL 和MotionEvent.ACTION_MOVE几个不同的事件。下面我们将逐一对其进行分析。
首先是MotionEvent.ACTION_UP中:
判断prepressed为true后,进入执行体;
设置setPressed(true, x, y);
判断mHasPerformedLongPress是否执行长按操作,如果mOnLongClickListener.onLongClick 返回true时,mHasPerformedLongPress = true,这时便不会执行performClick()方法。否则继续执行如下,判断mPerformClick为空,初始化一个实例。添加到消息队列中,如果添加失败则直接执行performClick()方法,否则在PerformClick对象的run中执行performClick()。查看一下performClick()方法,如下:
/**
* Call this view‘s OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
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;
}
如上,我们可以看到一个非常熟悉的方法onClick。在if中判断,如果我们给View设置了mOnClickListener的监听接口,在这里我们会回调mOnClickListener中的onClick方法。(原来点击事件的onClick方法是在ACTION_UP时,执行的)
接着,看到创建UnsetPressedState对象,然后执行UnsetPressedState对象中的run方法,我们进入这个方法查看,
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
/**
* Sets the pressed state for this view.
*
* @see #isClickable()
* @see #setClickable(boolean)
*
* @param pressed Pass true to set the View‘s internal state to "pressed", or false to reverts
* the View‘s internal state from a previously set "pressed" state.
*/
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
可以看到,这里面是用来取消mPrivateFlags 中的PFLAG_PRESSED标志,然后刷新背景。
ACTION_UP最后一步,removeTapCallback() 移除消息队列中的之前加入的所有回调操作。
接着分析MotionEvent.ACTION_DOWN中内部代码:
首先mHasPerformedLongPress = false,设置长按操作为false。
接着判断View是否处在可滑动的容器中,如果为false,则设置View的PRESSED状态和检查长按动作。
接着分析MotionEvent.ACTION_CANCEL的事件:
代码非常简单,设置PRESSED为false,移除所有的回调,移除长按的回调。
最后来分析MotionEvent.ACTION_MOVE的事件:
判断触摸点是否移出View的范围,如果移出了则执行removeTapCallback(),取消所有的回调。接着判断是否包含PRESSED标识,如果包含则执行方法removeLongPressCallback() 和 setPressed(false);
到这里我们可以知道,onTouchEvent方法中处理Touch事件的具体操作,并控制了View的点击事件。在如果在点击View时,想要长按和短按都产生效果,即setOnLongClickListener和setOnClickListener都能够执行的话,只需要在setOnLongClickListener的onLongClick方法中返回false,这时两个方法便都能执行。
至此关于View的Touch事件分发流程已经分析完成,下一篇将介绍ViewGroup的分发机制。