本篇文章主要结合面试中的问题,从以下几个方面分析Android事件分发,为方便理解,源码分析尽量点到为止,避免深入源码不可自拔。
- 通过重写
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
三个方法分析事件分发的流程 - 结合源码分析为什么是这样的
- 根据面试中常见的事件分发问题做一个回答
一、场景
activity 中放一个ViewGroup(红色的RelativeLayout),
ViewGroup中放一个View(灰色的TextView)
下文提到的ViewGroup指的是图中的RelativeLayout
下文提到的View指的是图中的TextView
二、重写方法进行分析
(假设你对事件分发的三个主要方法的意思已经清楚 )
Activity:
dispatchTouchEvent()
onTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
LogHelper.d("Activity->dispatchTouchEvent:" + getEventName(ev));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
LogHelper.d("Activity->onTouchEvent:" + getEventName(event));
return super.onTouchEvent(event);
}
private String getEventName(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return "ACTION_DOWN";
} else if (event.getAction() == MotionEvent.ACTION_UP) {
return "ACTION_UP";
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
return "ACTION_MOVE";
} else {
return event.getAction() + "";
}
}
ViewGroup:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
LogHelper.d("ViewGroup->dispatchTouchEvent:" + getEventName(ev));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
LogHelper.d("ViewGroup->onInterceptTouchEvent:" + getEventName(ev));
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
LogHelper.d("ViewGroup->onTouchEvent:" + getEventName(event));
return super.onTouchEvent(event);
}
private String getEventName(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return "ACTION_DOWN";
} else if (event.getAction() == MotionEvent.ACTION_UP) {
return "ACTION_UP";
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
return "ACTION_MOVE";
} else {
return event.getAction() + "";
}
}
View:
dispatchTouchEvent()
onTouchEvent()
(注意:没有onInterceptTouchEvent()方法)
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
LogHelper.d("View->dispatchTouchEvent:" + getEventName(event));
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
LogHelper.d("View->onTouchEvent:" + getEventName(event));
return super.onTouchEvent(event);
}
private String getEventName(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return "ACTION_DOWN";
} else if (event.getAction() == MotionEvent.ACTION_UP) {
return "ACTION_UP";
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
return "ACTION_MOVE";
} else if (event.getAction() ==MotionEvent.ACTION_CANCEL) {
return "ACTION_CANCEL";
} else {
return event.getAction() + "";
}
}
三、开始分析,打印日志
1. 默认返回值
点击Activity空白处
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP
由于点击空白处,事件到了Activity之后,没有传到里面的ViewGroup里,所以最终没人处理,就只能调用自己的onTouchEvent()方法,
点击ViewGroup外层
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP
首先是down事件,从Activity开始,传到ViewGroup,经过ViewGroup的dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent,ViewGroup默认没有消费事件,所以down事件还给Activity处理,既然你不处理down事件,那么后续的Move,Up事件不再询问ViewGroup,直接给Activity处理了。
点击View
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP
View没有消费down事件,所以down事件往上传递给ViewGroup,ViewGroup也不消费,所以最终交给Activity处理,之后的Move、Up事件都直接给Activity处理,没有调用ViewGroup的dispatchTouchEvent和onInterceptTouchEvent
2. ViewGroup、View消费事件的情况
View设置点击事件
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.d("view onClick");
}
});
看下日志
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP
D: view onClick
down、move、up事件都从上到下传递,由于View设置了点击监听,消费了事件,所以事件到View的onTouhEvent就结束了。
点击事件源码分析
看下设置点击事件发生了什么
//类:View
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public boolean onTouchEvent(MotionEvent event) {
...
switch(action) {
...
case MotionEvent.ACTION_UP:
...
performClick(); //ACTION_UP 的时候才响应点击事件
...
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//如果是可点击的状态,返回true,消费事件
return true;
}
}
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;
}
...
return result;
}
可以看到performClick是在onTouchEvent 的Up事件中回调的,也就是在事件的最尾部,然后如果控件是可点击的,就返回true消费事件。
ViewGroup设置点击事件
viewGroup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.d("ViewGroup onClick");
}
});
点击View(设置了点击事件)的情况日志如下
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP
D: view onClick
黑人问号,并没有调用ViewGroup的onClick???
因为ViewGroup默认是没有拦截事件的,看下源码
ViewGroup#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;
}
//默认返回false
return false;
}
事件传到View(可点击)的时候被消费掉了。我们要让ViewGroup处理点击事件要怎么办?
答案就是重写onInterceptTouchEvent 方法,返回true,拦截事件
public boolean onInterceptTouchEvent(MotionEvent ev) {
LogHelper.d("ViewGroup->onInterceptTouchEvent:" + getEventName(ev));
// return super.onInterceptTouchEvent(ev);
return true;
}
点击View,看下日志
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onTouchEvent:ACTION_UP
D: ViewGroup onClick
可以看到
onInterceptTouchEvent 返回true,拦截了事件,表示自己要处理,事件就不会向下传递
onInterceptTouchEvent 返回值意义知道了,那dispatchTouchEvent 的返回值代表什么呢?将ViewGroup的 dispatchTouchEvent 返回true看下结果
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
dispatchTouchEvent 返回true使得 onInterceptTouchEvent 没有被调用,事件就结束了,看下ViewGroup 的 dispatchTouchEvent 方法
ViewGroup#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 1、调用requestDisallowInterceptTouchEvent(true)会改变mGroupFlags,disallowIntercept会为true,
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//2、如果是down事件,调用onInterceptTouchEvent,询问是否拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
//3.如果子view调用 requestDisallowInterceptTouchEvent ,intercepted就为false,事件会
if (!canceled && !intercepted) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
dispatchTransformedTouchEvent
}
}
...
if (mFirstTouchTarget == null) {
// 4.反回值由这个方法决定
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
...
return handled;
}
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);
//没有子孩子则调用父类(View)的dispatchTouchEvent
//否则调用子view的dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
小结一下,ViewGroup#dispatchTouchEvent,内部是这样的:
1.如果是down事件,调用onInterceptTouchEvent,询问是否拦截
2.特殊情况,子view调用requestDisallowInterceptTouchEvent(true),将intercepted设置为false, 此时onInterceptTouchEvent失效,事件会传给子view,看注释1
3.常规情况,如果判断子view为空,就调用父类(View)的dispatchTouchEvent;如果有子view,那么返回值由子view的dispatchTouchEvent决定
看下View的dispatchTouchEvent。
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
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)) {
//1
result = true;
}
if (!result && onTouchEvent(event)) {
//2
result = true;
}
}
...
return result;
}
注释1:如果View设置了OnTouchListener并且onTouch返回true,则dispatchTouchEvent返回true,onTouchEvent没机会调用
注释2:如果没有设置onTouch返回true,那么dispatchTouchEvent的返回值由onTouchEvent决定。
3. onTouchEvent() 返回值不同的情况
前面已经分析了dispatchTouchEvent,onInterceptTouchEvent,剩下一个onTouchEvent
让View 的 onTouchEvent 返回true
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP
View是有设置点击事件的,onTouchEvent 返回true之后点击事件没有走,因为
performClick是在父类View的onTouchEvent的Up事件调用的,现在super.onTouchEvent没有调用,所以不会走onClick。
4. onTouch() 返回值不同的情况
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
LogHelper.d("child onTouch");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
return true;
// break;
}
/**
* onTouch 返回true 就不会走onClick,因为OnTouchEvet不会走
* 见源码 dispachTouchEvent()
* */
return false;
}
});
ACTION_UP返回true,看下结果
D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: child onTouch
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: child onTouch
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: child onTouch
可看到 onTouchEvent 的 ACTION_UP没有调用,原因就是因为 dispachTouchEvent 里面判断如果设置了TouchListener,先调用了onTouch,如果onTouch 返回true,就不会调用OnTouchEvent。忘记了吗,看下代码:
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
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;
}
...
}
到此我们对事件分发就有了一个大概的理解,接下来主要针对面试中的问题做一个分析。
三、面试中的事件分发问题
1. 说一下事件分发的流程
事件从Activity开始分发,看下Activity的dispatchTouchEvent
Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//交给window
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity先把事件交给Window,Window一个抽象类,唯一的实现类是PhoneWindow
PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return this.mDecor.superDispatchTouchEvent(event);
}
PhoneWindow把事件交给DecorView
DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
小结:DecorView是一个FrameLayout,所以事件就从Activity传到PhoneWindow再传到ViewGroup里了,然后就是ViewGroup的事件分发流程
ViewGroup主要三个方法:
分发(dispatchTouchEvent)、拦截(onInterceptTouchEvent)、处理(onTouchEvent)
View没有拦截(onInterceptTouchEvent)的方法,收到事件,dispatchTouchEvent肯定会被调用,返回true或者false代表是否自己要处理事件。
这里可能有疑问了,PhoneWindow是怎么跟Activity关联的,DecorView又是怎么跟Window关联的?
2.Activity、Window、DecorView的关系
简单来说就是Activity里有一个Window,Window里面有一个DecorView,我们通过setContentView添加的布局就是添加到DecorView里面的FrameLayout中。
以下是部分源码分析,加深理解
Activity#setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
我们在Activity的onCrate方法调用setContentView添加的布局最终是调用getWindow() 的setContentView
Activity#getWindow
public Window getWindow() {
return mWindow;
}
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//实例化
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}
void makeVisible() {
//1
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
//2
mDecor.setVisibility(View.VISIBLE);
}
可以看到,Activity内部有一个Window实例,是在attach方法实例化的,
注释1:在Activity可见的时候,通过WindowManager将DecorView添加到Window
注释2:DecorView设置可见,也就说明了DecorView是Activity根布局
PhoneWindow#setContentView
public void setContentView(View view, LayoutParams params) {
//1
if (this.mContentParent == null) {
this.installDecor();
} else if (!this.hasFeature(12)) {
this.mContentParent.removeAllViews();
}
if (this.hasFeature(12)) {
view.setLayoutParams(params);
Scene newScene = new Scene(this.mContentParent, view);
this.transitionTo(newScene);
} else {
//2
this.mContentParent.addView(view, params);
}
...
this.mContentParentExplicitlySet = true;
}
注释1:如果mContentParent空,则则调用installDecor
注释2:将我们的布局添加到mContentParent
PhoneWindow#installDecor
private void installDecor() {
this.mForceDecorInstall = false;
if (this.mDecor == null) {
//mDecor空则初始化
this.mDecor = this.generateDecor(-1);
...
} else {
//绑定Window
this.mDecor.setWindow(this);
}
if (this.mContentParent == null) {
//mContentParent 初始化,generateLayout里面根据不同主题mContentParent指向的内容会button
this.mContentParent = this.generateLayout(this.mDecor);
this.mDecor.makeOptionalFitsSystemWindows();
//decorContentParent 初始化
DecorContentParent decorContentParent = (DecorContentParent)this.mDecor.findViewById(16908823);
int transitionRes;
//标题栏设置
if (decorContentParent == null) {
this.mTitleView = (TextView)this.findViewById(16908310);
if (this.mTitleView != null) {
if ((this.getLocalFeatures() & 2) != 0) {
View titleContainer = this.findViewById(16909374);
if (titleContainer != null) {
titleContainer.setVisibility(8);
} else {
this.mTitleView.setVisibility(8);
}
this.mContentParent.setForeground((Drawable)null);
} else {
this.mTitleView.setText(this.mTitle);
}
}
}
}
小结:从上面分析,我们知道Activity、Window、DecorView之间的关系是:
Activity里有一个Window,Window里有一个DecorView,我们添加的布局就放在DecorView里的一个FrameLayout里。
3.ScrollView嵌套RecyclerView,如何解决滑动冲突?
RecyclerView 可以滑动的时候我们希望滑动的是RecyclerView,RecyclerView不能滑动的时候才去滑动ScrollView
滑动冲突的方案一般有两种
- 外部拦截法
- 内部拦截法
外部拦截
重写ScrollView的onInterceptTouchEven,在需要自己处理的时候返回true拦截事件,其它情况都不拦截
public boolean onInterceptTouchEvent(MotionEvent ev) {
//拦截条件
if (needIntercepter(ev)){
return true;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 外部拦截的条件
* @return
*/
private boolean needIntercept(MotionEvent event) {
if (mRecyclerView == null){
return true;
}
/**判断RecyclerView到达顶部或者底部,就拦截*/
// 测试view是否在点击范围内
/**g etRawX是以屏幕左上角为坐标做预案,获取X坐标轴上的值。*/
float x = event.getRawX();
float y = event.getRawY();
int[] locate = new int[2];
mRecyclerView.getLocationOnScreen(locate);
left = locate[0];
right = left + mRecyclerView.getWidth();
top = locate[1];
bottom = top + mRecyclerView.getHeight();
if (top > y || y > bottom) {
return true;
}
return false;
}
内部拦截法
1.RecyclerView 通过调用requestDisallowInterceptTouchEvent(true),请求父控件不要拦截我的事件
2.RecyclerView判断不需要的事件,调用requestDisallowInterceptTouchEvent(false),交给ScrollView去处理。
详细步骤
- ScrollView 需要重写 onInterceptTouchEvent,不拦截down事件,返回false,同时意味着自己的onTouchEvent的down事件永远不会调用到,所以手动调用onTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
/** 内部拦截法需要父布局不拦截 ACTION_DOWN ,否则所有事件都给父布局了*/
if (ev.getAction() == MotionEvent.ACTION_DOWN){
//手动调用onTouchEvent
onTouchEvent(ev);
return false;
}
return super.onInterceptTouchEvent(ev);
}
- RecyclerView分发到down事件的时候,调用requestDisallowInterceptTouchEvent(true),事件就会传过来。然后在move事件中判断如果不需要处理事件,就调用 requestDisallowInterceptTouchEvent(false),将事件交给ScrollView 处理,因为我们手动调用了ScrollView的onTouchEvent的down事件,所以对ScrollView来说,仍然是一个完整的事件序列。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN){
getParent().getParent().requestDisallowInterceptTouchEvent(true);
}else if (ev.getAction() == MotionEvent.ACTION_MOVE){
//向上滑动,且RecyclerView到底了
if (ev.getY() < lastY){
if (!canScrollVertically(1)){
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
//向下滑动,且RecyclerView到顶部了
}else if(ev.getY() > lastY){
if (!canScrollVertically(-1)){
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
lastY = ev.getY();
return super.dispatchTouchEvent(ev);
}
好了,本篇文章到此就结束了,总结一下:
是什么?
通过重写事件分发的三个方法,了解事件分发的大概流程为什么?
结合ViewGroup、View事件分发源码进行原理分析怎么做?
列举了面试中的几个问题,最后是事件冲突的解决办法。