您的位置:首页 > 移动开发 > Android开发

Android Touch事件分发和消费机制

2016-12-22 17:30 344 查看

概述

Touch事件分发和消费机制是指用户用手操作手机屏幕时所造成的事件触发,最基本的包括以下四种:

- 按下Down

- 移动Move

- 取消Cancel

- 离开触摸屏Up

一个完整的Touch过程一般是由Down->(Move)->Up/Cancel这四个事件组成,值得注意的是,一个完整的触摸事件必须由Down开始,再到Up/Cancel结束,中间的Move可以有可以没有。

Touch事件还有很多,这四个是最基础的。

总体流程

一个Touch事件的触发,必须经由Activity向下分发,最终到达接受的View进行消费处理。由Activity开始分发,如果ViewGroup在子View中找到可以处理该事件的View,则继续向下传递,否则ViewGroup会尝试消费处理该事件。

View对Touch事件的处理过程

首先来看View怎么处理Touch事件。

dispatchTouchEvent

//最主要的触摸事件的分发逻辑,向接收Touch事件的子View(包括自己)派发事件,对于View而非ViewGroup来说,这里只会对自己分发。
boolean dispatchTouchEvent(MotionEvent event);


这个方法用来判断和控制事件的分发,也就是说不论该View是否对Touch事件响应,都要触发这个方法。只有触发这个方法,才能确实事件究竟是消费还是继续向下分发。

来看一下部分源码分析。

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//1.停止嵌套滑动
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
//2.安全监测
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;

//3.如果当前View使能(setEnabled(true)),则调用Touch监听器
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
//停止嵌套滑动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}


首先可以看到,该方法会返回一个boolean值,用来判断事件是否继续向下分发。如果boolean值为true,则表明事件被消费;如果为false,则表明事件没有被这个View消费,继续向下分发。

流程

在dispatchTouchEvent一开始,就定义了一个result变量,并在最后返回。当result返回为false时,表明事件没有被这个View处理,继续分发;如果返回为true,则表明事件已经被这个View消费处理。

1. 先暂时停止嵌套滑动,开始对事件进行分析

2. 安全检测:如果view通过setFilterTouchesWhenObscured(true)方法设置了安全检测,那在此处就会做出判断。

- 安全检测:官方给出的解释是:

Specifies whether to filter touches when the view’s window is obscured by another visible window.

意思是说,指定当一个窗体显示的时候(获取焦点),是否隐藏该view的响应。

也就是说,如果我们的view被设置setFilterTouchesWhenObscured(true)或者android:filterTouchesWhenObscured的话,当另外一个窗体显示的时候,我们的view不响应任何事件。

注:

/**
* Filter the touch event to apply security policies. 过滤触摸事件应用安全策略
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.如果是True的话就向下传递,否则就跳出触摸事件
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch. 如果窗口被遮挡,就跳出触摸事件
return false;
}
return true;
}


从源码不难看出,如果此处窗口被遮挡,onFilterTouchEventForSecurity就会返回false,安全检测也会因为false而无法进入if内部,整个dispatchTouchEvent也就会的返回值result就会变成false,表明事件没有被消费处理,继续分发。

3. 判断当前的View是否使能,即是否能够处理Touch事件。如果可以,则调用该View的OnTouchListener中的onTouch方法。在View里可以通过重写onTouch方法操作Touch事件。之后dispatchTouchEvent的返回值result将被设置为true,表明该事件已经在此处被处理消费,不再继续分发。

4. 最后一步,如果onTouchListener返回false或者没有onTouchListener,就返回调用onTouEnent,之后同样将result改为true,最终返回时表示事件已经被消费处理。

onTouchEvent

首先来看几个动作

press :按下时候View状态的改变,比如View的背景的drawable会变成press 状态

click/tap: 快速点击

longClick:长按

focus:跟press类似,也是View状态的改变

touchDelegate:分发这个点击事件给其他的View,这个点击事件传到其他View前会改变这个事件的点击坐标,如果在指定的Rect里面,则是View的中点坐标,否则在View之外。

废话不多说,上源码

public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
//1.View不使能的情况下(setEnabled(false)),依然可能消费事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

//2.用TouchDelegate将自己的区域变成其他View中心点的操作
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

//3.从这里跟1结合可以知道,只要View是Clickable或者LongClickable,就一定消费事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//只有在press的情况下,才可能click,longClick也做了同样的判断
if ((mPrivateFlags & PRESSED) != 0) {
//4.如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//5.已经有longClick执行过了,就不再执行click了
if (!mHasPerformedLongPress) {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
if (!focusTaken) {
performClick();
}
}
//6.取消press
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (!post(mUnsetPressedState){
mUnsetPressedState.run();
}
}
break;

case MotionEvent.ACTION_DOWN:
//7.press,定时检测并且执行longclick
mPrivateFlags |= PRESSED;
refreshDrawableState();
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick();
}
break;

case MotionEvent.ACTION_CANCEL:
//8.清理状态
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
break;

case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();

//9.如果移动到View外,则不press,如果移动到View内,则press
int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press checks
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
} else {
// Inside button
if ((mPrivateFlags & PRESSED) == 0) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}

return false;


流程

首先,onTouchEvent同样会返回一个boolean值,当为true时,表示已经在onTouchEvent里消费处理了事件;如果为false,则表明事件没有被消费处理,依然要继续分发。

1.判断View控件是否使能 viewFlags是用来记录View的各种状态。不要纠结在viewFlags & ENABLED_MASK这一句,这是Android非常巧妙的位与运算。从宏观上看能够对事件常量进行判断,这里可以理解为,位于运算后获取一个属性值,如果是ENABLE就进入if。从源码中看,即便该View不使能,只要可以点击或长按,事件都会被消费掉。

2.判断这个View所占的区域是否被设定为其它View的操作响应区域。这里的mTouchDelegate对象包含了一个区域和另一个View。如果此处的mTouchDelegate不为空,则调用mTouchDelegate的触摸响应事件。这样一来,Touch事件也被消费掉了,因此返回一个true。

3.对事件进行分析判断。如果事件在前两步没有被消费,那走到这一步的时候,可以确定的是这个View是使能的而且没有被纳入其它View的响应消费空间。继续往下看,如果这个View是可点击或可长按,就要对传入的Touch事件进行分析和判断了。

- ACTION_DOWN:手指按在屏幕上时的响应

1. 将View的状态设置为pressed。

2. 刷新VIew在pressed状态时的状态(背景、资源等)。

3. 如果这个View的属性是可以长按,那就调用长按的方法进行处理。

- ACTION_UP:手指离开屏幕时抬起的响应

1. 判断是否是press状态,只有press的情况下才能执行点击和长按。

2. 如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click。(想想看我们打开dialog后,点击阴影部分的时候。)

3. 如果已经执行了长按,就不再执行点击了。

4. 最后就是取消press状态

- ACTION_CANCEL:当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL。例如按在一个ListView上,首先是按在item上,但你一旦手指滑动出item,就出发了item的ACTION_CANCEL,之后item就不做反应,表现为你开始拖着ListView上下滑动了。

1.清理View状态,然后刷新View的资源状态。

- ACTION_MOVE :滑动判断

1. 获取滑动的具体数据。

2. 如果移动到View外,则不press,如果移动到View内,则press。

(这里不作详细分析)

从宏观上看,只要有对应的触摸事件响应了,那Touch事件无疑也就被消费处理了,因此返回true。

4. 如果以上几部都没有对Touch事件作出处理,最后就会返回false,意味着事件没有在onTouchEvent中被消费,继续分发。

简单归纳一下

1. 不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true)

2. 如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作

3. 如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费

4. 处理包括focus,press,click,longclick

ViewGroup对Touch事件的处理过程

ViewGroup继承了View,所以有些处理过程是相同的,重点说说不同之处。

首先就是继承并覆盖了dispatchTouchEvent(MotionEvent event);

而且比View多了一个处理Touch的位置:

viewgroup.onInterceptTouchTouchEvent(MotionEvent event);


这个方法的返回值主要用于是否阻止向子View派发触摸事件,默认返回false,不阻止。

来看源码

public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (action == MotionEvent.ACTION_DOWN) {
//1.只有在非拦截的情况的下寻找target
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 防止onInterceptTouchEvent()的时候改变Action
ev.setAction(MotionEvent.ACTION_DOWN);
// 遍历子View,第一个消费这个事件的子View的为Target
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
//当然只遍历可见的,并且没有在进行动画的。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
if (child.dispatchTouchEvent(ev))  {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
}
}
}
}
}

boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
//up或者cancel的时候清空DisallowIntercept
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// 如果没有target,则把自己当成View,向自己派发事件
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
return super.dispatchTouchEvent(ev);
}

// 如果有Target,拦截了,则对Target发送Cancel,并且清空Target
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
//up 或者 cancel清空Target
if (isUpOrCancel) {
mMotionTarget = null;
}

//如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);

return target.dispatchTouchEvent(ev);
}


(源码待逐步分析)

ViewGroup的主要的任务是找一个Target,并且用这个target传递事件,主要逻辑如下

1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target。

2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch())

3.如果有Target并且拦截,则发送Cancel给子View

4.如果有Target并且不拦截,则调用Target的dispatchTouch

5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)

Activity Touch事件触发机制待续

总览流程图

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息