View的事件分發(fā)機制以及滑動沖突
[TOC]
點擊事件的傳遞規(guī)則
點擊時間的分發(fā)過程 總是繞不過三個很重要的方法來共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent ev)
? 用來進行事件的分發(fā)绞惦。如果時間能夠分發(fā)到當前View,那么此方法一定會被調(diào)用颇象,返回的結(jié)果受View的OntouchEvent和下級View的dispatchEvent方法的影響腮恩,表示是否消耗當前事件距辆。
? public boolean onIntercepTouchEvent(MotionEvent ev)
? 只有ViewGroup才會擁有的方法,用于攔截某個事件盗尸,如果當前的View攔截某個事件宣旱,那么在同一個時間序列中至会,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當前事件矮慕。
? **public boolean onTouchEvent(MotionEvent ev) **
? 在dispatchTouchEvent方法中調(diào)用帮匾,用來處理點擊事件,返回結(jié)果表示是否消耗當前事件痴鳄,如果不消耗瘟斜,則在同一個事件序列中,當前View無法再次接受到事件痪寻。
那么這三個方法的調(diào)用順序是如何呢螺句?
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
對于一個根ViewGroup來說,點擊事件產(chǎn)生后橡类,首先會傳遞給它壹蔓,此時它的dispatchTouchEvent就會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件猫态,接著事件就會交給這個ViewGroup處理佣蓉,及它的onTouchEvent方法就會被調(diào)用,如果它的onInterceptTouchEvent返回為false亲雪,表示它不攔截當前事件勇凭,此時當前事件就會繼續(xù)傳遞給它的子元素,此時如果是View义辕,則會直接調(diào)用onTouchEvent方法虾标。
OnTouchListener, View.onTouchEvent 和OnclickListener的區(qū)別
當一個View需要處理事件。設置了OnTouchListener,則OnTouchListener的onTouch方法會被回調(diào)灌砖,如果onTouch的方法返回True璧函,則View.onTouchEvent方法不會被調(diào)用傀蚌,反之則會被調(diào)用。View.onTouchEvent方法中蘸吓,如果當前設置的有OnclickListener善炫,其優(yōu)先級最低。
這三者的優(yōu)先級: OnTouchListener -> View.onTouchEvent -> OnclickListener
當一個點擊事件產(chǎn)生后库继,它的傳遞過程遵循如下順序 Activity -> PhoneWindow -> RootView箩艺。 由Activity 傳給PhoneWindow Window 最后傳給頂級View。
關(guān)于時間傳遞的機制宪萄,我們首先在這里給一些結(jié)論艺谆,
- 同一事件順序是指手指接觸屏幕的那一刻起, 到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件拜英,這個時間序列以Down事件開始静汤,中間含有數(shù)量不一的move事件。最后以up事件結(jié)束居凶。
- 正常情況下撒妈,一個事件序列只能被一個View攔截且消耗,因此一個時間序列的事件不能分別由兩個View同時處理排监,但是我們可以通過代碼控制事件傳遞狰右。
- 某個View一旦決定攔截孕豹,那么一個事件序列都只能由它來處理踩萎,并且它的onIntercepTouchEvent不會再被調(diào)用。當一個View決定攔截一個事件后璃诀,同一事件的剩下事件也會交給它來處理挨队,也就是說onIntercepTouchEvent不會被再調(diào)用谷暮。
- 某個View一旦開始處理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回為false)盛垦,那么同一事件中的其他時間都不會再交給它來處理湿弦,并且事件將重新交由它的父元素去處理。
- 如果View不消耗除了ACTION_DOWN以外的其他事件腾夯,那么這個點擊事件會消失颊埃,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)的事件蝶俱,最終這些消失的點擊事件會傳遞給Activity處理
- ViewGroup默認是不攔截任何事件班利,默認onInterceptTouchEvent方法默認返回False
- View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它榨呆,那么它的onTouchEvent方法就會被調(diào)用
- View的OntouchEvent默認都是會消耗事件罗标,除非它是不可以點擊的(clickable longclickable同時為false)。 View的longClickable屬性都是false,clickable屬性要分情況闯割,Button的clickable屬性默認為true彻消,TextView的clickable屬性默認為false。 當然 如果給view設置了setOnclickListener 或者setOnLongClickListener 會默認開啟宙拉。
- 事件傳遞過程是由外向內(nèi)的宾尚,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子元素鼓黔,通過requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分發(fā)過程央勒,但是ACTION_DOWN事件除外不见。
事件分發(fā)的源碼分析
點擊事件是有MotionEvent來表示澳化,當一個點擊事件發(fā)生,最先傳入的是Activity稳吮,由Activity的disPatchEvent來進行事件派發(fā)缎谷,,具體工作是由Activity內(nèi)部的Window來完成的灶似。 Window會將時間傳遞給DecorView列林,一般DecorView就是當前界面的頂級容器(即是setContentView所設置的View的父容器),如圖:
public boolean disPatchTouchEvent(MotionEvent ev){
if(ev.getAction == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return truel
}
return onTouchEvent(ev);
}
Window是如何將事件傳遞給ViewGroup的呢酪惭,Window類其實是一個抽象類 它可以控制頂級view的外觀和行為策略,Window的唯一實現(xiàn)是PhoneView類希痴,
publlic boolean superDisPatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
這個mDecot其實就是我們getWindow().getDecorView()返回的View,我們通過設置setContentView設置的View就是它的一個子View,自此事件傳遞到了頂級View,即我們設置的SetContentView所設置的View春感。
ViewGroup的事件分發(fā)機制
我們在看一下ViewGroup對點擊事件的分發(fā)過程砌创,主要實現(xiàn)在disPatchEvent方法中,
final boolean intercepted
if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept){
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
}else{
intercepted = false;
}
}else{
intercepted = true;
}
由ViewGroup的子元素成功處理時鲫懒, mFirsrtTouchTarget 會被賦值并指向子元素嫩实。所以一旦事件是由當前的ViewGroup攔截,接下來同一時序的其他事件都會默認交給ViewGroup來處理窥岩。
另外一個特殊情況就是FLAG_DISALLOW_INTERCEPT標識符甲献,這這個一般通過requestDisallowInterceptTouchEvent方法來設置的,一般用于子View颂翼,一般設置ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件晃洒,因為ViewGroup在事件分發(fā)的時候會重置FLAG_DISALLOW_INTERCEPT標識符,這意味著當面對ACTION_DOWN的時候朦乏,ViewGroup一定會調(diào)用onInterceptTouchEvent來判斷自己是否需要攔截事件
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
首先遍歷所有的ViewGroup的所有子元素,判斷是否能夠接受到點擊事件主要由兩點來衡量:子元素是否在播動畫和點擊事件的坐標是否落在子元素的區(qū)域內(nèi)锥累,dispatchTransformedTouchEvent就是調(diào)用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 則會繼續(xù)分發(fā)給下一個子元素(如果有的話), 在addTouchTarget(child, idBitsToAssign) 給mFirsrtTouchTarget 賦值集歇。mFirsrtTouchTarget 是否為null直接影響ViewGroup對事件的攔截策略桶略。
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
{
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
如果遍歷了所有的子元素事件都沒有被合適處理,這包含兩種情況: 第一種是ViewGroup沒有子元素,第二種是子元素處理了點擊事件际歼,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false惶翻,這是ViewGroup會自己處理點擊事件。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
View的事件分發(fā)
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
首先判斷是否有設置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不會被調(diào)用鹅心。
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;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就會消耗這件事吕粗,當ACTION_UP事件發(fā)生 會觸發(fā)performClick() 如果設置了OnclickListener ,performClick就是調(diào)用Onclick方法旭愧。
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;
}
}
設置Viewd的OnclickListener和OnLongClickListener()會自動將View的Clickable颅筋,LongClickAble為true。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
View的滑動沖突
常見的滑動沖突場景
- 場景1 ------ 外部滑動方向和內(nèi)部滑動方向不一致
- 場景2------- 外部滑動方向和內(nèi)部滑動方向一致
- 場景3------- 上面兩種情況的嵌套
對應處理的方法:
- 當用戶左右滑動時输枯,需要外部攔截點擊事件议泵,當用戶需要上下滑動時,需要讓內(nèi)部攔截點擊事件
- 這種場景無法根據(jù)滑動的角度桃熄, 距離差已經(jīng)速度差來做判斷 一般需要從業(yè)務上找到突破口
- 同上先口,一般也是從業(yè)務的需要上得出相應的處理規(guī)則
兩種滑動沖突的解決方案:
-
外部攔截法
所謂的外部攔截法就是所有的點擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件則攔截 如果不需要此事件就不攔截瞳收,這樣就可以解決滑動沖突的問題碉京,這種方法比較符合點擊事件的分發(fā)機制,外部攔截法需要重寫父容器的onInterceptTouchEvent方法螟深。偽代碼如下
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastY = y; break; case MotionEvent.ACTION_MOVE: float dy = y - mLastY; if(父容器需要當前點擊事件){ return true; } break; } // 默認返回的都是false return super.onInterceptTouchEvent(ev); }
-
內(nèi)部攔截法
內(nèi)部攔截法主要是父容器不攔截任何事件谐宙,所有事件都傳遞給子元素,如果子元素需要此事件就直接消耗界弧,否則交由父容器進行處理凡蜻,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般內(nèi)部攔截法比較的復雜夹纫,它的偽代碼如下咽瓷,我們需要重寫子元素的dispatchTouchEvent方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true) break; case MotionEvent.ACTION_MOVE: if(父容器需要當前點擊事件){ return parent.requestDisallowInterceptTouchEvent(false); } break; } // 默認返回的都是false return super.dispatchTouchEvent(ev); }
面對不同的滑動策越的時候只需要修改ACTION_MOVE事件即可,其他不需要動也不能改動舰讹。除了子元素需要處理外茅姜,父元素也要默認攔截除了ACTION_DOWN以外的所有其他事件,這樣當子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)時月匣,父元素才能繼續(xù)攔截所需要的事件钻洒。
?
-