UI界面组成
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
正如API所讲,Window是一个抽象基类,是WindowManager最顶层的试图,它负责背景、Title之类的标准的UI元素,整个Android系统中PhoneWindow是Window的唯一实现类。
在Activity类中有一个Window类的引用mWindow,并且可以通过getWindow()分发来获取这个Window对象,在attach()方法里面对mWindow进行初始化,如下:
final void attach(......) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
......
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
......
}
mWindow通过PhoneWindow的构造方法创建对象。
当调用了attach方法后就会调用onCreate方法了,在onCreate方法中我们首先要调用setContentView方法,如下:
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
方法定位到了Window的setContentView,那就看看PhoneWindow类的setContentView的实现。该类聚合了两个View对象,一个是mDecor,该对象是FrameLayout的子类;另一个是mContentParent对象;
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
......
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这个方法先判断mContentParent是否为null,如果为null的情况下调用installDecor()方法。该方法里面主要做了哪些事情呢?
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
......
private TextView mTitleView;
......
private CharSequence mTitle = null;
......
}
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
......
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
......
} else {
mTitleView = (TextView) findViewById(R.id.title);
if (mTitleView != null) {
..........
}
}
........
}
}
在installDecor()方法中主要完成了mDecor以及mContentParent初始化工作,为Window添加DecorView界面。
该方法先调用generateDecor()方法对mDecor来进行初始化;然后初始化mContentParent,在generateLayout方法里面有这么一句话可以看出mDecor跟mContentParent的关系:
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
.............
return contentParent;
而这个findViewById方法是父类Window提供的。
public View findViewById(int id) {
return getDecorView().findViewById(id);
}
可以看出来的是从mDecor获取一个View对象来赋值给mContentParent,所以mDecor和mContentParent是父View与子View关系。mContentParent有什么用么?继续观察 setContentView方法:
mLayoutInflater.inflate(layoutResID, mContentParent);
也就是说mContentParent这View是作为你的xml文件的父View,xml通过解析生成的View对象被作为mContentParent的子View通过addView添加到mContentParent上来;而由于mConentParent是mDecor的子View,所以我们说这个页面的父View或者根View就是mDecor。
最后简单的用图表示一下Activity、PhoneWindow和DecorView的关系,如下:
事件分发概述
1.当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)
Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。
主要发生的Touch事件有四种,如下:
- MotionEvent.ACTION_DOWN:按下View(所有事件的开始);
- MotionEvent.ACTION_MOVE:滑动View;
- MotionEvent.ACTION_CANCEL:非人为原因结束本次事件;
- MotionEvent.ACTION_UP:抬起View(与ACTION_DOWN对应)。
2. 事件分发的本质
当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理,这个事件传递的过程就是分发过程。因此,Android事件分发机制的本质是要解决:点击事件由哪个对象发出,经过哪些对象,最终到达哪个对象并得到处理。
3. 事件分发过程有哪些方法协作完成?
事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成。
-
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前的View的onTouchevent和下级的dispatchTouchEvent方法的影响,表示是否消耗当前事件。 -
public boolean onInterceptTouchEvent(MotionEvent ev)
用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件的序列中,此方法不会被再次调用,返回值表示是否拦截当前事件。 public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过上面的伪代码,我们可以大致的了解点击事件的传递规则:
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,首先调用dispatchTouchEvent(),如果它的onInterceptTouchEvent()返回true就是拦截当前事件,事件交给ViewGroup处理;如果返回false,表示不拦截当前事件,继续传递给它的子元素,接着子元素的dispatchTouchEvent()就会被调用,如此反复直到事件结束。
当一个View需要处理事件时,如果设置了OnTouchListener
,那么OnTouchListener
中的onTouch
方法会被回调。事件如何处理还要看onTouch
的返回值,如果返回false,则当前View的onTouchEvent
会被调用;如果返回true,那么onTouchEvent
将不会被调用。
4. 事件在哪些对象间传递?
Android的UI界面是由Activity、ViewGroup、View及其派生类组成。View是所有UI组件的基类。ViewGroup是容纳UI组件的容器,即一组View的集合(包含很多子View和子ViewGroup)。
因此,一个点击事件产生后,传递顺序是:Activity(Window)->ViewGroup->View。
1)android对事件分发的顺序为:Activity-->PhoneWindow->DecorView->yourView;
5. 结论
- 正常情况下,一个事件序列只能被一个View拦截并且消耗。一个事件序列中的事件不能分别由两个View同时处理,但是一个View可以将本该自己处理的是将通过onTouchEvent强行传递给其它View处理。
- 一个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再次调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchevent返回了false),那么同一事件序列中的其它事件都不会再交给它来处理,并且事件将交由它的父元素去处理。
- 如果View不消耗除ACTION_DOWN意外的其他事件,那么这个点击事件会消失,此时父元素的onTouchevent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件,View没有onInterceptTouchEvent方法。
- View的onTouchevent默认都会消耗事件,除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable都为false,而clickable则分情况,一般Button默认为true,TextView默认为false。
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick()会发生的前提是View是可以点击的,并且收到了down和up的事件。
- 事件传递过程由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过
requestDisallowInterceptTouchEvent
方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN除外。
事件分发机制方法及流程
Activity事件分发机制
源码分析
当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
.........
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//空方法,忽略
}
if (getWindow().superDispatchTouchEvent(ev)) {//把事件交给PhoneWindow处理
return true;//表明整个事件到此结束,处理完毕
}
return onTouchEvent(ev);//说明该事件在所有子view中没有处理,由Activity自己处理
}
在dispatchTouchEvent()方法中,
先把事件分发给Window,由于Window类是抽象类,且PhoneWindow是Window类的唯一实现类。所以这里调用
PhoneWindow的superDispatchTouchEvent()方法,如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);//mDecor是DecorView的实例,DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}
很简单,就是直接把该事件
传递给DecorView,这个view是所有视图的根试图,Activity界面中你能见到的各个View都是DecorView的子View。
到此为止事件已经分发到View上面,View获取到事件后有两个选择:处理和不处理该事件。如果处理该事件,那事件就不会继续向其子View分发下去;否则就继续分发下去交给子View对该事件做同样的判断,其实就是个递归的过程。
继续向下跟踪代码,上面调用到DecorView类的superDispatchTouchEvent()方法,如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView继承自FrameLayout,FrameLayout继承自ViewGroup。上面的代码通过super.dispatchTouchEvent(ev)调用了DecorView的父类FrameLayout,FrameLayout类没有重写dispatchTouchEvent()方法,而是由它的
父类ViewGroup实现。
所以getWindow.superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event)方法。
我们再回头看下Activity的dispatchTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
.........
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//空方法,忽略
}
if (getWindow().superDispatchTouchEvent(ev)) {//把事件交给PhoneWindow处理
return true;//表明整个事件到此结束,处理完毕
}
return onTouchEvent(ev);//说明该事件在所有子view中没有处理,由Activity自己处理
}
由于一般事件都是从ACTION_DOWN开始,基本上都会进入getWindow.superDispatchTouchEvent()方法,返回true。所以,
执行Activity.dispatchTouchEvent()实际上是执行了ViewGroup.dispatchTouchEvent()。这样事件就从Activity传递到了ViewGroup。
执行流程
当一个点击事件发生时,调用顺序如下:
1. 事件最先传到Activity的dispatchTouchEvent()方法中进行事件分发;
2. 调用Window类的实现类PhoneWindow的superDispatchTouchEvent()方法;
3. 调用DecorView的superDispatchTouchEvent()方法;
4. 最终调用DecorView父类ViewGroup的dispatchTouchEvent();如果ViewGroup的dispatchTouchEvent()返回true,就不执行Activity的onTouchEvent()方法;如果返回false,就执行Activity自己的onTouchEvent()方法。
疑问
那么,问题来了:ViewGroup的dispatchTouchEvent()方法什么时候返回true,什么时候返回false呢?
带着这个问题,我们继续分析ViewGroup的事件分发机制。
ViewGroup事件分发机制
源码分析
public boolean dispatchTouchEvent(MotionEvent ev) {
//1.用于测试,可直接忽略
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
//2.[辅助功能] 事件将会第一个派发给开启了accessibility focused的view
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
//3.表示窗口是否为模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),如果是窗口则表示不希望处理改事件(如dialog后的窗口)
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
//4.过滤字段的最后8bit,也就是指只关心是ACTION_DOWN、ACTION_UP等事件,而不关心是哪个手指引起的。
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//5.初始化相关状态
//清空mFirstTouchTarget链表("接受触摸事件的View"所组成的单链表),并设置mFirstTouchTarget为null。
//清空mGroupFlags的FLAG_DISALLOW_INTERCEPT标记,如果设置了FLAG_DISALLOW_INTERCEPT,ViewGroup对触摸事件进行拦截。
//清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN标记,作用是将下一个时间变为Cancel
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//6.如果为DOWN事件,或者mFirstTouchTarget为null(那么事件直接给到自己),就没必要执行拦截。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//查看是否设置了,禁止拦截的标记
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//ViewGroup的onInterceptTouchEvent不执行拦截,除非子类重写了该方法(如listview)
intercepted = onInterceptTouchEvent(ev);//false
//仅仅是避免action被篡改过。
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 intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
//查看是否被标记了PFLAG_CANCEL_NEXT_UP_EVENT 或者 当前是一个Cancel事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
//比如我们多个手指放到了屏幕上,是否要将第二个手指的事件分发下去
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
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.清除Targets中相应的pointer ids
removePointersFromTouchTargets(idBitsToAssign);
//遍历所有的child,将事件派发下去
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.
//以 1)Z轴(5.0系统引入) 2)draw的顺序 进行排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//可以理解为,是否按照draw的顺序(因为,buildOrderedChildList在都没有设置Z的情况下返回null)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//这里两端代码,简单的理解根据不同的排列选项(1、view添加到 2、view的draw顺序 3、viewZ 轴顺序)
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.如果存在开启了AccessibilityFocus 的view
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
//如果正是当前的childView开启了AccessibilityFocus,直接将i指向最后一个元素
//和 break的区别是,还将执行后面的代码,但是不会再进行循环了
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//canViewReceivePointerEvents判断child是否为visiable或者是否有动画
//isTransformedTouchPointInView 判断x, y是否在view的区域内(如果是执行了补间动画 则x,y会通过获取的matrix变换值
//换算当相应的区域,这也是为什么补间动画的触发区域不随着动画而改变)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//getTouchTarget 查找child是否已经记录在mFirstTouchTarget这个单链表中
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);
//简单的理解,dispatchTransformedTouchEvent就是将相应的事件传递下去
//不过需要注意一点的就是,event被传递给child的时候将会做相应偏移,如下
//final float offsetX = mScrollX - child.mLeft;
//final float offsetY = mScrollY - child.mTop;
//event.offsetLocation(offsetX, offsetY);
//为什么要做偏移呢?因为event的getX得到的值是childView到parentView边境的距离,是一个相对值
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
//找到childIndex所代表的child的最原始的index【?】看代码,children和mChildren指向同一链表
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//将相应该事件的child包装成一个Target,添加到mFirstTouchTarget的链表中
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();
}
//如果没有child相应该事件,则将此事件交给最近加入的target
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;
}
}
}
//mFirstTouchTarget == null表示,没有能相应该事件的child,那么就调用父类(也就是View)的dispatchTouchEvent
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//表示在Down事件处理中,已经将这个事件交给newTouchTarget处理过了,就不重复处理了
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//再次判定是否需要cancel(被标记为cancel 或者 事件被拦截)
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//问:难道看到这里你们会不会产生一个疑问,如果parent在ACTION_MOVE过程中拦截了该事件,哪里还处理呢?
//答:如果拦截了该事件,还是需要自身 dispatchTransformedTouchEvent 函数将事件交个自己的onTouchEvent
//此外dispatchTransformedTouchEvent完成上述操作需要一个条件,也就是child形参数为null
//问:那么怎么为null呢?
//答:往上面看10几行,不就是了吗?
//那么如何达到,其实条件就是 mFirstTouchTarget == null, 请看下面的分析
//如果intercepted == true的情况下, cancelChild == true, predecessor == null
//从而使得mFirstTouchTarget 一直 -> next,当target遍历到最后的时候,next == null,从而使得mFirstTouchTarget == null。
//问: 稍等,这里仅仅做了将mFirstTouchTarget 设置了为null,那么如何派发给自己的onTouchEvent呢?
//这个只能等下一个事件过来了
//结论【事件拦截,拦截了该事件,并没有将本次这个事件传递给自身的onTouchEvent,而需要等到下次】
//问:如何验证
//答:重新FrameLayout的 onInterceptTouchEvent 和 onTouchEvent 将相应的event.getEventTime打印出来,
//将会发现拦截的事件和传递到onTouchEvent的时间不是一个时间。
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// cancel ACTION_UP ACTION_HOVER_MOVE(表示鼠标滑动)等,清理状态
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
// 调试使用,可以忽略
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
ViewGroup的dispatchTouchEvent()方法比较繁琐,下面提炼下关键点。
1. 如果收到的ACTION_DOWN,那么清除所有和时间相关的状态。
2. 判断是否拦截事件。
(1).如果在子View中调用了getParent().requestDisallowInterceptTouchEvent(true),那么将不会拦截该事件。
问:有什么用呢?
答:在listView或其他srcollView都会在onInterceptTouchEvent进行拦截判断,只要距离或者速度达到一定要求,就会拦截该事件。那就做不到一view在listView中上下滑动。那么如何才能实现这个需求?那就需要使用getParent().requestDisallowInterceptTouchEvent(true)不要拦截该事件,将事件传递下去。
(2).onInterceptTouchEvent()函数这是一个很重要的函数,是在当前的事件达到一定要求之后才进行拦截处理。有了这个,listView才能实现item可点击,而item上下滑动。onInterceptTouchEvent()源码分析如下:
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;
}
这里返回false,表示不拦截,允许事件继续往下传递(向子View传递)。如果返回true,表示拦截事件,即自己处理该事件(执行自己的onTouchEvent()分发),事件就不会向下传递。
3.如果事件是ACTION_DOWN,并且没有被拦截,将会执行向子View的分发处理。
(1).分发顺序
5.0之前,基本按照子View被添加的顺序。
5.0之后,还需要考虑Z轴、Drawing顺序、添加的顺序。重点关注Z轴顺序,值最大的将会最优先,这是得到所有的子view,那么如何判断是否点击到子View区域内呢?
(2).判断子View是否可见或者存在动画,否则不处理。
(3).事件是否点击到子View的区域内。
(4).派发该事件给子View,看子View是否要处理。
(5).将相应该事件的子view加入到mFirstTouchTarget的单链表中(接下来的事件都会直接派发给其中的view)
4. 如果没有一个子View响应Action_DOWN 怎么办?
那么直接将事件交个父类View的dispatchTouchEvent(),再根据条件分发给自己。
5. 其他事件如 ACTION_MOVE, ACTION_UP, ACTION_CANCEL,则直接照着mFirstTouchTarget链表派发就行。
最后:
讲讲拦截,如果ViewGrop拦截该事件,那么将立马给之前相应事件的子View派发一个ACTION_CANCEL事件。另外,还需要注意的是,如果ViewGrop拦截该事件,并不会立马将该事件给自己的onTouchEvent,只有等下次事件过来才可能,所以在onInterceptTouchEvent()中需要对ACTION_UP和ACTION_CANCEL也做相应处理。
6. 在ViewGroup中,分发事件调用的是dispatchTransformedTouchEvent(),在这个方法内部调用了子视图的dispatchTouchEvent()。
结论
1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View。
2. 在ViewGroup中通过onInterceptTouchEvent()对事件传递进行拦截。
3. ViewGroup的onInterceptTouchEvent()返回false,不拦截事件,允许事件继续向子View传递;子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
View事件分发机制
源码分析
View中dispatchTouchEvent()的源码分析
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
//判断了当前事件是否可以获取焦点,如果不能获取焦点或者找不到一个view,直接返回false
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
//用于测试,直接忽略
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
//过滤字段的最后8bit,也就是指只关心是ACTION_DOWN、ACTION_UP等事件,而不关心是哪个手指引起的。
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
android.util.SeempLog.record(3);
// Defensive cleanup for new gesture
stopNestedScroll();//结束嵌套滑动机制
}
//onFilterTouchEventForSecurity判断View是否被遮住
if (onFilterTouchEventForSecurity(event)) {//安全模式下,忽略和这个事件;否则返回true
//判断View是否是enabled,以及判断事件是否是滚动条拖动
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//li.mOnTouchListener是否为null,在于view是否调用了setOnTouchListener(),设置了就不为null
//li.mOnTouchListener.onTouch(),主要判断onTouch的返回值,true表示消费了事件;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上面if条件满足,则不执行这里的onTouchEvent();
//这里可以得出一个结论:在dispatchTouchEvent方法里先执行onTouch()方法
//如果没有设置OnTouchListener,或者onTouch返回false,或者view不是enabled,执行这里的onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
.........
return result;
}
根据代码分析,我们知道ListenerInfo不为空(见下面getListenerInfo方法),同时View调用了setOnTouchListener(),则li.mOnTouchListener不为空,执行OnTouchListener的onTouch方法返回true,则直接返回true,不在执行自身的onTouchEvent响应事件。
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
所以onTouchEvent的执行与否跟onTouch的返回值有很大关系,有时候我们在对view的onTouch里面返回true,会发现这个view的点击事件没效果,点击事件是不是就在onTouchEvent方法里面?我们接着分析,不过我们可以得出一个结论,就是在
dispatchTouchEvent方法方法里面首先执行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();
//如果view是DISABLED状态,只要满足下面三种条件之一那么这个view虽然被禁用了,还是会消费这个事件,只是不响应它们而已。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {//如果view设置了代理,则执行mTouchDelegate的onTouchEvent方法
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//一个view是enable且满足三个状态之一则进入switch判断中去,反之则返回false
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//判断了不是在一个滚动操作的容器中
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
//当前view是否能够获取焦点,触摸是否能够获得焦点,当前view还没有获取焦点,就请求获取一个焦点
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);//设置按下状态
}
//判断用户是否进行了长按,如果没有移除相关检测
//mHasPerformedLongPress长按事件标志位,长按事件发生后会将这个标志位设置为true,不是则移除掉长按的延迟消息。
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
//判断有没有重新获取焦点,如果还没有重新获得焦点,那么就已经是一个按下的状态了
if (!focusTaken) {
//使用post到主线程中执行一个performClick的Runnable,而不是直接执行一个点击事件
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();//见下面源码,这里会执行onClick方法
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {//按下的效果显示时间
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration()/*64ms*/);
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;//将设置的按下的状态设置为false
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;//设置长按标记位
//一般的设备都是false,这是一个处理鼠标右键的事件
if (performButtonActionOnTouchDown(event)) {
break;
}
//判断当前view是否在一个滚动的view容器内,避免把滑动当成一次点击事件
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();//准备点击的标记位
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//发送一个延迟100ms的消息确定用户是要滚动还是点击
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//如果不是在一个滚动容器内,则调用setPressed(true, x, y)设置一个按下状态,然后再检查长按状态;
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//移除各种延迟发送的消息
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
我们看下上面方法中用到的performClick()方法,如下:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
//view调用了setOnClickListener时,mOnClickListener不为null
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);//执行Listener的onClick方法,消费了事件
result = true;
//至此,也就知道了OnClick方法是在onTouchEvent()里面执行的
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
点击事件是在onTouchEvent方法中的ACTION_UP中执行一个click事件,并且我们每次执行一个action,只有前一个action返回true,才会执行下一个action,因为前一个action返回了false,那么
dispatchTouchEvent
将不会派发下一次事件。