Android 监听 view显示或是隐藏 android监听事件的处理流程_控件

事件序列解析

所谓的安卓事件是什么?具体来说的就是点击和滑动两个操作;抽象着来说就是下面的表格。

MotionEvent/事件类型

具体操作

ACTION_DOWN

点下View

ACTION_UP

抬起View

ACTION_MOVE

滑动View

ACTION_CANCEL

非人为因素取消

事件序列一般组成:

点击的事件组成就是:Down --> Up

滑动的事件组成就是:Down --> Move --> Move .... --> Up

事件分发

使用到的函数dispatchTouchEvent():用于事件分发

onTouchEvent():消费事件

onInterceptTouchEvent():判断是否拦截事件,仅存在于ViewGroup

分发对象

Activity

ViewGroup

View

Activity的事件分发

public boolean dispatchTouchEvent(MotionEvent ev){

// 从判断语句中可以得出所有事件的起点就是Down

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

// 实现屏保功能

onUserInteraction();

}

// 向上传递至ViewGroup,调用其dispatchTouchEvent

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

文章中我已经添加了注释内容,其中getWindow()获得就是一个Window抽象类,根据其子类PhoneWindow我们可以很容易得知最后调用的其实就是ViewGroup的dispatchTouchEvent()方法

/**

* 实际上就是判断事件是否是DOWN事件,event的坐标是否在边界内等

*/

public boolean onTouchEvent(MotionEvent event){

if (mWindow.shouldCloseOnTouch(this, event)) {

finish();

return true;

}

return false;

}

最后就是Activity中的onTouchEvent()方法了,这个模块干的事情在注释中也就很清晰明了了。

ViewGroup的事件分发

public boolean dispatchTouchEvent(MotionEvent ev){

// ········

// 初始化Down事件

if (actionMasked == MotionEvent.ACTION_DOWN) {

// 丢弃之前手头上干的事情,重新开始响应Down事件

cancelAndClearTouchTargets(ev);

resetTouchState();

}

// 检查是否需要拦截

final boolean intercepted;

if (actionMasked == MotionEvent.ACTION_DOWN

|| mFirstTouchTarget != null) {

// 这个与运算是用于影响除Down以外的事件的

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 {

// 当前按压的位置没有控件,或者当前控件并不可被点击,直接被ViewGroup拦截

intercepted = true;

}

// ········

/**

*这个判断里面同样的还是判断响应的事件,然后就是通过一个for循环判断位置来判断当前的子控件是否在对应的位置内

* 还有非常重要的一点就是这个循环的判断还是倒叙的

*/

if (actionMasked == MotionEvent.ACTION_DOWN

|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)

|| actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {

········

if (newTouchTarget == null && childrenCount != 0) {

········

for (int i = childrenCount - 1; i >= 0; i--) {

final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);

final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

// 后面的篇幅主要用于判断当前的控件的各种属性是否是满足需要的。比如说位置、是否可以点击、是否隐藏等一系列信息

········

}

········

}

倒序是为了什么呢? 这个问题同样是一个开发过程中常见,但是却很容易被忽略的问题。

你是否有见过这样的一段代码,如果是ButtonA和ButtonB这两个按钮是在同一个位置出现,ButtonA略大于ButtonB,也就是下图所示

Android 监听 view显示或是隐藏 android监听事件的处理流程_控件_02

对应在XML布局文件中的代码一般类似于下面这段。

如果出现点击事件发生在ButtonA上时,只要它有足够的能力势必会被ButtonA所消费。但是如果时出现在ButtonB的区域呢? 这也就体现了倒序去进行事件消费的作用了,如果正序去进行消费的话,那这个事件最后的消费者一定是ButtonA,而我们需要的实际效果是ButtonB在有足够能力消费时交由它进行完成。

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;

}

由onInterceptTouchEvent(MotionEvent ev)函数可知,默认其实并不会去拦截。所以就一般情况而言,dispatchTouchEvent()方法是需要去循环遍历子控件集合去寻找对应的控件的。

使用一个伪代码解释以上的逻辑

public boolean dispatchTouchEvent(MotionEvent ev){

boolean consume = false;

if(onInterceptTouchEvent(ev)){

// 自己拦截,自己消费

onTouchEvent(ev);

}else{

// 不拦截,分发给子View进行消费

consume = child.dispatchTouchEvent(ev);

}

return consume;

}

View事件分发

public boolean dispatchTouchEvent(MotionEvent event){

// ·····

boolean result = false;

if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onTouchEvent(event, 0);

}

if (onFilterTouchEventForSecurity(event)) {

// ·····

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);

}

return result;

}

通过以往的实践,我们知道只有通过设置了监听器的View才能够去监听事件,那么在dispatchTouchEvent()方法中也是一样的,如果View并没有被设置监听器,变量result也不会被赋值成为true。

从代码中很容易看出onTouch()方法的优先级大于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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

switch (action) {

case MotionEvent.ACTION_UP:

// ·····

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

boolean focusTaken = false;

if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {

focusTaken = requestFocus();

}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

removeLongPressCallback();

if (!focusTaken) {

if (mPerformClick == null) {

mPerformClick = new PerformClick();

}

if (!post(mPerformClick)) {

performClickInternal();

}

}

}

// ·····

}

// ·····

mIgnoreNextUpEvent = false;

break;

// ·····

}

return true;

}

return false;

}

在onTouchEvent()方法中其实具体干了一件事情,那就是区别到底是长按事件还是点击事件。

那么先行判断的是长按事件还是点击事件呢?答案很明显,在代码行中removeLongPressCallback();有一个这样的函数,这就是去除长按事件回调的函数,所以答案就是长按事件是第一个被判断的事件,然后才是点击事件。

判断这个方法的事件的方法就是通过做出Up动作时的时间和做出Down动作时的时间间隔。如果Down和Up两个动作之间的时间间隔小于500ms,就是点击事件。

总结


Android 监听 view显示或是隐藏 android监听事件的处理流程_点击事件_03