在从事android开发过程中,我们经常会碰到View之间的滑动冲突,如ScrollView与Listview、RecyclerView之间的嵌套使用。在很好的解决此类问题之前,我们应深入的了解Android事件响应机制。
预备知识
MotionEvent对象的四种状态
MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
通常一次Touch事件是由ACTION_DOWN 开始,经过多次ACTION_MOVE 到ACTION_UP 结束此次点击事件。
Android事件响应机制流程
由外到内”分发、“由内到外”处理的形式实现。Android 事件分发总是遵循 Activity => ViewGroup => View 的传递顺序,从上往下调用dispatchTouchEvent()分发,Activity A ->> ViewGroup B ->> View C,从下往上调用onTouchEvent()处理
View C ->> ViewGroup B ->> Activity A。
事件分发核心函数
1,dispatchTouchEvent()
2,onTouchEvent()
3,onInterceptTouchEvent()
下面我将结合demo以及源码来详细介绍事件分发及处理过程:
在activity中:
protected void initView() {
super.initView();
CustominzeLinearLayout linearLayout = findViewById(R.id.custom_linearlayout);
CustomizeView button = findViewById(R.id.custom_button);
linearLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(tag1,"customizLinearLayout Onclick");
}
});
linearLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(tag1,"customizLinearLayout Ontouch");
return false;
// return true;
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(tag1,"customizeView onclick");
}
});
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(tag1,"customizeView onTouch");
return false;
// return true;
}
});
button.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.d(tag1,"customizeView onLongclick");
return false;
// return true;
}
});
button.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
Log.d(tag1,"customizeView OnScroll");
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(tag1,"Activity dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
// return false;
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(tag1,"Activity onTouchEvent");
return super.onTouchEvent(ev);
//return false;
// return true;
}
在自定的LinearLayout中(LinearLayout 是ViewGroup的一个子类):
public class CustominzeLinearLayout extends LinearLayout {
public CustominzeLinearLayout(Context context) {
super(context);
}
public CustominzeLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustominzeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// public CustominzeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// super(context, attrs, defStyleAttr, defStyleRes);
// }
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(tag1,"ViewGroup dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
// return false;
// return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(tag1,"ViewGroup onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
// return false;
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(tag1,"ViewGroup onTouchEvent");
return super.onTouchEvent(ev);
// return false;
// return true;
}
}
在自定义的Button中(View的子类):
public class CustomizeView extends Button {
public CustomizeView(Context context) {
super(context);
}
public CustomizeView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomizeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// public CustomizeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// super(context, attrs, defStyleAttr, defStyleRes);
// }
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(tag1,"View dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
// return false;
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(tag1,"View onTouchEvent");
return super.onTouchEvent(ev);
// return false;
// return true;
}
}
我们先默认正常情况下 点击button按Onclick响应事件,看下打印的log信息如下:
正如前文所述从上往下调用dispatchTouchEvent()分发,Activity A ->> ViewGroup B ->> View C,由于子view直接处理了onclick点击,所以在onclick方法中处理完后没在向上往调用调用onTouchEvent()处理。
下面结合核心函数及源码介绍事件分发的过程:
public boolean dispatchTouchEvent(MotionEvent ev)
当监听到有触发事件时,首先由Activity进行捕获,然后事件就进入事件分发的流程。Activity本身没有事件拦截,从而将事件传递给最外层的View的dispatchTouchEvent(MotionEvent ev)方法,该方法将对事件进行分发。
return true : View消费所有事件。停止向下分发,
Actiivity dispatchTouchEvent 返回true:
ViewGroup :
View:
return false :停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,那么事件将被系统消费、处理。
将自定义的View的dispatchTouchEvent 返回false,点击查看打印日志如下:
可见交有View的上层空间CustomizLinearLayout 处理事件。
super.dispatchTouchEvent(ev): 默认情况:我们查看一下源码
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//实现屏保功能,该方法为空方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
onUserInteraction();
}
/**
a. getWindow() = 获取Window类的对象
* b. Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象
* c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现 */
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
当一个点击事件未被Activity下任何一个View接收 处理时
return onTouchEvent(ev);
}
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// mDecor = 顶层View(DecorView)的实例对象,mDecor继承自FrameLayout,而FrameLayout 继承ViewGroup,也即mDecor 间接是ViewGroup 的一个子类
return mDecor.superDispatchTouchEvent(event);
}
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
实现从activity的dispatchTouchEvent 分发到 ViewGroup 的dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
.........
View的dispatchTouchEvent 源码部分
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)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
//调用onTouch 方法,后续对比onclick方法
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
。。。。。。
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
return true: 对事件拦截,交由本层的onTouchEvent进行处理。
return false: 不拦截,分发到子View,由子View的dispatchTouchEvent方法处理。
super.onInterceptTouchEvent(ev):在ViewGroup的dispatchTouchEvent中去获取是否拦截,默认不拦截
截取ViewGroup中的一段源码如下:
final boolean intercepted;
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;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
从源码可见,只有在ACTION_DOWN 操作是去判断onInterceptTouchEvent 是否拦截。
事件响应:public boolean onTouchEvent(MotionEvent ev)
return true: 表示onTouchEvent处理完事件后消费了此次事件。
return false: 不响应事件,不断的传递给上层的onTouchEvent方法处理,直到某个View的onTouchEvent返回true,则认为该事件被消费。如果到最顶层View还是返回false,那么该事件不消费,将交由Activity的onTouchEvent进行处理。
return: super.onTouchEvent,不响应事件,结果与return返回false一样。
看一下View的onTouchEvent的源码分析
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
若该控件可点击,则进入switch判断中
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)) {
performClickInternal();
}
}
}
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:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//调用onclick方法
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从View的dispatchTouchEvent 与onTouchEvent 源码中可以看出onTouch方法先比onclick方法调用,如果onTouch 方法返回true,那么onclick方法将不会得到调用,将demo中的onTouch 返回true,打印日志如下:只调用了onTouch方法。
总结
1,Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。
2,onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。
3,onTouch,onTouchEvent,onClick 执行顺序及关系
onTouch,onTouchEvent 都是在View的dispatchTouchEvent中调用。但onTouch()优先于onTouchEvent执行;若手动复写在onTouch()中返回true(即 将事件消费掉),将不会再执行onTouchEvent()
onTouch() 执行总优先于 onClick(),所以执行先后顺序是onTouch, onTouchEvent,onClick。
整个事件分发过程用流程图如下:
在各个角色中3个核心方法的作用如下所示: