android 按键会触发ontouch吗?_这次,我把Android事件分发机制翻了个遍
一名優秀的Android開發,需要一份完備的 知識體系,在這里,讓我們一起成長為自己所想的那樣~。
這次說下Android中的事件分發機制 從開始點擊屏幕開始,就會產生從Activity開始到decorview一直到最里層的view一連串事件傳遞。每一層view或者viewgroup都會首先調用它的dispatchTouchEvent方法,然后判斷是否就在當前一層消費掉事件view的事件分發
首先上一段偽代碼,是在書上看到的,也是我覺得總結的最好的
public boolean dispatchTouchEvent(MotionEvent event) {boolean isConsume = false;if (isViewGroup) {if (onInterceptTouchEvent(event)) {isConsume = onTouchEvent(event);} else {isConsume = child.dispatchTouchEvent(event);}} else {//isViewisConsume = onTouchEvent(event);}return isConsume; }如果當前是viewgroup層級,就會判斷 onInterceptTouchEvent 是否為true,如果為true,則代表事件要消費在這一層級,不再往下傳遞。接著便執行當前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent為false,則代表事件繼續傳遞到下一層級的 dispatchTouchEvent方法,接著一樣的代碼邏輯,一直到最里面一層的view。
ok,還沒完哦,到最里面一層就會直接執行onTouchEvent方法,這時候,view有沒有權利拒絕消費事件呢? 按道理view作為最底層的,應該是沒有發言權才對。但是呢,秉著公平公正原則,view也是可以拒絕的,可以在onTouchEvent方法返回false,表示他不想消費這個事件。那么這個事件又會怎么處理呢?見下面一段偽代碼:
public void handleTouchEvent(MotionEvent event) {if (!onTouchEvent(event)) {getParent.onTouchEvent(event);} }如果view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent又會被調用,如果父容器的onTouchEvent又返回false,則又交給上一級。一直到最上層,也就是Activity的onTouchEvent被調用。
至此,消費流程完畢 但是,關于onTouch,onTouchEvent和onClick又是怎么樣的調用關系呢? 那就再來一段偽代碼:
public void consumeEvent(MotionEvent event) {if (setOnTouchListener) {onTouch();if (!onTouch()) {onTouchEvent(event);}} else {onTouchEvent(event);}if (setOnClickListener) {onClick();} }當某一層view的onInterceptTouchEvent被調用,則代表當前層級要消費事件。如果它的onTouchListener被設置了的話,則onTouch會被調用,如果onTouch的返回值返回true,則onTouchEvent不會被調用。如果返回false或者沒有設置onTouchListener,則會繼續調用onTouchEvent。而onClick方法則是設置了onClickListener則會被正常調用。
這里用一張流程圖總結下:
源碼分析
一個觸摸事件,首先是傳到Activity層級,然后傳到根view,通過一層層的viewgroup最終到底最里面一層的view,我們來一層層解析
Activity(dispatchTouchEvent)
直接上代碼
//Activity.javapublic boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}public void onUserInteraction() {}這里可以看到,onUserInteraction方法是空的,主要是調用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消費了。返回false,就代表下層沒人處理,那就直接到了activity的onTouchEvent方法,這點跟之前的消費傳遞也是吻合的。
繼續看看superDispatchTouchEvent方法,然后就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代碼:
//PhoneWindow.javaprivate DecorView mDecor;@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}//DecorView.javapublic boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}這里可以看到,依次經過了PhoneWindow到達了DecorView,DecorView是activity的根view,也是setcontentView所設置的view的父view,它是繼承自FrameLayout。所以這里super.dispatchTouchEvent(event)方法,其實就是走到了viewgroup的dispatchTouchEvent 方法。
ViewGroup(dispatchTouchEvent)
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {if (onFilterTouchEventForSecurity(ev)) {// Check for interception,表示是否攔截的字段final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {//FLAG_DISALLOW_INTERCEPT標志是通過requestDisallowInterceptTouchEvent設置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;}//mFirstTouchTarget賦值while (target != null) {final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {} else {if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}continue;}}} }這里截取了部分關鍵的代碼,首先是兩個條件
- actionMasked == MotionEvent.ACTION_DOWN
- mFirstTouchTarget != null
如果滿足了其中一個條件才會繼續走下去,執行onInterceptTouchEvent方法等,否則就直接intercepted = true,表示攔截。 第一個條件很明顯,就是表示當前事件位按下事件(ACTION_DOWN) 第二個條件是個字段,根據下面的代碼可以得知,當后面有view消費掉事件的時候,這個mFirstTouchTarget字段就會賦值,否則就為空。
所以什么意思呢,當ACTION_DOWN事件時候,一定會執行到后面代碼。當其他事件來的時候,要看當前viewgroup是否消費了事件,如果當前viewgroup已經消費了事件,沒傳到子view,那么mFirstTouchTarget字段就為空,所以就不會執行到后面的代碼,就直接消費掉所有事件了。 這就符合了之前的所說的一種機制:
某個view一旦開始攔截,那么后續事件就全部就給它處理了,也不會執行onInterceptTouchEvent方法了
但是,兩個條件滿足了一個,就能執行到onInterceptTouchEvent了嗎?不一定,這里看到還有一個判斷條件:disallowIntercept。這個字段是由requestDisallowInterceptTouchEvent方法設置的,后面我們會講到,主要用于滑動沖突,意思就是子view告訴你不想讓你攔截,那么你就不攔截了,直接返回false。
ok,繼續看源碼,之前的內容我們了解到,如果viewgroup不攔截事件,應該會傳遞給子view,那在哪里傳的呢?繼續看看dispatchTouchEvent的代碼:
if (!canceled && !intercepted) {final int childrenCount = mChildrenCount;if (newTouchTarget == null && childrenCount != 0) {final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);if (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}childWithAccessibilityFocus = null;i = childrenCount - 1;}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 indexfor (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);}if (preorderedList != null) preorderedList.clear();}}}這里可以看到,進行了一個子view的便利,其中,如果滿足兩個條件中的一個,就跳出。否則就執行dispatchTransformedTouchEvent方法。先看看這兩個條件:
- !child.canReceivePointerEvents()
- !isTransformedTouchPointInView(x, y, child, null)
看名字是看不出啥了,直接看代碼吧:
protected boolean canReceivePointerEvents() {return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;}protected boolean isTransformedTouchPointInView(float x, float y, View child,PointF outLocalPoint) {final float[] point = getTempPoint();point[0] = x;point[1] = y;transformPointToViewLocal(point, child);final boolean isInView = child.pointInView(point[0], point[1]);if (isInView && outLocalPoint != null) {outLocalPoint.set(point[0], point[1]);}return isInView;}哦,原來是這個意思。canReceivePointerEvents方法就代表view是不是可以接受點擊事件,比如是不是在播放動畫。而isTransformedTouchPointInView方法代表點擊事件的坐標是不是在這個view的區域上面。 ok,如果條件都滿足,就執行到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;} }這個方法大家應該都猜到了,其實就是執行了child.dispatchTouchEvent(event)。也就是下一層view的dispatchTouchEvent方法唄,開始事件的層級傳遞。
View(dispatchTouchEvent)
到view 層級的時候,自然就執行的view的dispatchTouchEvent,上代碼
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo 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;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;}這里可以看到,首先會判斷li.mOnTouchListener != null,如果不為空,就會執行onTouch方法。 根據onTouch方法返回的結果,如果為false,result就為false,那么onTouchEvent才會執行。這個邏輯也是符合我們之前說的傳遞方式。
最后我們再看看view的onTouchEvent都做了什么事:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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)) {performClickInternal();}}}}mIgnoreNextUpEvent = false;break;}return true;}從代碼可以得知,如果設置了CLICKABLE或者LONG_CLICKABLE,那么這個view就會消費事件,并且執行performClickInternal方法,然后執行到performClick方法。這個performClick方法大家應該都很熟悉,就是觸發點擊的方法,其實內部就是執行了onClick方法。
private boolean performClickInternal() {notifyAutofillManagerOnClick();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;}至此,源代碼也看的差不多了,內部其實有很多細節,這里也就不一一說明了,大家有空可以去研究下。
事件分發的應用(requestDisallowInterceptTouchEvent)
那既然學會了事件分發機制,我們實際工作中會怎么應用呢?其實最常見的就是解決滑動沖突的問題。一般有兩種解決辦法:
- 一種是外部攔截:從父view端處理,根據情況決定事件是否分發到子view
- 一種是內部攔截:從子view端處理,根據情況決定是否阻止父view進行攔截,其中的關鍵就是requestDisallowInterceptTouchEvent方法。
第一種方法,其實就是在onInterceptTouchEvnet方法里面進行判斷返回true還是返回false。 第二種方法,就是用到了requestDisallowInterceptTouchEvent方法,這個方法的意思就是讓父view不要去攔截事件了,在dispatchTouchEvent方法里面就有這個標志位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段為true,就不會去執行onInterceptTouchEvent方法,而是返回false,不攔截事件。
上代碼:
//外部攔截法:父view.java @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {boolean intercepted = false;//父view攔截條件boolean parentCanIntercept;switch (ev.getActionMasked()) {case MotionEvent.ACTION_DOWN:intercepted = false;break;case MotionEvent.ACTION_MOVE:if (parentCanIntercept) {intercepted = true;} else {intercepted = false;}break;case MotionEvent.ACTION_UP:intercepted = false;break;}return intercepted;}外部攔截很簡單,就是判斷條件,然后決定是否進行攔截。
//父view.java @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}//子view.java@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {//父view攔截條件boolean parentCanIntercept;switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:getParent().requestDisallowInterceptTouchEvent(true);break;case MotionEvent.ACTION_MOVE:if (parentCanIntercept) {getParent().requestDisallowInterceptTouchEvent(false);}break;case MotionEvent.ACTION_UP:break;}return super.dispatchTouchEvent(event);}感覺內部攔截有點復雜呀,還要重寫父view的方法,這里分析下,為什么要去這么寫:
- 父view ACTION_DOWN的時候,不能攔截,因為如果攔截,那么后續事件也就跟子view無關了
- 父view 其他事件的時候,要返回true,表示攔截。因為onInterceptTouchEvent方法的調用是被FLAG_DISALLOW_INTERCEPT標志位所控制,所以子view需要父view攔截的時候,才會走到這個onInterceptTouchEvent方法中來,那么這時候要保證方法中一定是要攔截的。
至此,事件的分發機制也就說的差不多了。有說的不對的地方望指正,謝謝。
你的一個 ,就是我分享的動力??。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的android 按键会触发ontouch吗?_这次,我把Android事件分发机制翻了个遍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络带宽由什么决定_你的二手奢侈品价格到
- 下一篇: 中金所技术各部门介绍_【电视技术】液晶电