Android动态响应怎么关闭,Android 事件分发中你可能忽略掉的点!

原标题:Android 事件分发中你可能忽略掉的点!

本文作者

作者: fishforest

链接:

https://www.jianshu.com/p/0a8ec531d5fb

前言

对于Android MotionEvent,我们平时大多关注的是ACTION_DOWN、ACTION_UP、ACTION_MOVE,本篇将重点分析ACTION_CANCEL产生的原因及其滑动事件的处理。

通过本篇文章,你将了解到:

1、ACTION_CANCEL产生的原因。

2、手指离开当前View时事件处理。

3、手指离开屏幕时事件处理。

1

ACTION_CANCEL 产生的原因

从ViewGroup 入手分析

事件分发是从ViewGroup-->View,因此想要知道View是否收到ACTION_CANCEL,需要从ViewGroup入手,而ViewGroup 分发的重点即在dispatchTouchEvent(xx)里。

72713ab0c63c01a617c8ccd8a2c43412.png

先看看dispatchTouchEvent(xx)代码,之前的文章有详细分析过,此次重点关注

ACTION_CANCEL的处理逻辑:

#ViewGroup.java

@Override

publicboolean dispatchTouchEvent(MotionEvent ev) {

boolean handled = false;

if(onFilterTouchEventForSecurity(ev)) {

finalint action = ev.getAction;

finalint actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.

if(actionMasked == MotionEvent.ACTION_DOWN) {

//首次Down事件处理------------>(1)

cancelAndClearTouchTargets(ev);

resetTouchState;

}

//ViewGroup是否拦截事件

...

//是否发送取消事件

finalboolean canceled = resetCancelNextUpFlag( this)

|| actionMasked == MotionEvent.ACTION_CANCEL;

//寻找接收了Down事件的子View(子布局)

...

if(mFirstTouchTarget == null) {

//没有子View(子布局)消费事件,于是事件流转到ViewGroup onTouchEvent

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

} else{

//有子View(子布局)消费事件

TouchTarget predecessor = null;

TouchTarget target = mFirstTouchTarget;

while(target != null) {

//遍历消费链

finalTouchTarget next = target.next;

if(alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {

//已经消费过,直接返回

handled = true;

} else{

//判断是否需要发送取消事件------------>(2)

finalboolean cancelChild = resetCancelNextUpFlag(target.child)

|| intercepted;

//分发事件------------>(3)

if(dispatchTransformedTouchEvent(ev, cancelChild,

target.child, target.pointerIdBits)) {

handled = true;

}

if(cancelChild) {

//如果是取消事件,则子View(子布局)没必要消费了,因此从消费链里摘除

if(predecessor == null) {

mFirstTouchTarget = next;

} else{

predecessor.next = next;

}

target.recycle;

target = next;

continue;

}

}

predecessor = target;

target = next;

}

}

...

}

...

returnhandled;

}

上面列出了三个重点,将一一分析:

(1)

主要是 cancelAndClearTouchTargets(xx)方法:

#ViewGroup.java

privatevoidcancelAndClearTouchTargets( MotionEvent event){

//子View(子布局) 消费了Down事件

if(mFirstTouchTarget != null) {

boolean syntheticEvent = false;

...

for(TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {

resetCancelNextUpFlag(target.child);

//分发cancel 事件

dispatchTransformedTouchEvent( event, true, target.child, target.pointerIdBits);

}

...

}

}

可以看出,最终调用了dispatchTransformedTouchEvent(xx)发送cancel事件。

按照正常流程来说,因为收到 Down事件时,mFirstTouchTarget==null,因此此处通常不会执行发送cancel事件。

(2)

resetCancelNextUpFlag(xx)方法,顾名思义:重置取消标记。

#ViewGroup.java

privatestaticboolean resetCancelNextUpFlag( @NonNull View view){

if((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {

//之前设置过取消标记,此处重置

view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;

//返回true

returntrue;

}

returnfalse;

}

PFLAG_CANCEL_NEXT_UP_EVENT标记的作用是记录View是否被临时移出Window。比如View.performButtonActionOnTouchDown(xx)处理鼠标相关的问题,这个值平时也很少用到。

既要判断resetCancelNextUpFlag(xx)返回值,也要判断intercepted值:ViewGroup是否拦截并消费了事件。

若是两者之一有一者满足,则认为需要发送 cancel事件。

(3)

(2)点仅仅是判断是否需要发送 cancel事件,真正发送事件的方法是dispatchTransformedTouchEvent(xx):

#ViewGroup.java

privateboolean dispatchTransformedTouchEvent( MotionEvent event, boolean cancel,

View child, intdesiredPointerIdBits ) {

final boolean handled;

final intoldAction = event.getAction;

//判断参数cancel 是否为true

if(cancel || oldAction == MotionEvent.ACTION_CANCEL) {

//将event 事件设置为cancel

event.setAction(MotionEvent.ACTION_CANCEL);

if(child == null) {

//ViewGroup自己消费

handled = super.dispatchTouchEvent( event);

} else{

//子View(子布局) 消费

handled = child.dispatchTouchEvent( event);

}

//处理后,将action重置

event.setAction(oldAction);

returnhandled;

}

//正常事件流程

}

结合上述3点可知,在ViewGroup.dispatchTouchEvent(xx)里发送cancel事件,常用的判断即是:

1、ViewGroup是否拦截消费了事件,若是则给子View(子布局)发送cancel事件。

2、发送给子 View cancel事件后,后续的事件将不会发给子View。

那么除了ViewGroup 拦截消费事件,还有哪些地方触发发送cancel事件呢?

ViewGroup 移除View

想象一种场景:

手指在View 上滑动,在此过程中,View 被移出ViewGroup。

来看ViewGroup.remove(xx)的实现:

removeView(view)-->removeViewInternal(view)-->removeViewInternal(index, view)

核心功能在removeViewInternal(index, view)实现:

#ViewGroup.java

privatevoidremoveViewInternal( intindex, View view){

...

view.clearAccessibilityFocus;

//发送cancel 事件

cancelTouchTarget(view);

cancelHoverTarget(view);

if(view.getAnimation != null||

(mTransitioningViews != null&& mTransitioningViews.contains(view))) {

addDisappearingView(view);

} elseif(view.mAttachInfo != null) {

//调用detached

view.dispatchDetachedFromWindow;

}

...

}

可以看出,当View 从ViewGroup 移除后,若是它已经消费了事件,那么将会收到cancel事件。

使用如下代码测试:

viewGroup.postDelayed( newRunnable {

@Override

publicvoidrun{

viewGroup.removeAllViews;

}

}, 3000);

手指按在View 上,3s后将View 从ViewGroup里移除。

Window 移除View

当调用WindowManager.removew(view)方法时,最终会调用到ViewGroup.dispatchDetachedFromWindow(xx)方法:

##ViewGroup.java

voiddispatchDetachedFromWindow{

//发送 cancel 事件

cancelAndClearTouchTargets( null);

...

}

使用如下代码测试:

viewGroup.postDelayed( newRunnable {

@Override

publicvoidrun{

getWindowManager.removeView(getWindow.getDecorView);

}

}, 3000);

手指按在View 上,3s后将View 从Window里移除。

可以看出,触发发送cancel事件常见的有三种场景:

e42765e734ca7cdae2d56398469360d3.png

场景

2

手指离开当前View时事件处理

99a5fc689448a0e64ea940b1242c73fc.png

手指图

如上图,手指在View 上按下,View消费了Down事件,此时手指滑出View,并在ViewGroup上滑动,那么整个事件分发是怎么样的呢?

通常来说,单指滑动的时候事件分为三个过程:

1、按下事件--->Down事件。

2、滑动事件--->Move事件。

3、抬起事件---> Up事件。

从事件分发流程可知,当手指按下的时候:

1、若View 消费了Down事件,那么之后的Move、Up事件都会传递给View(不考虑ViewGroup拦截情况)。

2、手指在滑动的过程中,滑出了View,在ViewGroup上滑动,此时 Move、Up事件依然会传递给View。

从直觉上来说,应该不响应的。

来看看源码里是如何处理的:

#View.java

publicboolean onTouchEvent( MotionEvent event){

//点击的坐标

final floatx = event.getX;

final floaty = event.getY;

...

if(clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

switch(action) {

caseMotionEvent.ACTION_UP:

...

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

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

//处在按下状态

...

//没有发生长按动作

if(!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

//移除长按动作

removeLongPressCallback;

if(!focusTaken) {

if(mPerformClick == null) {

mPerformClick = newPerformClick;

}

//响应点击动作

if(!post(mPerformClick)) {

performClickInternal;

}

}

}

...

}

mIgnoreNextUpEvent = false;

break;

caseMotionEvent.ACTION_DOWN:

...

break;

caseMotionEvent.ACTION_CANCEL:

if(clickable) {

//重置按下状态

setPressed( false);

}

//移除延时单击动作

removeTapCallback;

//移除长按动作

removeLongPressCallback;

...

break;

caseMotionEvent.ACTION_MOVE:

...

if(!pointInView(x, y, touchSlop)) {

//移除延时单击动作

removeTapCallback;

//移除长按动作

removeLongPressCallback;

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

重置按下状态

setPressed( false);

}

mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

}

...

break;

}

returntrue;

}

returnfalse;

}

A、正常响应点击/长按动作

对于正常情况来说,比如上面的对ACTION_UP事件的处理。

先说单击动作:

1、首先需要当前View 处在按下状态。

B、移动时处理点击/长按动作

1、一直在监测当前的事件点坐标是否还在目标View里,若不在,则进行第2步处理。

C、总结

1、一旦View消费了Down事件,那么后续的Move、Up、Cancel等事件都会交给它处理。(ViewGroup没有拦截消费的前提下)

2、即使滑动超出了当前View的范围,它依然能够收到上述事件。3

手指离开屏幕时事件处理

当手指滑动离开屏幕时,如下图:

a87cfae1f0410aab73de968491c84e1a.png

其处理逻辑与手指离开当前View时事件处理是一致的。

本文基于Android 10。

各种事件Demo请移步: 相关代码演示

https://github.com/fishforest/AndroidDemo

最后推荐一下我做的网站,玩Android: wanandroid.com,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!

开启B站少女心,探究APP换肤

不需要权限的悬浮窗方案了解一下~

引入Jetpack架构后,你的App会发生哪些变化?

如果你想要跟大家分享你的文章,欢迎投稿~

┏(^0^)┛明天见!返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值