本篇文章主要結(jié)合面試中的問題,從以下幾個方面分析Android事件分發(fā),為方便理解,源碼分析盡量點到為止,避免深入源碼不可自拔。
- 通過重寫
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三個方法分析事件分發(fā)的流程 - 結(jié)合源碼分析為什么是這樣的
- 根據(jù)面試中常見的事件分發(fā)問題做一個回答

一、場景
activity 中放一個ViewGroup(紅色的RelativeLayout),
ViewGroup中放一個View(灰色的TextView)
下文提到的ViewGroup指的是圖中的RelativeLayout
下文提到的View指的是圖中的TextView

二、重寫方法進行分析
(假設(shè)你對事件分發(fā)的三個主要方法的意思已經(jīng)清楚 )
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里,所以最終沒人處理,就只能調(diào)用自己的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,經(jīng)過ViewGroup的dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent,ViewGroup默認沒有消費事件,所以down事件還給Activity處理,既然你不處理down事件,那么后續(xù)的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處理,沒有調(diào)用ViewGroup的dispatchTouchEvent和onInterceptTouchEvent
2. ViewGroup、View消費事件的情況
View設(shè)置點擊事件
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設(shè)置了點擊監(jiān)聽,消費了事件,所以事件到View的onTouhEvent就結(jié)束了。
點擊事件源碼分析
看下設(shè)置點擊事件發(fā)生了什么
//類: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 的時候才響應(yīng)點擊事件
...
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//如果是可點擊的狀態(tài),返回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事件中回調(diào)的,也就是在事件的最尾部,然后如果控件是可點擊的,就返回true消費事件。
ViewGroup設(shè)置點擊事件
viewGroup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.d("ViewGroup onClick");
}
});
點擊View(設(shè)置了點擊事件)的情況日志如下
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
黑人問號,并沒有調(diào)用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看下結(jié)果
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 沒有被調(diào)用,事件就結(jié)束了,看下ViewGroup 的 dispatchTouchEvent 方法
ViewGroup#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 1、調(diào)用requestDisallowInterceptTouchEvent(true)會改變mGroupFlags,disallowIntercept會為true,
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//2、如果是down事件,調(diào)用onInterceptTouchEvent,詢問是否攔截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
//3.如果子view調(diào)用 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);
//沒有子孩子則調(diào)用父類(View)的dispatchTouchEvent
//否則調(diào)用子view的dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
小結(jié)一下,ViewGroup#dispatchTouchEvent,內(nèi)部是這樣的:
1.如果是down事件,調(diào)用onInterceptTouchEvent,詢問是否攔截
2.特殊情況,子view調(diào)用requestDisallowInterceptTouchEvent(true),將intercepted設(shè)置為false, 此時onInterceptTouchEvent失效,事件會傳給子view,看注釋1
3.常規(guī)情況,如果判斷子view為空,就調(diào)用父類(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設(shè)置了OnTouchListener并且onTouch返回true,則dispatchTouchEvent返回true,onTouchEvent沒機會調(diào)用
注釋2:如果沒有設(shè)置onTouch返回true,那么dispatchTouchEvent的返回值由onTouchEvent決定。
3. onTouchEvent() 返回值不同的情況
前面已經(jīng)分析了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是有設(shè)置點擊事件的,onTouchEvent 返回true之后點擊事件沒有走,因為
performClick是在父類View的onTouchEvent的Up事件調(diào)用的,現(xiàn)在super.onTouchEvent沒有調(diào)用,所以不會走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,看下結(jié)果
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沒有調(diào)用,原因就是因為 dispachTouchEvent 里面判斷如果設(shè)置了TouchListener,先調(diào)用了onTouch,如果onTouch 返回true,就不會調(diào)用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;
}
...
}
到此我們對事件分發(fā)就有了一個大概的理解,接下來主要針對面試中的問題做一個分析。
三、面試中的事件分發(fā)問題
1. 說一下事件分發(fā)的流程
事件從Activity開始分發(fā),看下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一個抽象類,唯一的實現(xiàn)類是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);
}
小結(jié):DecorView是一個FrameLayout,所以事件就從Activity傳到PhoneWindow再傳到ViewGroup里了,然后就是ViewGroup的事件分發(fā)流程
ViewGroup主要三個方法:
分發(fā)(dispatchTouchEvent)、攔截(onInterceptTouchEvent)、處理(onTouchEvent)
View沒有攔截(onInterceptTouchEvent)的方法,收到事件,dispatchTouchEvent肯定會被調(diào)用,返回true或者false代表是否自己要處理事件。
這里可能有疑問了,PhoneWindow是怎么跟Activity關(guān)聯(lián)的,DecorView又是怎么跟Window關(guān)聯(lián)的?
2.Activity、Window、DecorView的關(guān)系
簡單來說就是Activity里有一個Window,Window里面有一個DecorView,我們通過setContentView添加的布局就是添加到DecorView里面的FrameLayout中。
以下是部分源碼分析,加深理解
Activity#setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
我們在Activity的onCrate方法調(diào)用setContentView添加的布局最終是調(diào)用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內(nèi)部有一個Window實例,是在attach方法實例化的,
注釋1:在Activity可見的時候,通過WindowManager將DecorView添加到Window
注釋2:DecorView設(shè)置可見,也就說明了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空,則則調(diào)用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里面根據(jù)不同主題mContentParent指向的內(nèi)容會button
this.mContentParent = this.generateLayout(this.mDecor);
this.mDecor.makeOptionalFitsSystemWindows();
//decorContentParent 初始化
DecorContentParent decorContentParent = (DecorContentParent)this.mDecor.findViewById(16908823);
int transitionRes;
//標題欄設(shè)置
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);
}
}
}
}
小結(jié):從上面分析,我們知道Activity、Window、DecorView之間的關(guān)系是:
Activity里有一個Window,Window里有一個DecorView,我們添加的布局就放在DecorView里的一個FrameLayout里。
3.ScrollView嵌套RecyclerView,如何解決滑動沖突?
RecyclerView 可以滑動的時候我們希望滑動的是RecyclerView,RecyclerView不能滑動的時候才去滑動ScrollView
滑動沖突的方案一般有兩種
- 外部攔截法
- 內(nèi)部攔截法
外部攔截
重寫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是否在點擊范圍內(nèi)
/**g etRawX是以屏幕左上角為坐標做預(yù)案,獲取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;
}
內(nèi)部攔截法
1.RecyclerView 通過調(diào)用requestDisallowInterceptTouchEvent(true),請求父控件不要攔截我的事件
2.RecyclerView判斷不需要的事件,調(diào)用requestDisallowInterceptTouchEvent(false),交給ScrollView去處理。
詳細步驟
- ScrollView 需要重寫 onInterceptTouchEvent,不攔截down事件,返回false,同時意味著自己的onTouchEvent的down事件永遠不會調(diào)用到,所以手動調(diào)用onTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
/** 內(nèi)部攔截法需要父布局不攔截 ACTION_DOWN ,否則所有事件都給父布局了*/
if (ev.getAction() == MotionEvent.ACTION_DOWN){
//手動調(diào)用onTouchEvent
onTouchEvent(ev);
return false;
}
return super.onInterceptTouchEvent(ev);
}
- RecyclerView分發(fā)到down事件的時候,調(diào)用requestDisallowInterceptTouchEvent(true),事件就會傳過來。然后在move事件中判斷如果不需要處理事件,就調(diào)用 requestDisallowInterceptTouchEvent(false),將事件交給ScrollView 處理,因為我們手動調(diào)用了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);
}
好了,本篇文章到此就結(jié)束了,總結(jié)一下:
是什么?
通過重寫事件分發(fā)的三個方法,了解事件分發(fā)的大概流程為什么?
結(jié)合ViewGroup、View事件分發(fā)源碼進行原理分析怎么做?
列舉了面試中的幾個問題,最后是事件沖突的解決辦法。