View的事件體系
View的基礎
-
view位置參數(shù)
-
View的位置主要由它的四個頂點來決定,分別對應于View的四個屬性:
top
芦缰、left
垄提、right
、bottom
评矩,其中top是左上角縱坐標叶堆,left是左上角橫坐標,right是右下角橫坐標斥杜,bottom是右下角縱坐標, 這四個參數(shù)的坐標值都是View相對于父View的.
View的寬高和坐標的關系:width = right - left; height = bottom - top;
如何得到這四個參數(shù):
Left = getLeft(); Right = getRight(); Top = getTop(); Bottom = getBottom();
-
從Android 3.0開始虱颗,view增加了
x
、y
蔗喂、translationX
忘渔、translationY
四個參數(shù),這幾個參數(shù)也是相對于父容器的坐標. x和y是左上角的坐標缰儿,而translationX和translationY是view左上角相對于父容器的偏移量畦粮,默認值都是0.x = left + translationX y = top + translationY
View在平移過程中改變的是
x
,y
,translationX
,translationY
這四個參數(shù),left
和top
等是原始左上角的位置信息, 其值不會隨著平移改變.
setX()內部也是調用的setTranslationX()
setLeft()方法系統(tǒng)不建議我們人為調用, 因為left屬性在layout()時系統(tǒng)會隨時更改 View在滑動其內容時更改的是它的
mScrollX
mScrollY
這兩個參數(shù)
mScrollX
的值總是等于View左邊緣和View內容左邊緣在水平方向的距離
mScrollY
的值總是等于View上邊緣和View內容上邊緣在垂直方向的距離
scrollTo()
和scrollBy()
內部其實就是更改這兩個參數(shù).
-
-
MotionEvent和TouchSlop
-
MotionEvent
在手指觸摸屏幕后所產(chǎn)生的一系列事件中,典型的事件類型有:- ACTION_DOWN ----- 手指剛接觸屏幕
- ACTION_MOVE ----- 手指在屏幕上移動
- ACTION_UP ----- 手機從屏幕上松開的一瞬間
正常情況下乖阵,一次手指觸摸屏幕的行為會觸發(fā)一系列點擊事件宣赔,考慮如下幾種情況:
- 點擊屏幕后離開松開,事件序列為 DOWN -> UP
- 點擊屏幕滑動一會再松開瞪浸,事件序列為DOWN->MOVE->...->UP
通過MotionEvent對象我們可以得到點擊事件發(fā)生的x和y坐標儒将,getX/getY返回的是相對于當前View左上角的x和y坐標,getRawX和getRawY是相對于手機屏幕左上角的x和y坐標对蒲。
TouchSlop
TouchSlope是系統(tǒng)所能識別出的可以被認為是滑動的最小距離钩蚊,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()
-
-
VelocityTracker、GestureDetector和Scroller
-
VelocityTracker
用于追蹤手指在滑動過程中的速度蹈矮,包括水平和垂直方向上的速度.
VelocityTracker的使用方式://初始化 VelocityTracker mVelocityTracker = VelocityTracker.obtain(); //在onTouchEvent方法中 mVelocityTracker.addMovement(event); //獲取速度 mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity();//一般在MotionEvent.ACTION_UP的時候調用 //重置和回收 mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用 mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用
-
GestureDetector
手勢檢測砰逻,用于輔助檢測用戶的點擊、滑動泛鸟、長按蝠咆、雙擊等行為.我們通過查看源碼,發(fā)現(xiàn)在GestureDetector類中封裝了兩個接口和一個內部類:
分別為
OnGestureListener
和OnDoubleTapListener
兩種listener.
SimpleOnGestureListener
實現(xiàn)了上述兩種listener
, 但是內部的實現(xiàn)方法都為null, 使用時根據(jù)個人需要來實現(xiàn)對應的方法.
GestureDetector使用方式:GestureDetector mGestureDetector = new GestureDetector(new SimpleOnGestureListener () { //實現(xiàn)需要用到的方法 }); mGestureDetector.setIsLongPressEnabled(false);//解決長按屏幕后無法拖動的現(xiàn)象. boolean consume = mGestureDetector.onTouchEvent(event);//一般在onTouchEvent中接管event return consume;
OnGestureListener
和OnDoubleTapListener
接口具體如下:public interface OnGestureListener { boolean onDown(MotionEvent e); //手指剛剛觸碰屏幕的一瞬間北滥, 由一個ACTION_DOWN觸發(fā) void onShowPress(MotionEvent e); //手指輕輕觸碰屏幕刚操, 尚未松開或拖動, 由一個ACTION_DOWN觸碑韵,它和onDown的區(qū)別是它強調的是沒有松開或者拖動的狀態(tài) boolean onSingleTapUp(MotionEvent e); //單擊行為赡茸, 伴隨著一個ACTION_UP而觸發(fā) boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); //手指按下屏幕并拖動, 由一個ACTION_DOWN和多個ACTION_MOVE組成祝闻,這是拖動行為 void onLongPress(MotionEvent e); //用戶長久的按著屏幕不放占卧,即長按 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); //快速滑動行為遗菠,由一個ACTION_DOWN,多個ACTION_MOVE华蜒,一個ACTION_UP觸發(fā) } public interface OnDoubleTapListener { boolean onSingleTapConfirmed(MotionEvent e); //嚴格的單擊行為辙纬, 即這只可能是單擊而不可能是雙擊中的一次單擊 boolean onDoubleTap(MotionEvent e); //雙擊行為,它不可能和onSingleTapConfirmed共存 boolean onDoubleTapEvent(MotionEvent e); //表示發(fā)生了雙擊行為叭喜, 在雙擊期間ACTION_DOWN贺拣,ACTION_MOVE,ACTION_UP均會觸發(fā)此回調 }
在日常開發(fā)中捂蕴,比較常用的有:
onSingleTapUp(單擊)
譬涡、onFling(快速滑動)
、onScroll(拖動)
啥辨、onLongPress(長按)
、onDoubleTap(雙擊)
.
建議:如果只是監(jiān)聽滑動相關的事件在onTouchEvent
中實現(xiàn)溉知;如果要監(jiān)聽雙擊這種行為的話陨瘩,那么就使用GestureDetector
恒傻。 -
Scroller
彈性滑動對象,用于實現(xiàn)View的彈性滑動契吉。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll
方法配合使用才能共同完成這個功能寄摆。
Scroller
使用方式Scroller scroller = new Scroller(mContext); // 緩慢滾動到指定位置 private void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms內滑動到destX的位置 mScroller.startScroll(scrollX, 0, delta, 0, 1000); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
原理:
invalidate()
方法會觸發(fā)computeScroll()
方法, 然后我們重寫了computeScroll()
在里面調用scrollTo
來讓View
移動到Scroller
計算過后的位置割择, 然后再次觸發(fā)invalidate()
方法眷篇, 直到Scroller
計算完成。
-
View的滑動
使用scrollTo或scrollBy
scrollTo()
是基于所傳參數(shù)的絕對滑動,scrollBy()
是基于目前所在位置的相對滑動.
scrollTo()
和scrollBy()
只能改變View內容的位置, 不能改變View在布局中的位置.-
使用動畫
android中動畫分為三種:View動畫
幀動畫
屬性動畫
.
我們通過View動畫
和屬性動畫
都可以完成View的滑動, 使用動畫主要操作的是View
的translationX
和translationY
這兩個屬性(因為setX()內部其實調用的時setTranslationX()).使用上我們需要注意以下兩點:
-
view動畫
操作的是控件的影像而不是view的位置參數(shù)(它不會移動view的本身也不會移動view的內容)荔泳,盡管我們在視覺上看到了滑動的效果蕉饼,但實際上view的位置卻不曾發(fā)生改變。這點可以從如果我們不設置view的控件參數(shù)fillAftrer為true的時候玛歌,那么當動畫完成后昧港,View會瞬間恢復到動畫前的效果就可以看得出來。而且支子,即便我們設置了fillAfter參數(shù)為true创肥。也只是相當于把view投影到移動的位置,但當我們再要執(zhí)行點擊操作的時候值朋,卻是不能發(fā)生響應的叹侄。因為view的位置不會發(fā)生改變。它的真身仍在原始位置上昨登。 -
view的屬性動畫
可以解決上面的問題, 但是它無法兼容3.0以下的版本.
-
-
通過改變布局參數(shù)
通過改變布局參數(shù)的方式來實現(xiàn)滑動趾代,實際上改變的是LayoutParams參數(shù)绘搞,如果我們想要滑動某個控件番电,則直接通過修改LayoutParams參數(shù)來實現(xiàn),這個方法最為簡單暴力渔伯,但操作較為復雜,需要根據(jù)不同的情況去做不同的處理尿褪。使用方法如下(以移動一個Button為例):Button button = (Button) findViewById(R.id.btn_changeparams); MarginLayoutParams params = (MarginLayoutParams) button.getLayoutParams(); params.width += 100; params.leftMargin +=100; button.requestLayout();
-
三種滑動方式對比:
- scrollTo/scrollBy: 操作簡單睦擂,適合對View內容的滑動
- 動畫: 操作簡單,主要適用于沒有交互的View和實現(xiàn)復雜的動畫效果
- 改變布局參數(shù): 操作稍微復雜杖玲,適用于有交互的View
View彈性滑動
使用Scroller
上面已經(jīng)介紹過了Scroller
的原理和使用方法使用動畫
采用這種方法除了能完成彈性滑動以外顿仇,還可以實現(xiàn)其他動畫效果,我們完全可以在onAnimationUpdate
方法中加上我們想要的其他操作摆马。-
使用延時策略
使用延時策略來實現(xiàn)彈性滑動臼闻,它的核心思想是通過發(fā)送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用Handler
的sendEmptyMessageDelayed(xxx)
或view
的postDelayed()
方法囤采,也可以使用線程的sleep
方法述呐。private Handler = new Handler(){ public void handleMwssage(Message msg){ switch(msg.what){ case MOVE_VIEW: //move view step handle.sendEmptyMessageDelayed(MOVE_VIEW,1000); break; } } };
View的事件分發(fā)機制
-
點擊事件的傳遞規(guī)則
所謂點擊事件的事件分發(fā),其實就是對MotionEvent
的分發(fā)過程蕉毯。當一個MotionEvent
產(chǎn)生之后乓搬,系統(tǒng)需要將其傳遞給某個具體的View
,比如Button
控件代虾,并且被這個View
所消耗进肯。整個事件分發(fā)過程由三個方法完成,分別是:- dispatchTouchEvent(MotionEvent event)
/** * 這個方法用來進行事件的分發(fā)棉磨,當MotionEvent事件傳遞到當前View時江掩,便會觸發(fā)當前View的這個方法, * 返回的結果受當前View的onTouchEvent和下級的dispatchTouchEvent方法的影響乘瓤,表示是否消耗該MotionEvent环形。 * true表示被當前View所消耗,false則表示事件未被消耗衙傀。 */ public boolean dispatchTouchEvent(MotionEvent event);
- onInterceptTouchEvent(MotionEvent event)
/** * 這個方法在dispatchTouchEvent方法內部調用抬吟,用來判斷是否攔截某個事件, * 如果當前View攔截了某個事件差油,那么在同一個事件序列中拗军,此方法不會再被調用任洞, * 返回結果表示是否攔截當前事件蓄喇。 */ public boolean onInterceptTouchEvent(MotionEvent event);
- onTouchEvent(MotionEvent event)
/** * 這個方法在dispatchTouchEvent方法內部調用,用來處理點擊事件交掏, * 返回結果表示是否消耗當前事件妆偏,如果不消耗(ACTION_DOWN),則在同一事件序列中盅弛,當前View無法再次接收到該事件钱骂。 */ public boolean onTouchEvent(MotionEvent event);
以上三者的關系可以用偽代碼進行表示:
public boolean dispatchTouchEvent(MotionEvent event){ boolean consume = false; if(onInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else{ consume = childView.dispatchTouchEvent(event); } return consume; }
對于一個根
ViewGroup
來說叔锐,當產(chǎn)生點擊事件后,首先會傳遞給它见秽,此時調用它的dispatchTouchEvent
方法愉烙,如果dispatchTouchEvent
方法中的onInterceptTouchEvent(event)
返回true
,則表示這個ViewGroup
要消耗當前事件,于是調用ViewGroup
的OnTouchEvent(event)
方法解取。而如果onInterceptTouchEvent(event)
返回的是false
步责,則將該event
交給這個當前View
的子元素的dispatchTouchEvent
去處理。如此遞歸禀苦,直到事件被最終處理掉蔓肯。當一個點擊事件產(chǎn)生后,它的傳遞順序如下:
Activity -> Window -> View
Activity是怎么接收到點擊事件的請參考這篇文章
當頂級View接收到該事件后振乏,就會將其按照事件分發(fā)機制去分發(fā)該事件蔗包,也即從父容器到子容器間層層傳遞,直到在某一個階段事件被消耗完畢慧邮。但在這里存在另一個問題:如果最底層的子元素并沒有消耗點擊事件调限,怎么辦?為解決這個問題误澳,系統(tǒng)做了以下的措施:如果一個View的onTouchEvent方法返回的是false旧噪,那么該view的父容器的onTouchEvent方法也會被調用,以此類推脓匿,若該點擊事件沒有任何元素去消耗淘钟,那么最終仍是會由Activity進行處理關于事件傳遞的機制,有以下結論:
- 同一個事件序列是指從手指接觸到屏幕的那一刻起陪毡,到手指離開屏幕的那一刻結束米母。期間以
Down
為開始,中間含有數(shù)量不等(可以為0)的MOVE
,最終則以UP
結束毡琉。 - 正常情況下铁瞒,一個事件序列只能被一個View攔截且進行消耗。
- 某個
View
一旦決定攔截事件序列桅滋,那么這一個事件序列只能由它來處理(只要在這個view
進行攔截之前沒有其他view
對這個事件序列進行攔截)慧耍,并且它的onInterceptTouchEvent
方法也不會再被調用。 - 某個
View
一旦開始處理事件序列丐谋,如果它不消耗ACTION_DOWN
事件(OnTouchEvent
返回false
)芍碧,那么同一個事件序列中的其他事件都不會由它來處理,而是直接將其交由父元素去處理号俐。并且當前view
是無法再次接收到該事件的泌豆。 - 如果
View
不消耗除了ACTION_DOWN
之外的其他事件,那么這個點擊事件就會消失吏饿,并且父元素的OnTouchEvent
方法也不會被調用踪危,同時蔬浙,當前View
可以持續(xù)收到后續(xù)的事件,最終這些消失的點擊事件會交由Activity
進行處理贞远。 -
ViewGroup
不攔截任何事件畴博。Android
源碼中ViewGroup
的onInterceptTouchEvent
方法默認返回false
。 - 在
Android
源碼中蓝仲,View
并沒有onInterceptTouchEvent
方法绎晃,一旦有點擊事件傳遞給它。那么它的OnTouchEvent
方法就會被調用杂曲。 -
view
的OnTouchEvent
默認會消耗該事件(默認返回true
),除非它是不可點擊的(clickable
和longclickable
同時為false
)庶艾。 -
view
的enable
屬性不影響onTouchEvent
的默認放回值。即便該view
是disable
狀態(tài)的擎勘,但只要它的clickable
或longClickable
有一個為true
,那么它的返回值就為true
咱揍。 -
onclick
會發(fā)生的前提是當前View
是可點擊的,并且它接收到了ACTION_DOWN
和ACTION_UP
事件棚饵。 - 事件傳遞過程是由外向內的煤裙,及事件總是先傳遞給父元素。然后再有父元素去分發(fā)給子元素噪漾。但通過
requestDisallowInterceptTouchEvent
方法可以在子元素中干預父元素的分發(fā)過程硼砰,但ACTION_DOWN
事件除外。
- dispatchTouchEvent(MotionEvent event)
-
從源碼去看事件分發(fā)機制:
-
Activity分發(fā)
從上面我們知道欣硼,每個MotionEvent
都是最先交由Activity
進行的题翰,那么我們來看看Activity
中的dispatchTouchEvent
方法public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
-
Window分發(fā)
我們可以看到Activity
其實是將點擊事件交給了Window
進行下一步處理, 但是Window
類其實是一個抽象類, 它里面的superDispatchTouchEvent()
方法是一個抽象方法.
所以我們需要去它的唯一實現(xiàn)類PhoneWindow
中去查看superDispatchTouchEvent()
是如何實現(xiàn)的.//PhoneWindow中的superDispatchTouchEvent()方法 public boolean superDispatchTouchEvent(MotionEvent event){ return mDecor.superDispatchTouchEvent(event); }
這里的
mDecor
其實就是DecorView
,那么DecorView
是什么呢诈胜?我們來看private final class DecorView extends FrameLayout implements RootViewSurfaceTacker{ private DecorView mDecor; @override public final View getDecorView(){ if(mDecor == null){ installDecor(); } return mDecor; } }
我們知道豹障,通過
(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0);
這種方式可以獲取Activity
的所預設置的View
,而這個mDector
顯然就是返回的對象焦匈。也就是說血公,這里的DecorView
是頂級View(ViewGroup)
,內部有titlebar
和contentParent
兩個子元素缓熟,contentParent
的id
是content
累魔,而我們設置的main.xml
布局則是contentParent
里面的一個子元素。那么够滑,當事件傳遞到DecorView
這里的時候垦写,因為DecorView
繼承了FrameLayout
且還是父View
,所以最終的事件會傳送到我們在setContentView()
所設置的頂級View
中版述。 -
ViewGroup分發(fā)
那么梯澜,現(xiàn)在事件已經(jīng)傳遞到頂級View
(一個ViewGroup
)了,接下來又該是怎樣的呢渴析?邏輯思路如下:頂級View調用dispatchTouchEvent方法 if 頂級view需要攔截事件(onInterceptTouchEvent方法返回true) 處理點擊事件 else 把事件傳遞給子元素進行處理
根據(jù)這個晚伙,我們先來看一下ViewGroup對點擊事件的分發(fā)過程,其主要體現(xiàn)在dispatchTouchEvent方法中俭茧。因為這個方法比較長咆疗,分段說明,先看下面一段:
public boolean dispatchTouchEvent(MotionEvent ev) { //....省略 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); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } //....省略 }
從上面的代碼可以看出母债,
ViewGroup
會在兩種情況下判斷是否攔截當前事件:一是事件類型為ACTION_DOWN
午磁,二則是mFirstTouchTarget != null
。在這里毡们,mFirstTouchTarget
是什么意思呢迅皇? 可以這么理解:當事件由ViewGroup
的子元素成功處理時,mFirstTouchTarget
會被賦值并指向子元素衙熔。也就是說登颓,當ViewGroup
不攔截事件并且把事件交給子元素處理時,則mFirstTouchTarget != null
红氯。反之框咙,如果ViewFroup
攔截了這個事件,則mFirstTouchTarget != null
就不成立, 所以當ACTION_MOVE
和ACTION_UP
事件到來時,actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
為false
,ViewGroup
將會直接攔截事件而不會再次調用它自己的onInterceptTouchEvent(ev)方法痢甘,并且同一序列中的其他事件會交由它處理(前提是事件到達它之前沒有被攔截)喇嘱。對上面第3條結論的驗證當然,事實無絕對塞栅,此處有一個特殊情況者铜,就是
FLAG _DISALLOW _INTERCEPT
這個標志位,它是通過requestDisallowInterceptTouchEvent()
方法來設置的放椰,一般用于子View
中王暗。它一旦被設置,ViewGroup
則將無法攔截除了ACTION _DOWN
以外的其他點擊事件庄敛。為什么是除了ACTION_DOWN
以外呢俗壹?public boolean dispatchTouchEvent(MotionEvent ev) { //省略... // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } //省略... }
在這段源碼中,
ViewGroup
會在ACTION_DOWN
事件到來時做重置狀態(tài)的操作藻烤,而在resetTouchState
方法中會對FLAG _DISALLOW _INTERCEPT
進行重置绷雏,因此子View
調用requestDisallowInterceptTouchEvent
方法時并不能影響ViewGroup
對ACTION _DOWN
的影響。
接著我們再看當ViewGroup
不攔截事件的時候怖亭。事件會向下分發(fā)涎显,交由它的子View
進行處理的過程:public boolean dispatchTouchEvent(MotionEvent ev) { // 省略...View的LONG_CLICKABLE屬性默認為false,而CLICKABLE的屬性則和具體的View有關兴猩。通過setClickable和setLongClickable方法可以修改這兩個值期吓。此外,在setOnClickListener中也會自動將CLICKABLE屬性改為true倾芝,而setOnLongClickListener則將LONG _CLICKABLE設置為true讨勤。 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); // 如果一個child沒有播放動畫&&點擊事件落在了它的區(qū)域內 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } // 省略... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 省略... // 這個child消耗了這個點擊事件, 對mFirstTouchTarget賦值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } // 省略... if (mFirstTouchTarget == null) { // 沒有子View消耗了點擊事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } }
從源碼中箭跳,我們可以發(fā)現(xiàn)它的過程如下:首先遍歷
ViewGroup
的所有子元素,然后判定子元素是否能夠接收到點擊事件(子元素是否在播動畫或者點擊事件的坐標是否落在子元素的區(qū)域內)潭千。如果某個子元素滿足這兩個條件谱姓,那么事件就會交由它來處理∨偾纾可以看到屉来,dispatchTransformedTouchEvent
方法實際上調用的就是子元素的dispatchTouchEvent
方法。怎么看的呢狈癞?在這個方法的內部茄靠,有這么一段:private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { // 省略... if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } // 省略... return handled; }
返回上一段源碼,如果子元素的
dispatchTouchEvent(event)
方法返回true
蝶桶,那么我們就不需考慮事件在子元素是怎么派發(fā)的慨绳,那么mFirstTouchTarget
就會被賦值,同時跳出for循環(huán)莫瞬。從源碼中抽取相關部分見下:newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;
有人說儡蔓,這段代碼并沒有對
mFirstTouchTarget
的賦值,因為它實際上出現(xiàn)在addTouchTarget
方法中疼邀,源碼如下:private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
從這個方法的內部結構可以看出喂江,
mFirstTouchTarget
是以一種單鏈表結構,它的賦值與否直接影響到了ViewGroup
的攔截策略旁振。接下來我們再次返回最初的源碼中, 如果遍歷所有的子元素事件后都沒有被合適地處理获询,這包含兩種情況:一是
ViewGroup
中沒有子元素,二則是子元素處理了點擊事件拐袜,但是在dispatchTouchEvent
方法中返回了false
吉嚣。在這兩種情況下,ViewGroup
會調用它自己的onTouchEvent()
處理點擊事件:if (mFirstTouchTarget == null) { // 沒有子View消耗了點擊事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
注意這一段源碼的第三個參數(shù)
child
為null
蹬铺,從前面的分析就可以知道尝哆,它會調用super.dispatchTouchEvent(event)
,很顯然甜攀,這里就從ViewGroup
轉到了View
的dispatchTouchEvent(event)
秋泄。
在隨后我們對View
的dispatchTouchEvent(event)
分析中我們會發(fā)現(xiàn),View
的dispatchTouchEvent(event)
會調用onTouchEvent()
方法.注意:在這時View的
dispatchTouchEvent()
中其實調用的是ViewGroup
中的onTouchEvent()
方法.
因此當一個ViewGroup
的ACTION_DOWN
事件沒有被子View
消耗時, 這個ViewGroup
本身的onTouchEvent()
就會被調用來處理這個點擊事件(對上面第4條結論的驗證)
這時你們可能會奇怪, 為什么我們在View
的dispatchTouchEvent()
方法中調用ViewGroup
中的onTouchEvent()
方法.我們來看下面這段代碼:
public class A { public void AA() { System.out.println("A.AA"); BB(); } public void BB() { System.out.println("A.BB"); } } public class B extends A { @Override public void AA() { System.out.println("B.AA"); } @Override public void BB() { System.out.println("B.BB"); } public void CC() { super.AA(); } }
我們定義兩個類A和B, A和B中都有
AA
和BB
方法, 并且輸出不同的Log, 那么此時我們執(zhí)行new B().CC()
會輸出什么結果呢?
答案是:A.AA B.BB
是不是猜錯了?
為什么會是這樣的結果呢, 因為我們是在B中調用的super.AA()
, 因此在A的AA()
方法中我們調用this
其實拿到的是一個B的引用, 如下圖
所以在A的AA()
方法中我們會執(zhí)行B的BB()
方法.現(xiàn)在是不是就明白了, 為什么我們在View的
dispatchTouchEvent()
中調用的是ViewGroup
中的onTouchEvent()
方法了? 因為View的dispatchTouchEvent()
是通過ViewGroup
調起來的. -
View分發(fā)
接下來我們回過頭繼續(xù)看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; } // 省略... return result; }
View
對點擊事件的處理過程比較簡單,因為View
是一個單獨的元素规阀,因此無法向下傳遞事件恒序。所以它只能自己處理事件。從上面的源碼可以看出View
對點擊事件的處理過程:首先判斷有沒有設置onTouchListener
,如果OnTouchListener
中的onTouch
方法返回true
,那么onTouchEvent
就不會被調用谁撼,由此可見OnTouchListener
方法的優(yōu)先級高于onTouchEvent
歧胁。接下來,分析
onTouchEvent
的實現(xiàn)。先看當View
處于不可用狀態(tài)下點擊事件的處理過程:public boolean onTouchEvent(MotionEvent event) { // 省略... if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } // 省略... }
很顯然喊巍,不可用狀態(tài)下的view照樣會消耗點擊事件屠缭,盡管它看起來不可用。
接著玄糟,再來看一下onTouchEvent方法中對點擊事件的具體處理:
public boolean onTouchEvent(MotionEvent event) { // 省略... if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: // 省略... if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } // 省略... break; case MotionEvent.ACTION_DOWN: // 省略... break; case MotionEvent.ACTION_CANCEL: // 省略... break; case MotionEvent.ACTION_MOVE: // 省略... break; } return true; } }
從源碼來看勿她,只要
View
的CLICKABLE
和LONG_CLICKABLE
有一個為true
袄秩,那么它就將消耗這個事件阵翎,即onTouchEvent
返回true
, 不管它是不是DISABLE
狀態(tài)。
而當MOTION_UP
事件發(fā)生時之剧,則觸發(fā)performClick()
方法郭卫,如果View
設置了onClickListener
,那么performClick()
方法內部會調用它的onClick
方法View
的LONG_CLICKABLE
屬性默認為false
,而CLICKABLE
的屬性則和具體的View
有關背稼。通過setClickable
和setLongClickable
方法可以修改這兩個值贰军。此外,在setOnClickListener
中也會自動將CLICKABLE
屬性改為true
蟹肘,而setOnLongClickListener
則將LONG_CLICKABLE
設置為true
词疼。
-
view的滑動沖突
Android中的滑動沖突是比較常見的一個問題,只要在界面中內外兩層同時滑動的時候帘腹,就會產(chǎn)生滑動贰盗。意即有一個占主導地位的View搶著去執(zhí)行滑動操作,從而帶來非常差的用戶體驗阳欲。常見的滑動沖突場景分為如下三種:
場景一:外部滑動方向與內部滑動方向不一致舵盈,主要是將ViewPager和Fragment配合使用所形成的頁面滑動效果。在這個效果中球化,可以通過左右滑動來切換頁面秽晚,而每個頁面內部往往又是一個Listview。這種情況下本來是很容易發(fā)生滑動沖突的筒愚,但ViewPager內部處理了這種滑動沖突赴蝇,所以如果使用ViewPager,則無需擔心這個問題巢掺。但如果使用的是Scroller句伶,則必須手動處理滑動沖突了。否則后果就是內外兩層只能有一層能夠滑動址遇。
處理規(guī)則:當用戶左右滑動時熄阻,需要讓外部的View攔截點擊事件。當用戶上下滑動時倔约,需要讓內部View攔截點擊事件秃殉。這個時候我們就可以根據(jù)它們的特征來解決滑動沖突。具體來說是:根據(jù)滑動的方向判斷到底由什么來攔截事件。場景二:外部滑動和內部滑動方向一致钾军,比如ScrollView嵌套ListView鳄袍,或者是ScrollView嵌套自己。表現(xiàn)在要么只能有一層能夠滑動吏恭,要么兩者滑動起來顯得十分卡頓拗小。
處理規(guī)則:從業(yè)務上尋找突破點,比如業(yè)務上有規(guī)定:當處于某種狀態(tài)時需要外部View處理用戶的操作樱哼,而處理另一種狀態(tài)時則讓內部View處理用戶的操作哀九。場景三:上面兩種情況的嵌套。
處理規(guī)則:同場景二
滑動沖突的解決方式:
針對場景一的滑動沖突搅幅,有兩種處理滑動的解決方式:
- 外部攔截法:
所謂外部攔截法是指點擊事件都先經(jīng)過父容器的攔截處理阅束,如果父容器需要此事件就攔截,如果不需要此事件就不攔截茄唐,這樣就可以解決滑動沖突的問題息裸,這個方法需要重寫父容器的onInterceptTouchEvent()
方法。偽代碼如下所示:@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted=false; int x=(int)event.getX(); int y=(int)event.getY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: intercepted=false; break; case MotionEvent.ACTION_MOVE: if(父容器需要當前點擊事件){ intercepted=true; }else { intercepted=false; } break; case MotionEvent.ACTION_UP: intercepted=false; break; default: break; } mLastXIntercept=x; mLastYIntercept=y; return intercepted; }
- 內部攔截法:
內部攔截法是指父容器不攔截任何事件沪编,所有的事件傳遞給子元素呼盆,如果子元素需要此事件就直接消耗掉,如果不需要則交由父容器處理蚁廓。需要配合requestDisallowInterceptTouchEvent()
方法才能正常工作访圃。偽代碼如下:
另外,為了使父容器不接收@Override public boolean dispatchTouchEvent(MotionEvent event) { int x=(int)event.getX(); int y=(int)event.getY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX=x-mLastX; int deltaY=y-mLastY; if(父容器需要當前點擊事件){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX=x; mLastY=y; return super.dispatchTouchEvent(event); }
ACTION_DOWN
事件纳令,我們需要對父類進行一下修改:
以上兩種方式挽荠,是針對場景一而得出的通用的解決方法。對于場景二和場景三而言平绩,只需改變相關的滑動規(guī)則的邏輯即可圈匆。@Override public boolean onInterceptTouchEvent(MotionEvent event) { int action=event.getAction(); if (action==MotionEvent.ACTION_DOWN){ return false; }else{ return true; } }
注意:因為內部攔截法的操作較為復雜,因此推薦采用外部攔截法來處理常見的滑動沖突捏雌。