事件分发机制
onClick 和 onTouch都会打印吗
事件分发开始会走到这里View.dispatchTouchEvent()->
li.mOnTouchListener.onTouch(this, event)->
false的话->onTouchEvent(event)->case MotionEvent.ACTION_UP->
performClick();-> li.mOnClickListener.onClick(this);所onTouch返回false时会执行onClick,true则不会
ViewPager包含Listview
- onInterceptTouchEvent返回true时 为什么只能左右滑动 不能上下滑
onInterceptTouchEvent 返回true不会问下面了,直接处理 消费掉,
return false 才会传给下面.
- onInterceptTouchEvent返回true时 为什么只能上下滑动 不能左右滑
[Activity.java]
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
[Activity.java]
public Window getWindow() {
return mWindow;
}
private Window mWindow;
Window只有唯一的实现类PhoneWindow
[Window.java]
The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {...}
[PhoneWindow.java]
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
[DecorView.java]
public class DecorView extends FrameLayout ..{...}
public boolean superDispatchTrackballEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
FrameLayout
没有重写dispatchTouchEvent
,接下来,直接到了
disallowIntercept
:是否禁用掉拦截,可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)
请求不拦截- 先走ViewGroup的
onInterceptTouchEvent
,默认返回false - ViewGroup不拦截的话,分发事件,怎么分发的?
buildTouchDispatchChildList():
根据Z值对它的子view 进行排序,返回一个ArrayList- 遍历判断
canViewReceivePointerEvents
:是否能够接收点击事件isTransformedTouchPointInView
:点击事件是否在点击的view区域中dispatchTransformedTouchEvent
分发调用child.dispatchTouchEvent, 这里child如果是ViewGroup,相当于这里递归调用了VIewGroup的dispatchTouchEvent;向下传递的过程中,如果拦截/响应了事件则终止向下传递,并返回给父,父也不用处理了- 拦截/响应事件后设置参数
newTouchTarget = addTouchTarget(child,) = mFirstTouchTarget;
alreadyDispatchedToNewTouchTarget = true
[ViewGroup.java]
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
.................................
// 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;
}
...............
//2.事件分发
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
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;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
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 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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
.......
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;
}
}
}
·················
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
...
}
dispatchTransformedTouchEvent
是对事件的分发或者处理:
返回值变量handled: false表示无人处理,true表示activity处理了
(拦截的话走到这里)child == null时,调用
view
的dispatchTouchEvent
(),就是转到View的事件处理,处理就返回true,赋值给handled变量作为dispatchTransformedTouchEvent
的返回值,true表示;
private boolean dispatchTransformedTouchEvent(MotionEvent event, 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;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
第一个是Down事件
再来是Move事件,会沿着(前面形成的事件分发链)销售链走
怎么沿着销售链走的?
- Move事件照样会走到Activity …ViewGroup的
dispatchTouchEvent
,还是有拦截事件的权利 - 此时 mFirstTouchTarget != null
- 如果第2步不拦截, 会走到
dispatchTransformedTouchEvent(ev,cancelChild,targe.child,..)
,target是自己,然后(递归)去分发
当销售链形成后,11号有什么权利?底层才对上层有反向制约的权利,通过对disallowIntercept
(是否不允许拦截)布尔变量设置
1.onInterceptTouchEvent
返回true就是对事件的拦截(ViewGroup是经销商?),如果disallowIntercept=true
,则ViewPager中onInterceptTouchEvent
返回true也没用。
[ViewGroup.java]
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;
}
2.requestDisallowInterceptTouchEvent(boolean disallowIntercept)
请求不拦截
ViewPager包裹ListView时,让ViewPager不拦截
在ListView中重写如下方法,
- ACTION_MOVE时即左右滑动时,viewPager拦截
- ACTION_DOWN时,上下滑动时,viewPager不拦截
,因此解决办法就是在onTouch方法里返回false。
Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。
总结:
Android事件分发机制分为View和ViewGroup的事件分发和处理,是从ViewGroup传给view的
其中View的会调用onTouch()方法,如果返回true,表示这个事件被他消费掉。则onClick不会执行,点击事件失效
ViewGroup的
1. 首先看他自己是否要拦截,可重写onInterceptTouchEvent中,返回true表示拦截,不会向下传递,默认是返回false,也可通过
requestDisallowInterceptTouchEvent(true)
请求禁用掉拦截.该开关优先于onInterceptTouchEvent 2. 如果不拦截,则遍历它的子view,递归分发事件,一旦某一层消费掉,就往上回传结果,但父view也就不再能处理。
再举几个例子:
layout中2个button,layout的onInterceptTouchEvent返回true,则button的click事件失效。
Android开发艺术探索-事件分发
1.位置参数
View的位置主要由它的4个顶点来决定,对应View的四个属性:top,left,right,bottom
从3.0开始,
新增x,y是新增x,y是View左上角的坐标,
translationX和translationY是View左上角相对于父容器的偏移量
x = left + translationX;
y = top + translationY;
注:View在平移过程中,top和left表示的是原始左上角的位置信息不会改变,此时改变的是新增的4个参数
2.MotionEvent和TouchSlop
3.VelocityTracker、GestureDetector和Scroller
Scroller
用于实现View的弹性滑动.
View的滑动
4.View的事件分发机制
4.1 点击事件的传递规则
所谓点击事件的事件分发,就是对MotionEvent事件的分发过程,由三个很重要的方法来共同完成.
public boolean dispatchTouchEvent(MotionEvent ev).
用来进行事件的分发,返回是否消耗当前事件.
public boolean onIntecerptTouchEvent(MotionEvent event)
上面方法中的方法,用来判断是否拦截某个事件(ViewGroup 中源码默认返回false表示不拦截)
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent
方法中调用,用来处理点击事件,返回是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件.
ViewGroup
关于拦截
1.requestDisallowInterceptTouchEvent() 请求禁用拦截
一般用于子View中调用父view的这个方法,让父view不再拦截除了ACTION_DOWN以外的点击事件。
当面对ACTION_DOWN
事件时,ViewGroup总是会调用自己的onIntecerptTouchEvent询问自己是否拦截事件.
2.当ViewGroup决定拦截事件后,后续的点击事件(不包括ACTION_DOWN
)将会默认交给它处理并且不再问自己是否拦截.
3.onIntecerptTouchEvent()不是每次都会调用,如果想提前处理所有的点击事件,要选择dispatchTouchEvent,前提当然是事件能传到当前的ViewGroup.
分发(不拦截时)
事件分发交给它的子View进行处理,怎么分发?
1.循环遍历ViewGroup的所有孩子,交给能接收点击事件(点击事件的坐标是否落在子元素内)的孩子进行处理(dispatchTouchEvent分发)。
2.如果孩子消耗掉这个事件(dispatchTouchEvent返回true),mFirstTouchTarget被赋值并终止循环;
反之,则交给它的下一个孩子进行处理
3,如果遍历所有的子元素后事件都没有被合适地处理(消耗),两种情况
- 没有孩子
- 所有孩子处理了但是dispatchTouchEvent中返回了false(没消耗)
mFirstTouchTarget就为null,因为没有赋值。ViewGroup会自己处理点击事件(super.dispatchTouchEvent(event)这里是View的方法,没有拦截,没有分发),进入View的点击事件处理
为什么mFirstTouchTarget为null ,ViewGroup就默认拦截接下来同一序列中的所有点击事件.??
因为
if(事件是ACTION_DOWN || mFirstTouchTarget != if(事件是ACTION_DOWN || mFirstTouchTarget != null){
先看是否禁用掉了拦截,判断是否拦截
}else{
intercepted = true;
}
View对点击事件的处理
onTouch(设置了触摸监听器时) 优先于onTouchEvent;
ACTION_UP发生时,会触发performclick方法,内部会调用它的onClick方法;
View的dispatchTouchEvent方法中的switch 对ACTION的分别处理最后都是返回true,表示消耗事件
View的滑动冲突?
1.什么是滑动冲突?
在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。
2.三种场景
- 父View和子View滑动方向不一致
- 父View和子View滑动方向一致(系统无法知道让哪一层滑动,卡顿或者只有1层能滑动)
- 上面两种的嵌套
3.处理规则
- 场景1:用户左右滑动时,让父View拦截touch事件,上下滑动时,子View拦截/消耗touch事件;根据他们的特征解决滑动冲突。如何判断是竖直还是水平滑动?
- 场景2:无法根据滑动的角度,距离差(滑动规则),以及速度差来判断,这个时候一般能在业务上找到突破点
- 场景3:同样是从业务上找突破点
不管多复杂的滑动冲突,他们之间的区别仅仅是滑动规则不同而已。
/**
* 分析1:onUserInteraction()
* 作用:实现屏保功能
* 注:
* a. 该方法为空方法
* b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
*/
public void onUserInteraction() {
}
b. DecorView继承自FrameLayout,是所有界面的父类
* c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
如果viewgroup不拦截事件, 那么哪些子view可以接收到事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。
Activity
对点击事件的分发机制
ViewGroup
对点击事件的分发机制
View
对点击事件的分发机制
场景一 点击如图的空白区域
W/DemoSocketActivity: W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN
E/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN false
W/MyLinearLayout: onTouchEvent: ACTION_DOWN
E/MyLinearLayout: onTouchEvent: ACTION_DOWN false
E/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN false
W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWNfalse
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UP
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UPfalse
没有ACTION_UP事件是因为MyLinearLayout没处理,MyLinearLayout默认是不可点击的。
点击测试按钮
W/DemoSocketActivity: W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN
E/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN false
W/MyButton: dispatchTouchEvent: ACTION_DOWN
W/MyButton: onTouchEvent: ACTION_DOWN
E/MyButton: onTouchEvent: ACTION_DOWN true
E/MyButton: dispatchTouchEvent: ACTION_DOWN true
E/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN true
W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWNtrue
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UP
W/MyLinearLayout: dispatchTouchEvent: ACTION_UP
W/MyLinearLayout: onInterceptTouchEvent: ACTION_UP
E/MyLinearLayout: onInterceptTouchEvent: ACTION_UP false
W/MyButton: dispatchTouchEvent: ACTION_UP
W/MyButton: onTouchEvent: ACTION_UP
E/MyButton: onTouchEvent: ACTION_UP true
E/MyButton: dispatchTouchEvent: ACTION_UP true
E/MyLinearLayout: dispatchTouchEvent: ACTION_UP true
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UPtrue
onTouchEvent返回true是因为Button是可点击的
背诵版总结
从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件。一般情况下,事件列都是以DOWN
事件开始、UP
事件结束,中间有无数的MOVE事件。事件分发指的是 将点击事件(MotionEvent)传递到某个具体的View
& 处理的整个过程。
事件在哪些对象之间进行传递?
答:Activity、ViewGroup、View
事件分发过程主要由3个方法协作完成。
*dispatchTouchEvent() *:事件分发
onInterceptTouchEvent():拦截事件
和onTouchEvent():处理事件
分发从3个方面讲
1.Activity`对点击事件的分发机制
当一个点击事件发生时,事件最先传到Activity
的dispatchTouchEvent()
进行事件分发
2.ViewGroup`对点击事件的分发机制
先看自己是否拦截,默认可以认为不拦截。一旦拦截,那么这个事件序列(如果事件序列能够传递的话)都只能由它来处理,并且它的onInterceptTouchEvent不再调用,
遍历ViewGroup的所有子元素,判断子元素是否能够接收到点击事件。主要由:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。
3.View`对点击事件的分发机制
某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent 返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父容器去处理。
View的onTouchEvent默认都会消耗事件,除非它是不可点击的。
如何设置了有onTouchListener,且onTouch返回true表示消耗了事件,则不会调用onTouchEvent方法。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!