(给
安卓开发精选
加星标)
转自:Ghost_Liu
https://juejin.im/post/5d8dbe79e51d4578282ce224
在我们平时开发中,总是会遇到滑动冲突。那么,如果要解决滑动冲突,首先就要求我们理解Android中一个非常重要的知识点:事件分析机制。
基础知识准备
Android UI层级
在了解事件分发机制之前,我们要知道Android布局层级。我们在写
Activity
的时候通常是通过
setContentView(int layoutResID)
方法设置布局,所以我们先从这个方法入手看一下布局层级。
public void setContentView (@LayoutRes int layoutResID ) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow ( ) {
return mWindow;
}
由上面的代码我们可以看到, Activity
的 setContentView
方法实际上是调用了 Window
的 setContentView
方法,让我们看一下 Window
对象。
/**
* 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.
*
* The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
....
}
从注释中我们可以看到
Window
是一个抽象类了,并且它的唯一一个子类是
PhoneWindow
,也就是说上面的方法调用实际上是调用了PhoneWindow的setContent方法,我们来看一下。
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null ) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true ;
}
private void installDecor() {
if (mDecor == null ) {
mDecor = generateDecor(-1 );
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true );
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0 ) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this );
}
if (mContentParent == null ) {
mContentParent = generateLayout(mDecor);
...
}
}
在这里我只挑出了主要的代码,第一段代码中我们可以看到通过
installDecor
方法去初始化
DecorView
,通过查看源码我们可以知道
DecorView
实际上是一个
Framelayout
。
通过第二段代码的1处我们可以看到,其调用了
generateLayout
方法,我们来来看一下这里面做了什么。
protected ViewGroup generateLayout (DecorView decor ) {
...
if (a.getBoolean(R.styleable.Window_windowNoTitle, false )) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false )) {
requestFeature(FEATURE_ACTION_BAR);
}
...
if ((features & (1 << FEATURE_NO_TITLE)) == 0 ) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true );
layoutResource = res.resourceId;
} else if ((features & (1
<< FEATURE_ACTION_BAR)) != 0 ) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
}
...
}
上面的代码省略了很多,我们从代码中看到
generateLayout
方法中会根据不同的
Future
来给
layoutResource
设置布局。
我们看一下注释1处的布局文件。
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android"
android:orientation ="vertical"
android:fitsSystemWindows ="true" >
<ViewStub android:id ="@+id/action_mode_bar_stub"
android:inflatedId ="@+id/action_mode_bar"
android:layout ="@layout/action_mode_bar"
android:layout_width ="match_parent"
android:layout_height ="wrap_content"
android:theme ="?attr/actionBarTheme" />
<FrameLayout
android:layout_width ="match_parent"
android:layout_height ="?android:attr/windowTitleSize"
style ="?android:attr/windowTitleBackgroundStyle" >
<TextView android:id ="@android:id/title"
style ="?android:attr/windowTitleStyle"
android:background ="@null"
android:fadingEdge ="horizontal"
android:gravity ="center_vertical"
android:layout_width ="match_parent"
android:layout_height ="match_parent" />
FrameLayout >
<FrameLayout android:id ="@android:id/content"
android:layout_width ="match_parent"
android:layout_height ="0dip"
android:layout_weight ="1"
android:foregroundGravity ="fill_horizontal|top"
android:foreground ="?android:attr/windowContentOverlay" />
LinearLayout >
从布局文件中我们看到三个部分
ViewStub:用来显示ActionBar
第一个FrameLayout:用来显示标题
第二个FrameLayout:用来显示内容
通过上面的源码我们可以看出:
Activity中包含一个Window对象,这对象是由PhoneWindow来实现。
PhoneWindow中又包含一个DecorView,并将其作为跟布局。
这个DecorView又分成两个部分,我们平时设置布局时其实是将布局展示在Content区域。
因为DecorView是继承自FrameLayout,而FrameLayout又继承自ViewGroup。
所以我们可以将事件的流向简单的表示出来:
Activity->ViewGroup->View。
事件概念
提了很多次事件,事件的具体概念是什么?
其实,这里所说的事件,是指当手指从触碰到手机屏幕到离开手指屏幕所产生的一系列Touch事件,其中也包括手指在手指在屏幕上滑动所对应的操作。
这些Touch事件被封装到一个MotionEvent对象中,接下来让我们看一下这个对象具体是做什么的。
实际上,事件分发机制处理的对象是手指操作屏幕时所产生的一些列
MotionEvent
对象。
MotionEvent事件分类和对应含义
事件分类
对应含义
MotionEvent.ACTION_DOWN
A pressed gesture has started(一个按压动作已经开始了)
MotionEvent.ACTION_MOVE
A change has happened between ACTION_DOWN and ACTION_UP(在点击和抬起事件中间的一些列操作)
MotionEvent.ACTION_UP
A pressed gesture has finished(一个按压动作已经结束)
MotionEvent.ACTION_CANCEL
A movement has happened outside of the normal bounds of the UI element(在滑动的过程中超出边界)
上面的表格中只列举了开发中最常用的几种事件类型,其实还有很多事件类型,有兴趣的小伙伴可以自行查看。
MotionEvent对象中不仅封装了事件类型,同时封装了手指在操作过程中的坐标。
涉及到的方法
事件分发机制中主要涉及到三个方法:
dispatchTouchEvent()、onTouchEvent()和onInterceptTouchEvent()。
方法名
方法的含义
dispatchTouchEvent()
被用来事件分发,如果事件传递到当前的View,该方法一定会被调用,默认返回false
onInterceptTouchEvent()
该方法被用来判断是否拦截某个事件,在dispatchTouchEvent方法中调用 ,返回值表示是否拦截当前事件,通常存在于ViewGroup,一般View中没有该方法
onTouchEvent()
该方法被用来处理点击事件,返回值表示是否消耗当前的事件,如果不进行消耗,该系事件序列将不会被接受
小结
从上面的分析中我们可以进行一下小结:
当手指触摸屏幕产生的一些列操作会被封装在MotionEvent中,通过Activity、ViewGroup和View调用一系列方法,找到事件的接受者并处理该事件的一个过程。
源码分析
从上面的分析我们已经知道事件的大致流向:
Activity
->
ViewGroup
->
View
,接下来让我们从源码的角度逐个分析。
Activity事件分发
既然事件最初是从Activity中进行传递,所以我们首先到
Activity
中找到
dispatchTouchEvent()
方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true ;
}
return onTouchEvent(ev);
}
注释1处的
getWindow()
方法获取的是
Window
对象,但是
Window
是一个抽象类,我们看一下它的唯一子类
PhoneWindow
的
superDispatchTouchEvent()
方法
PhoneWindow.superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent (MotionEvent event ) {
return mDecor.superDispatchTouchEvent(event );
}
DecorView.superDispatchTouchEvent
public boolean superDispatchTouchEvent (MotionEvent event ) {
return super.dispatchTouchEvent(event );
}
我们看到在
PhoneWindow
的
superDispatchTouchEvent()
方法实际上调用了
DecorView
的
superDispatchTouchEvent()
方法,这个方法内部调用了
super.dispatchTouchEvent()
。
在基础知识准备中我们知道
DecorView
是继承自
Framelayout
,在
Framelayout
中是没有
dispatchTouchEvent()
的方法,但是
Framelayout
是继承自
ViewGroup
。
所以,最后还是调用到了
ViewGroup
的
dispatchTouchEvent()
,自此事件从
Activity
传递到了
ViewGroup
。
ViewGroup事件分发
既然事件已经从
Activity
传递到了
ViewGroup
,让我们看一下
ViewGroup
具体做了什么处理。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false ;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
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);
} else {
intercepted = false ;
}
} else {
intercepted = true ;
}
final boolean canceled = resetCancelNextUpFlag(this ) || actionMasked == MotionEvent.ACTION_CANCEL;
TouchTarget newTouchTarget = null ;
boolean alreadyDispatchedToNewTouchTarget = false ;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0 ) {
...
for (int i = childrenCount - 1 ; i >= 0 ; i--) {
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null )) {
ev.setTargetAccessibilityFocus(false );
continue ;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null ) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break ;
}
if (dispatchTransformedTouchEvent(ev, false , child, idBitsToAssign)) {
...
}
}
}
}
}
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 )) {
return ;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null ) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
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 ;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
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;
}
...
}
经过一番查找,终于把源码摘清。我这里是Android9.0的源码,如果跟各位的有出入,以各位手上的源码为准。闲话少说,直接看源码中都做了什么。
上面的代码共有五处注释:
注释1:这部分代码主要是清除事件接收目标同时初始化事件状态。
我们看到先判断是否是ACTION_DOWN事件,也就是手指按下的事件。
如果是,则调清除之前的事件接收目标,同时也初始化了点击状态。
这也很好理解,当触发ACTION_DOWN时说明用户重新对手机做了操作,
有可能点击了不同的控件,之前事件接收目标及其点击状态都应该重置掉。
注释2 :这部分代码主要是用来判断是否要拦截事件。
首先判断了是否是ACTION_DOWN事件,或者事件接收目标是否为空。如果这两个条件都不成立,将会标识拦截。
在判断条件内我们还看到了FLAG_DISALLOW_INTERCEPT标识,这个标识表示子View是否允许父控件拦截事件,
当子View调用requestDisallowInterceptTouchEvent(boolean )方法时设置的。
只有当requestDisallowInterceptTouchEvent(boolean )传入值为true 时,
表示不允许父控件拦截(上面第二段代码)。disallowIntercept默认值为false ,也就是子控件没有设置禁止父控件拦截事件。
这时,会调用onInterceptTouchEvent()方法(第三段代码).
注释3:这段代码主要来判断是否发生了ACTION_CANCEL事件。
这个事件在基础知识准备中已经有所说明,这里就不再详细讲解。
注释4:通过循环遍历找到子View。
首先是通过倒序循环遍历的方式找到每一个子View,为什么是倒序呢,。。。。
找到子View之后会调用canViewReceivePointerEvents()和isTransformedTouchPointInView()方法来判断子View的状态。
这里边包括是否可见、是否正在播放动画、点击事件的坐标是否落在子View的显示范围内。
注释5 :这部分代码是ViewGroup事件分发的核心。
这里会调用dispatchTransformedTouchEvent()方法(第四段代码),在这个方法中我们可以看到,回去判断是否有子View,
如果有,继续调用子View的dispatchTouchEvent()方法;如果没有,则去调用super .dispatchTouchEvent()方法。
我们都知道ViewGroup是继承自View的,所以接下来让我们看一下View的dispatchTouchEvent()方法。
View事件分发
public boolean dispatchTouchEvent (MotionEvent event ) {
...
boolean result = false ;
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 ;
}
}
...
return result;
}
从上面的代码中我们看到,首先会获取
OnTouchListener
信息,也会调用
OnTouchListener
的
onTouch()
方法。
如果
OnTouchListener
不为空,并且
onTouch()
方法返回true之后,就不会再执行
onTouchEvent()
方法,否则的话还要去执行
onTouchEvent()
方法。
从中我们也可以看出
OnTouchListener
中的
onTouch()
方法优先级要高于
onTouchEvent(MotionEvent)
方法。
onTouchEvent(MotionEvent)
View
的事件分发过程中调用了
onTouchEvent(MotionEvent)
方法,我们来看一下。
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0 ) {
setPressed(false );
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false ;
mHasPerformedLongPress = false ;
mIgnoreNextUpEvent = false ;
break ;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0 ;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
if (!focusTaken) {
if (mPerformClick == null ) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
break ;
case MotionEvent.ACTION_DOWN:
if (!clickable) {
checkForLongClick(0 , x, y);
break ;
}
if (performButtonActionOnTouchDown(event)) {
break ;
}
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null ) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true , x, y);
checkForLongClick(0 , x, y);
}
break ;
...
}
return true ;
}
return false ;
}
private boolean performClickInternal() {
...
return 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 ;
}
...
return result;
}
注释1处检查了该
View
是否是可点击或者是长按点击,并记录下来;
注释2处我们从英文注释中可以看到,虽然一个
View
是一个
disabled
状态,但是只要是
clickable
任然会消费事件,只是不做任何反馈而已。
之后会根据事件类型进行判断,当处于
ACTION_UP
也就是说当手指离开时,会触发
performClickInternal
方法,这个方法去执行内部点击,具体调用方法在最后两段代码中。