概述
本文主要分享Android常見的事件沖突處理烂完,處理方式有兩種:
- 外部攔截:父容器處理沖突
- 內(nèi)部攔截:子控件處理沖突
在介紹這兩種處理方法之前祸挪,我們必須先了解兩件事情:
- 事件在控件中是如何傳遞的
- 事件沖突產(chǎn)生的根本原因
事件在控件中是如何傳遞的
先來看一張事件分發(fā)的大致流程圖:
通過流程圖可知套耕,事件的分發(fā)是從Activity的dispatchTouchEvent開始傳遞的丰刊,然后調(diào)用PhoneWindow的superDispatchTouchEvent澜躺,再調(diào)用DecorView的superDispatchTouchEvent桩蓉,再調(diào)用到ViewGroup的dispatchTouchEvent方法淋纲,ViewGroup要先走分發(fā)流程,再走處理流程院究,而View只能走處理流程洽瞬。下面便從ViewGroup的dispatchTouchEvent方法分析事件的傳遞流程。
DOWN事件
事件的分發(fā)是從Down事件開始的业汰,Down事件只有一個(gè)片任,ViewGroup的dispatchTouchEvent方法對(duì)Down事件的處理方式有以下兩種:
- 攔截事件
- 不攔截事件
接下來結(jié)合源碼分析這兩種處理方式有什么區(qū)別。
攔截事件
跟蹤ViewGroup中dispatchTouchEvent方法針對(duì)ACTION_DOWN處理的關(guān)鍵代碼:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
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;
}
...
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上述中有一句關(guān)鍵代碼:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept表示是否不允許父控件攔截蔬胯,由于在MotionEvent.ACTION_DOWN中調(diào)用了resetTouchState方法:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
所以在MotionEvent.ACTION_DOWN時(shí)disallowIntercept的值為fasle,此時(shí)會(huì)調(diào)用ViewGroup的onInterceptTouchEvent对供,因?yàn)閿r截了Down事件,所以onInterceptTouchEvent返回true,此時(shí)事件停止向子控件分發(fā)氛濒,交給自身處理即mFirstTouchTarget==null,然后會(huì)調(diào)用到以下的關(guān)鍵代碼:
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)
跟蹤dispatchTransformedTouchEvent方法的關(guān)鍵代碼:
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
由源碼可知产场,由于child==null會(huì)調(diào)用到 super.dispatchTouchEvent方法,即調(diào)用到View的dispatchTouchEvent方法舞竿,關(guān)鍵代碼如下:
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
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
由源碼可知京景,View的dispatchTouchEvent中會(huì)根據(jù)mOnTouchListener.onTouch或onTouchEvent是否返回true,來判斷是否消費(fèi)該事件骗奖。到這里攔截事件的基本流程就結(jié)束了确徙。
這里補(bǔ)充一個(gè)小知識(shí)點(diǎn)醒串,由于mOnTouchListener.onTouch是優(yōu)先與onTouchEvent,所以當(dāng)mOnTouchListener.onTouch返回true時(shí)鄙皇,以下代碼不會(huì)執(zhí)行:
if (!result && onTouchEvent(event)) {
result = true;
}
那如果此時(shí)控件同時(shí)設(shè)置了onClick事件便會(huì)失效芜赌,因?yàn)樵趏nTouchEvent的ACTION_UP事件中調(diào)用了 performClick() 方法:
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
不攔截事件
ViewGroup的onInterceptTouchEvent返回false(默認(rèn)返回false)時(shí),此時(shí)會(huì)將事件分發(fā)給子控件處理伴逸,如果子控件都不處理則自己處理該事件缠沈,關(guān)鍵代碼如下:
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
遍歷子控件集合時(shí),會(huì)根據(jù)子控件的dispatchTransformedTouchEvent方法判斷是否有子控件處理了事件错蝴,若有子控件處理洲愤,會(huì)執(zhí)行如關(guān)鍵代碼:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
這里采用了鏈表來存儲(chǔ)目標(biāo)控件,此時(shí)mFirstTouchTarget不為空顷锰,那如果子控件都沒有處理柬赐,是如何將事件再交給父控件處理呢?繼續(xù)跟蹤源碼:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
當(dāng)沒有子控件都沒有處理事件時(shí)官紫,mFirstTouchTarget=null,此時(shí)會(huì)調(diào)用ViewGroup的dispatchTransformedTouchEvent方法自己處理該事件躺率,如果有子控件處理,會(huì)執(zhí)行以下判斷:
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
因?yàn)橛凶涌丶幚硗蚍藭r(shí)alreadyDispatchedToNewTouchTarget = true、mFirstTouchTarget=newTouchTarget慎框、 target.next=null即滿足上述條件良狈,while循環(huán)只會(huì)執(zhí)行一次,到這里不攔截事件的基本流程就結(jié)束了笨枯。
MOVE事件
Move事件的傳遞也是通過以下兩種方式進(jìn)行分析:
- 不攔截Move事件傳遞
- 攔截Move事件傳遞
Move事件正常傳遞
父控件不攔截事件時(shí)薪丁,Move事件的傳遞也是通過dispatchTouchEvent方法傳遞給目標(biāo)控件的,關(guān)鍵代碼如下:
boolean alreadyDispatchedToNewTouchTarget = false;
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.....
}//此時(shí)是Move事件馅精,不會(huì)執(zhí)行這段代碼
}
.....
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
這里需要注意由于此時(shí)alreadyDispatchedToNewTouchTarget=false,所以會(huì)走else分支严嗜,會(huì)執(zhí)行以下關(guān)鍵代碼:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
將Move事件交給對(duì)應(yīng)的目標(biāo)控件(Down事件保存的Target),到這里正常的Move事件就執(zhí)行完了洲敢。
攔截Move事件傳遞
分析攔截事件時(shí)漫玄,先來看一段代碼:
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;
}
通過上面Down事件分析可知,由于Down事件做了重置操作压彭,所以disallowIntercept的值為false,即if分支的代碼一定會(huì)執(zhí)行睦优,此時(shí)攔截子控件的Move事件,會(huì)執(zhí)行以下關(guān)鍵代碼:
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
因?yàn)閿r截了move事件壮不,此時(shí)intercepted=true, cancelChild=true,此時(shí)會(huì)設(shè)置子控件的Action為MotionEvent.ACTION_CANCEL汗盘,取消子控件的事件,并且注意以下代碼:
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
由于此時(shí)cancelChild=true, mFirstTouchTarget被設(shè)置成null,本次Move事件就結(jié)束了询一,注意ViewGroup是在下一Move事件才能夠接收到事件隐孽,因?yàn)橄乱淮蜯ove事件會(huì)重新走dispatchTouchEvent方法癌椿,關(guān)注以下代碼:
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;
}
由于此時(shí)是Move事件并mFirstTouchTarget=null,所以此時(shí)走else分支intercepted = true菱阵,Move事件會(huì)交給自身處理踢俄,關(guān)聯(lián)代碼:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
小結(jié)一下,當(dāng)父控件攔截Move事件時(shí)送粱,第一次會(huì)將子控件的事件類型設(shè)置為MotionEvent.ACTION_CANCEL并將mFirstTouchTarget賦值為null,此時(shí)第一次Move事件結(jié)束(由于子控件的dispatchTransformedTouchEvent返回true),第二次以后的Move事件才會(huì)傳遞到父控件褪贵。
UP與Cancel事件
一次完整的事件,首先有Down事件開始抗俄,中間有多個(gè)Move事件脆丁,最后由Up/Cancel事件結(jié)束,Up事件是正常結(jié)束动雹,而Cancel事件是被父控件攔截后產(chǎn)生的
事件分發(fā)完整流程圖
為了進(jìn)一步理解上述的源碼分析流程槽卫,下面提供一張完整的事件分發(fā)流程圖:
事件沖突處理
通過前面的鋪墊,可以知道事件沖突只能在Move事件中處理胰蝠,可以通過外部攔截和內(nèi)部攔截處理事件沖突歼培,這里以SwipeRefreshLayout嵌套ViewPager為例:
外部攔截
根據(jù)父控件的滑動(dòng)邏輯在onInterceptTouchEvent方法中返回true/false,核心代碼:
public class CustomSRL2 extends SwipeRefreshLayout {
//外部攔截成員變量
private float startX;
private float startY;
//ViewPager是否滾動(dòng)
boolean mIsVpMove = false;
//觸發(fā)移動(dòng)事件的最小距離,如果小于這個(gè)距離就不觸發(fā)移動(dòng)控件,如Viewpager就是用這個(gè)距離來判斷用戶是否翻頁
private int mTouchSlop;
public CustomSRL2(Context context) {
this(context, null);
}
public CustomSRL2(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//外部攔截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
mIsVpMove = false;
break;
case MotionEvent.ACTION_MOVE:
//若此時(shí)ViewPager還在滑動(dòng)茸塞,則返回false,不攔截
if (mIsVpMove) {
return false;
}
float x = ev.getX();
float y = ev.getY();
float deltaX = Math.abs(x - startX);
float deltaY = Math.abs(y - startY);
if (deltaX > mTouchSlop && deltaX > deltaY) {
mIsVpMove = true;
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsVpMove = false;
break;
}
return super.onInterceptTouchEvent(ev);
}
}
內(nèi)部攔截
根據(jù)子控件的滑動(dòng)邏輯調(diào)用父控的requestDisallowInterceptTouchEvent(true/false)方法通知父控件是否不攔截事件,核心代碼:
public class CustomSRL2 extends SwipeRefreshLayout {
public CustomSRL2(Context context) {
super(context);
}
public CustomSRL2(Context context, AttributeSet attrs) {
super(context, attrs);
}
//以下代碼為內(nèi)部攔截代碼
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//在ACTION_DOWN事件返回false,不攔截事件躲庄,將事件交給子控件處理
if(ev.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;//攔截事件
}
}
public class CustomVPInner extends ViewPager {
private float startX;
private float startY;
public CustomVPInner(Context context) {
super(context);
}
public CustomVPInner(Context context, AttributeSet attrs) {
super(context, attrs);
}
//內(nèi)部攔截:使用ViewCompat.setNestedScrollingEnabled(this,true),參考以下代碼
/**
* public void requestDisallowInterceptTouchEvent(boolean b) {
* // if this is a List < L or another view that doesn't support nested
* // scrolling, ignore this request so that the vertical scroll event
* // isn't stolen
* if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
* || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
* // Nope.
* } else {
* super.requestDisallowInterceptTouchEvent(b);
* }
* }
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
ViewCompat.setNestedScrollingEnabled(this,true);
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float x = ev.getX();
float y = ev.getY();
float deltaX = Math.abs(x - startX);
float deltaY = Math.abs(y - startY);
if (deltaX < deltaY) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
//打印ViewPager是否消費(fèi)了該事件钾虐,如果沒有噪窘,事件還是會(huì)交給SwipeRefreshLayout處理
boolean consume = super.dispatchTouchEvent(ev);
Log.e("fmt","consume=" + consume);
return super.dispatchTouchEvent(ev);
}
}
完整代碼實(shí)現(xiàn)
百度鏈接
密碼:1cq9