編輯:關於android開發
相關文章
Android View體系(一)視圖坐標系
Android View體系(二)實現View滑動的六種方法
Android View體系(三)屬性動畫
Android View體系(四)從源碼解析Scroller
三年前寫過事件分發機制的文章但是寫的不是很好,所以重新再寫一篇,關於事件分發機制的文章已經有很多,但是希望我這篇是最簡潔、最易懂的一篇。
我們知道View的結構是樹形的結構,View可以放在ViewGroup中,這個ViewGroup也可以放到另一個ViewGroup中,這樣層層的嵌套就組成了View的層級。
當我們點擊屏幕,就產生了觸摸事件,這個事件被封裝成了一個類:MotionEvent。而當這個MotionEvent產生後,那麼系統就會將這個MotionEvent傳遞給View的層級,MotionEvent在View的層級傳遞的過程就是點擊事件分發。
點擊事件有三個重要的方法它們分別是:
dispatchTouchEvent(MotionEvent ev):用來進行事件的分發 onInterceptTouchEvent(MotionEvent ev):用來進行事件的攔截,在dispatchTouchEvent()中調用,需要注意的是View沒有提供該方法 onTouchEvent(MotionEvent ev):用來處理點擊事件,在dispatchTouchEvent()方法中進行調用為了了解這三個方法的關系,我們先來看看ViewGroup的dispatchTouchEvent()方法的部分源碼:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...省略
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;
}
...省略
return handled;
}
很明顯在dispatchTouchEvent()方法中調用了onInterceptTouchEvent()方法來判斷是否攔截事件,來看看onInterceptTouchEvent()方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
onInterceptTouchEvent()方法默認返回false,不進行攔截,接著來看看dispatchTouchEvent()方法剩余的部分源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
...省略
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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
...省略
}
我們看到了for循環,首先遍歷ViewGroup的子元素,判斷子元素是否能夠接收到點擊事件,如果子元素能夠接收到則交由子元素來處理。接下來看看37行的dispatchTransformedTouchEvent()方法中實現了什麼:
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);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...省略
}
如果有子View則調用子View的dispatchTouchEvent(event)方法。如果ViewGroup沒有子View則調用super.dispatchTouchEvent(event),ViewGroup是繼承View的,我們再來看看View的dispatchTouchEvent(event):
public boolean dispatchTouchEvent(MotionEvent event) {
...省略
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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不為null並且onTouch()方法返回true,則表示事件被消費,就不會執行onTouchEvent(event),否則就會執行onTouchEvent(event)。再來看看onTouchEvent()方法的部分源碼:
public boolean onTouchEvent(MotionEvent event) {
...省略
final int action = event.getAction();
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 (!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();
}
}
}
...省略
}
return true;
}
return false;
}
上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一個為true,那麼onTouchEvent就會返回true消耗這個事件。CLICKABLE和LONG_CLICKABLE代表View可以被點擊和長按點擊,可以通過View的setClickable和setLongClickable方法來設置,也可以通過View的setOnClickListenter和setOnLongClickListener來設置,他們會自動將View的設置為CLICKABLE和LONG_CLICKABLE。
接著在ACTION_UP事件會調用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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
如果View設置了點擊事件OnClickListener,那麼它的onClick()方法就會被執行。
看到這裡我們就可以知道點擊事件分發的這三個重要方法的關系,用偽代碼來簡單表示就是:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result=false;
if(onInterceptTouchEvent(ev)){
result=super.onTouchEvent(ev);
}else{
result=child.dispatchTouchEvent(ev);
}
return result;
當點擊事件產生後會由Activity來處理在傳遞給Window再傳遞給頂層的ViewGroup,一般在事件傳遞中只考慮ViewGroup的onInterceptTouchEvent()方法,因為一般情況我們不會去重寫dispatchTouchEvent()方法。
對於根ViewGroup,點擊事件首先傳遞給它的dispatchTouchEvent()方法,如果該ViewGroup的onInterceptTouchEvent()方法返回true,則表示它要攔截這個事件,這個事件就會交給它的onTouchEvent()方法處理,如果onInterceptTouchEvent()方法返回false,則表示它不攔截這個事件,則交給它的子元素的dispatchTouchEvent()來處理,如此的反復下去。如果傳遞給最底層的View,View是沒有子View的,就會調用View的dispatchTouchEvent()方法,一般情況下最終會調用View的onTouchEvent()方法。
舉個現實的例子,就是我們的應用產生了重大的bug,這個bug首先會匯報給技術總監那:
技術總監(頂層ViewGroup)→技術經理(中層ViewGroup)→工程師(底層View)
技術總監不攔截,把bug分給了技術經理,技術經理不攔截把bug分給了工程師,工程師沒有下屬只有自己處理了。
事件由上而下傳遞返回值規則為:true,攔截,不繼續向下傳遞;false,不攔截,繼續向下傳遞。
點擊事件傳給最底層的View,如果他的onTouchEvent()方法返回true,則事件由最底層的View消耗並處理了,如果返回false則表示該View不做處理,則傳遞給父View的onTouchEvent()處理,如果父View的onTouchEvent()仍舊返回返回false,則繼續傳遞給改父View的父View處理,如此的反復下去。
再返回我們現實的例子,工程師發現這個bug太難搞不定(onTouchEvent()返回false),他只能交給上級技術經理處理,如果技術經理也搞不定(onTouchEvent()返回false),那就把bug傳給技術總監,技術總監一看bug很簡單就解決了(onTouchEvent()返回true)。
事件由下而上傳遞返回值規則為:true,處理了,不繼續向上傳遞;false,不處理,繼續向上傳遞。
Android中SQLite應用詳解 上次我向大家介紹了SQLite的基本信息和使用過程,相信朋友們對SQLite已經有所了解了,那今天呢,我就和大家分享一下在Andro
viewpager和fragment預加載的解決,viewpagerfragment在使用Viewpager和fragment處理中會出現預加載的問題,最近看別人的代碼,
Android動畫全解,Android動畫在Android開發中經常會碰到動畫,看到別的應用有很酷炫的應用時,總是想怎麼去實現,但是每次都是發現感覺是知道怎麼做的,實際做
Android 圖片的顏色處理,仿造美圖秀秀移動鼠標調整seekbar,調整圖片的顏色 項目布局如下: <LinearLayout xmlns:android=h