在咱們平時開發中,老是會遇到滑動衝突。那麼,若是要解決滑動衝突,首先就要求咱們理解Android中一個很是重要的知識點:事件分析機制。android
在瞭解事件分發機制以前,咱們要知道Android佈局層級。咱們在寫Activity
的時候一般是經過setContentView(int layoutResID)
方法設置佈局,因此咱們先從這個方法入手看一下佈局層級。設計模式
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); //1
initWindowDecorActionBar(); //2
}
public Window getWindow() {
return mWindow;
}
複製代碼
由上面的代碼咱們能夠看到,Activity
的setContentView
方法其實是調用了Window
的setContentView
方法,讓咱們看一下Window
對象。bash
/** * 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方法,咱們來看一下。app
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor(); //1
} 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); //1
...
}
}
複製代碼
在這裏我只挑出了主要的代碼,第一段代碼中咱們能夠看到經過installDecor
方法去初始化DecorView
,經過查看源碼咱們能夠知道DecorView
其實是一個Framelayout
。經過第二段代碼的1處咱們能夠看到,其調用了generateLayout
方法,咱們來來看一下這裏面作了什麼。ide
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)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
...
if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
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; //註釋1
}
// System.out.println("Title!");
}
...
}
複製代碼
上面的代碼省略了不少,咱們從代碼中看到generateLayout
方法中會根據不一樣的Future
來給layoutResource
設置佈局。咱們看一下注釋1處的佈局文件。源碼分析
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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區域。 post
提了不少次事件,事件的具體概念是什麼?其實,這裏所說的事件,是指當手指從觸碰到手機屏幕到離開手指屏幕所產生的一系列Touch事件,其中也包括手指在手指在屏幕上滑動所對應的操做。這些Touch事件被封裝到一個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對象中不只封裝了事件類型,同時封裝了手指在操做過程當中的座標。ui
事件分發機制中主要涉及到三個方法:dispatchTouchEvent()、onTouchEvent()和onInterceptTouchEvent()。
方法名 | 方法的含義 |
---|---|
dispatchTouchEvent() | 被用來事件分發,若是事件傳遞到當前的View,該方法必定會被調用,默認返回false |
onInterceptTouchEvent() | 該方法被用來判斷是否攔截某個事件,在dispatchTouchEvent方法中調用 ,返回值表示是否攔截當前事件,一般存在於ViewGroup,通常View中沒有該方法 |
onTouchEvent() | 該方法被用來處理點擊事件,返回值表示是否消耗當前的事件,若是不進行消耗,該系事件序列將不會被接受 |
從上面的分析中咱們能夠進行一下小結:
當手指觸摸屏幕產生的一些列操做會被封裝在MotionEvent中,經過Activity、ViewGroup和View調用一系列方法,找到事件的接受者並處理該事件的一個過程。
複製代碼
從上面的分析咱們已經知道事件的大體流向:Activity
->ViewGroup
->View
,接下來讓咱們從源碼的角度逐個分析。
既然事件最初是從Activity中進行傳遞,因此咱們首先到Activity
中找到dispatchTouchEvent()
方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) { //1
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
。
既然事件已經從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;
//--------------------1----------------------
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//--------------------2----------------------
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;
}
//--------------------3----------------------
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) {
...
//--------------------4----------------------
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;
}
//--------------------5----------------------
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()方法。
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)
方法。
View
的事件分發過程當中調用了onTouchEvent(MotionEvent)
方法,咱們來看一下。
public boolean onTouchEvent(MotionEvent event) {
...
//----------------------1----------------------
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//----------------------2----------------------
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
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)) {
//----------------------3----------------------
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 {
// Not inside a scrolling container, so show the feedback right away
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
方法,這個方法去執行內部點擊,具體調用方法在最後兩段代碼中。從最後一段代碼中咱們看到了很熟悉的代碼塊li.mOnClickListener.onClick(this)
,這裏的OnClickListener
就是咱們本身設置的。可是咱們在ACTION_DOWN
中沒有看到執行點擊的方法。因此,執行點擊的方法不是在觸摸時產生,而是在手指離開時產生。
從上面的源碼中咱們也能夠看到, 最後從網上找了一張事件分發機制流程圖,感謝各位大佬。
下面我會經過一個案列加以說明
public class MyActivity extends AppCompatActivity {
private MyButton my_buttom;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
my_buttom = findViewById(R.id.my_buttom);
my_buttom.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("EventDemo", "MyButton--->onClick");
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("EventDemo", "MyActivity--->dispatchTouchEvent"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("EventDemo", "MyActivity--->onTouchEvent"+event.getAction());
return super.onTouchEvent(event);
}
}
複製代碼
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("EventDemo", "MyLinearLayout--->dispatchTouchEvent" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("EventDemo", "MyLinearLayout--->onInterceptTouchEvent" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("EventDemo", "MyLinearLayout--->onTouchEvent" + event.getAction());
return super.onTouchEvent(event);
}
}
複製代碼
public class MyButton extends AppCompatButton {
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("EventDemo", "MyButton--->dispatchTouchEvent" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("EventDemo", "MyButton--->onTouchEvent" + event.getAction());
return super.onTouchEvent(event);
}
}
複製代碼
<com.example.eventdemo.MyLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.eventdemo.MyButton
android:id="@+id/my_buttom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="點我" />
</com.example.eventdemo.MyLinearLayout>
複製代碼
咱們直接默認設置,直接運行點擊看一下效果。
咱們在MyActivity中添加以下代碼:
my_buttom.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("EventDemo", "MyButton--->onTouch");
return false;
}
});
複製代碼
咱們來看一下運行結果:
寫了這麼多,讓咱們總結一下事件分發機制。
一、一般狀況下,事件的序列是ACTION_DOWN開始,到ACTION_UP結束。
二、當一個事件由一個View決定攔截時(ACTION_DOWN觸發時決定攔截),該事件序列都會由其接收。不然該事件序列將再也不被其接收。
三、從優先級上講:OnTouchListener.onTouch > onTouchEvent > onClickListener。
四、當一個View是disable狀態時,但它是可點擊狀態時,事件仍是由它來接收,只是不作任何反饋。
五、事件分發機制的核心設計模式是責任鏈設計模式,這個模式在okhttp的源碼中也能看到。
複製代碼
到這裏事件分發機制算是過了一遍,若是有什麼寫的不對的地方或者是有哪裏遺漏了,還請各位大佬批評指正。