從 ViewRoot 來分析 TouchEvent 觸控事件
阿新 • • 發佈:2019-02-19
在 ViewRoot 中:
有這幾個資料成員:
InputChannel mInputChannel; InputQueue.Callback mInputQueueCallback; InputQueue mInputQueue; private final InputHandler mInputHandler = new InputHandler() { public void handleKey(KeyEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchKey(event, true); } public void handleMotion(MotionEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchMotion(event, true); } };
這個 mInputHandler 是在 setView 中註冊的:
/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; mWindowAttributes.copyFrom(attrs); attrs = mWindowAttributes; if (view instanceof RootViewSurfaceTaker) { mSurfaceHolderCallback = ((RootViewSurfaceTaker)view).willYouTakeTheSurface(); if (mSurfaceHolderCallback != null) { mSurfaceHolder = new TakenSurfaceHolder(); mSurfaceHolder.setFormat(PixelFormat.UNKNOWN); } } Resources resources = mView.getContext().getResources(); CompatibilityInfo compatibilityInfo = resources.getCompatibilityInfo(); mTranslator = compatibilityInfo.getTranslator(); if (mTranslator != null || !compatibilityInfo.supportsScreen()) { mSurface.setCompatibleDisplayMetrics(resources.getDisplayMetrics(), mTranslator); } boolean restore = false; if (mTranslator != null) { restore = true; attrs.backup(); mTranslator.translateWindowLayout(attrs); } if (DEBUG_LAYOUT) Log.d(TAG, "WindowLayout in setView:" + attrs); if (!compatibilityInfo.supportsScreen()) { attrs.flags |= WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW; } mSoftInputMode = attrs.softInputMode; mWindowAttributesChanged = true; mAttachInfo.mRootView = view; mAttachInfo.mScalingRequired = mTranslator != null; mAttachInfo.mApplicationScale = mTranslator == null ? 1.0f : mTranslator.applicationScale; if (panelParentView != null) { mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); } mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); mInputChannel = new InputChannel(); try { res = sWindowSession.add(mWindow, mWindowAttributes, getHostVisibility(), mAttachInfo.mContentInsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; unscheduleTraversals(); throw new RuntimeException("Adding window failed", e); } finally { if (restore) { attrs.restore(); } } if (mTranslator != null) { mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets); } mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingVisibleInsets.set(0, 0, 0, 0); if (Config.LOGV) Log.v(TAG, "Added window " + mWindow); if (res < WindowManagerImpl.ADD_OKAY) { mView = null; mAttachInfo.mRootView = null; mAdded = false; unscheduleTraversals(); switch (res) { case WindowManagerImpl.ADD_BAD_APP_TOKEN: case WindowManagerImpl.ADD_BAD_SUBWINDOW_TOKEN: throw new WindowManagerImpl.BadTokenException( "Unable to add window -- token " + attrs.token + " is not valid; is your activity running?"); case WindowManagerImpl.ADD_NOT_APP_TOKEN: throw new WindowManagerImpl.BadTokenException( "Unable to add window -- token " + attrs.token + " is not for an application"); case WindowManagerImpl.ADD_APP_EXITING: throw new WindowManagerImpl.BadTokenException( "Unable to add window -- app for token " + attrs.token + " is exiting"); case WindowManagerImpl.ADD_DUPLICATE_ADD: throw new WindowManagerImpl.BadTokenException( "Unable to add window -- window " + mWindow + " has already been added"); case WindowManagerImpl.ADD_STARTING_NOT_NEEDED: // Silently ignore -- we would have just removed it // right away, anyway. return; case WindowManagerImpl.ADD_MULTIPLE_SINGLETON: throw new WindowManagerImpl.BadTokenException( "Unable to add window " + mWindow + " -- another window of this type already exists"); case WindowManagerImpl.ADD_PERMISSION_DENIED: throw new WindowManagerImpl.BadTokenException( "Unable to add window " + mWindow + " -- permission denied for this window type"); } throw new RuntimeException( "Unable to add window -- unknown error code " + res); } if (view instanceof RootViewSurfaceTaker) { mInputQueueCallback = ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue(); } if (mInputQueueCallback != null) { mInputQueue = new InputQueue(mInputChannel); mInputQueueCallback.onInputQueueCreated(mInputQueue); } else { InputQueue.registerInputChannel(mInputChannel, mInputHandler, //這個地方注意一下. Looper.myQueue()); } view.assignParent(this); mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0; mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0; } } }
這裡最主要的是 mInputHandler 中的 handleMotion 中呼叫到了 dispatchMotion 方法:
public void handleMotion(MotionEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchMotion(event, true); }
見 ViewRoot 中的 dispatchMotion 方法:
private void dispatchMotion(MotionEvent event, boolean sendDone) { int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { dispatchPointer(event, sendDone); //這個地方! } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { dispatchTrackball(event, sendDone); } else { // TODO Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event); if (sendDone) { finishInputEvent(); } } } private void dispatchPointer(MotionEvent event, boolean sendDone) { Message msg = obtainMessage(DISPATCH_POINTER); //發出這樣的訊息. msg.obj = event; msg.arg1 = sendDone ? 1 : 0; sendMessageAtTime(msg, event.getEventTime()); }
處理這個訊息:
case DISPATCH_POINTER: {//觸控事件訊息的處理 MotionEvent event = (MotionEvent) msg.obj; try { deliverPointerEvent(event);//走入到這裡.----這個地方 } finally { event.recycle(); //處理完這個事件後, 把這個event回收掉. if (msg.arg1 != 0) { finishInputEvent();//向發出訊息模組發一個回執, 以便進行下一次的訊息派發. } if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!"); } } break; private void deliverPointerEvent(MotionEvent event) { if (mTranslator != null) { mTranslator.translateEventInScreenToAppWindow(event);//物理座標向邏輯座標的轉換. } boolean handled; if (mView != null && mAdded) { // enter touch mode on the down boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN; if (isDown) { ensureTouchMode(true);//進入觸控模式.----這個方法見下面 } if(Config.LOGV) { captureMotionLog("captureDispatchPointer", event); } if (mCurScrollY != 0) { event.offsetLocation(0, mCurScrollY); } if (MEASURE_LATENCY) { lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano()); } //進行事件的派發, 對view和activity系統產生影響. 見 DecorView 和 ViewGroup中的方法. handled = mView.dispatchTouchEvent(event); //--------------------------這句話是最重要的. if (MEASURE_LATENCY) { lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano()); } if (!handled && isDown) {//對於上面沒有處理的事件, 進行螢幕邊界偏移.螢幕偏移用(edge slop)進行表示. //它的作用是當用戶正好觸控到螢幕邊界時,系統自動對原始訊息進行一定的偏移, //然後在新的偏移後的位置上尋找是否有匹配的檢視, //為什麼要有"螢幕偏移"呢? 因為對於觸控式螢幕而言, 尤其是電容觸控式螢幕, 人類手指尖有一定的大小, //當觸控到邊界時, 力量會被自動吸附到螢幕邊界, //所以, 此處根據上下左右不同的邊界物件訊息原始位置進行一定的偏移. int edgeSlop = mViewConfiguration.getScaledEdgeSlop(); final int edgeFlags = event.getEdgeFlags(); int direction = View.FOCUS_UP; int x = (int)event.getX(); int y = (int)event.getY(); final int[] deltas = new int[2]; if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) { direction = View.FOCUS_DOWN; if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { deltas[0] = edgeSlop; x += edgeSlop; } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { deltas[0] = -edgeSlop; x -= edgeSlop; } } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) { direction = View.FOCUS_UP; if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { deltas[0] = edgeSlop; x += edgeSlop; } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { deltas[0] = -edgeSlop; x -= edgeSlop; } } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { direction = View.FOCUS_RIGHT; } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { direction = View.FOCUS_LEFT; } if (edgeFlags != 0 && mView instanceof ViewGroup) { View nearest = FocusFinder.getInstance().findNearestTouchable( ((ViewGroup) mView), x, y, direction, deltas); if (nearest != null) { event.offsetLocation(deltas[0], deltas[1]); event.setEdgeFlags(0); mView.dispatchTouchEvent(event); } } } } } 其中 ensureTouchMode 如下所示: boolean ensureTouchMode(boolean inTouchMode) {//進否進入觸控模式.---即 非觸控模式 與 觸控模式 之間的切換. if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current " + "touch mode is " + mAttachInfo.mInTouchMode); //如果當前觸控 與 原來的觸控模式 相同, 則沒有改變, 所以返回false. if (mAttachInfo.mInTouchMode == inTouchMode) return false; // tell the window manager----即通知window----因為 wms在佈局視窗時, 會根據不同的touch模式進行不同的處理 try { //通知視窗, WmS在進行客戶窗口布局時, 需要根據客戶視窗的Touch模式進行不同的處理. sWindowSession.setInTouchMode(inTouchMode); } catch (RemoteException e) { throw new RuntimeException(e); } // handle the change ----view自身的改變.----如清除焦點, 或者requestFocus 之類的 可能涉及 介面更新的操作. return ensureTouchModeLocally(inTouchMode);//---點進去去看下. 這個方法 其實就在這下面. }
如果這個 mView是 DecorView 而言, 執行這個:
public boolean dispatchTouchEvent(MotionEvent ev) { //注意, activity實現了 Window.CallBack介面, 這裡獲得的cb, 就是這個activity. final Callback cb = getCallback(); return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super .dispatchTouchEvent(ev); }
- 如果 cb為空, 則直接執行 ViewGroup中的 dispatchTouchEvent方法.—-下面會講到.
如果 cb不為空, 則執行 activity中的 dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { //如果是down事件的話, activity有機會在事件響應之前做點事情. onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { //調的是PhoneWindow中的superDispatchTouchEvent //--->DecorView中的superDispatchTouchEvent //--->ViewGroup中的dispatchTouchEvent方法. return true; } return onTouchEvent(ev); //如果view系統不處理, 則呼叫 activity中的 onTouchEvent. }
如果這個 mView直接就是 ViewGroup的話, 那直接調到 ViewGroup中的 dispatchTouchEvent.
反正先 處理 viewgroup的 dispatchTouchEvent, 如果沒有消化掉, 才去處理 activity中的 onTouchEvent方法.
至於 ViewGroup中的 dispatchTouchEvent(event)方法:
/** * {@inheritDoc} */ @Override public boolean dispatchTouchEvent(MotionEvent ev) {//touch到時, 會從 ViewRoot那裡調到 ViewGroup的這個方法. if (!onFilterTouchEventForSecurity(ev)) { return false; } final int action = ev.getAction(); //當前ViewGroup佈局座標系的座標. 當前ViewGroup檢視座標原點在佈局座標系中的位置為(-mScrollX, -mScrollY) final float xf = ev.getX(); final float yf = ev.getY(); //座標系 轉換成 當前ViewGroup檢視座標系的座標. 這個混算要整明白.----不要誤以為是child什麼的. //因為當前這個viewgroup可能會在scroll的, //所以要算上(mScrollX, mScrollY)來得到這個觸控點相對於當前這個Viewgroup檢視座標原點的座標. final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//true表示不允許攔截 if (action == MotionEvent.ACTION_DOWN) { //先處理 action_down 情況 if (mMotionTarget != null) {//這個mMotionTarget是指這個viewgroup中的捕獲事件的child. // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. //當action_down時, 通常情況下, 這個mMotionTarget當然應為null. 不為空則可能是出錯的. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowIntercept || !onInterceptTouchEvent(ev)) {//不允許攔截 或者 沒有攔截 // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN);//重置 // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat;//檢視座標 final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren;//要對孩子進行遍歷, 這個孩子可能是相鄰, 也可能是相互前後疊加. final int count = mChildrenCount; //被觸控的點處, 可能會疊加多個孩子. //讓序號最後面的child先拿事件試試, 如果不要的話, 再讓序號前面的孩子拿事件. for (int i = count - 1; i >= 0; i--) {//遍歷孩子, 確定孩子要不要這個down事件. final View child = children[i]; //只有當child是可見或者動畫時, 才可以響應這個down. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame);//獲得該child的佈局區域----父view檢視座標系中的. if (frame.contains(scrolledXInt, scrolledYInt)) {//判斷點選的位置是否在這個child上. // offset the event to the view's coordinate system //座標系切到孩子的佈局座標系統上. 這個要理解好. final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; //點選事件在child上, 則現在座標轉換到以child的原點為基準,---但非child顯示區域座標啊. ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; if (child.dispatchTouchEvent(ev)) {//交給child來分發了. // Event handled, we have a target now. //如果孩子消費了這個down事件, 則這個mMotionTarget就記錄這個孩子, 然後返回. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||//指是不是up或cancel事件, true表示是. (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { //如果現在的事件是up或者cancel掉了, 那麼應當允許攔截. 因為在按下還沒有釋放時, 要攔截訊息的. // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event //現在允許攔截.----因為 這一系列的(down/move/up/cacel)事件 已經結束了! //----所以沒有是否允許攔截的意義了. //----即 設一個不允許攔截, 其有效期僅這麼一套down/move/up/cancel週期而已. mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; if (target == null) { //如果child沒有消耗這個down事件的話, 說明move和up也不會響應. 所以, 應由這個viewgroup自己響應. // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf);//座標移回viewgroup自己的座標體系, 即佈局座標. if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {//這個CANCEL_NEXT_UP_EVENT通常是不存在的 ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev);//呼叫viewgroup的view部分的dispatchTouchEvent方法. } //如果有孩子響應了down事件, 那麼往下走.----上面剛處理的是 沒有孩子響應的事情, 現在處理有孩子響應的情況. // if have a target, see if we're allowed to and want to intercept its // events if (!disallowIntercept && onInterceptTouchEvent(ev)) { //如果允許viewgroup截獲, 並且確實被viewgroup截獲了, //那麼child應當放棄down,move,up事件, 所以下面用cancel來取消child. //這個target是指捕獲down事件的child //即, 將獲得點選位置---即以child的佈局座標系統來算的. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL);//事件改為action_cancel事件. ev.setLocation(xc, yc);//座標換到child的佈局座標來. if (!target.dispatchTouchEvent(ev)) { //用於讓child處理cancel事件 //----因為原來的事件被父viewgroup給攔截了,所以用cancel來逐個逐級通知child處理cancel. // target didn't handle ACTION_CANCEL. not much we can do //這個cancel事件可以通知child去取消之前對事件的追蹤, 如長按, 特定手勢之類. // but they should have. } // clear the target mMotionTarget = null;//把其置為null // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true;//返回true, 表示事件被消耗了.----這裡是被viewgroup消耗了, 而不是child. } // if (isUpOrCancel) {//true表示當前事件是 up或cancel事件, //表示 事件處理 處於 尾聲了. //mMotionTarget置回空, 不過target仍在, 以便下面呼叫 target.dispatchTouchEvent. //對於 move事件, 因為事件 後面還會有, 所以 mMotionTarget不能為空的. mMotionTarget = null; } //下面這些, 都是指 由child來響應 move, up事件! // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft;//轉到child的佈局座標方式. final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc);//更改座標系統為child的佈局座標方式. if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { //通常不走進來.這個CANCEL_NEXT_UP_EVENT表示 取消 隨後的up事件. ev.setAction(MotionEvent.ACTION_CANCEL); //走進來的話, 表示要取消隨後的up事件, 所以事件改為cancel事件. target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; //然後 也把 這個置為null } return target.dispatchTouchEvent(ev);//由child來處理這個move和up事件.----以及可能的cancel事件. }
View 中的 dispatchTouchEvent 方法:
//這裡注意的是: 這裡先處理 外界設定的 OnTouchEventListener // 如果返回 true, 說明外界要 搶佔 這個事件, 所以不執行 控制元件自身的 onTouchEvent. // 如果返回 false, 說明外界 認為可以 把這個事件 分發給 控制元件自身的 onTouchEvent處理. //主要是這點: //(1) 提供了一個介面給外界設定, 即通過 setOnTouchEventListener 設定一個監聽器. //(2) 自身處理的方法: onTouchEvent ----在自定義一個view時寫的. //優先執行 外界的要求(即監聽器中的方法), 如果返回 false, 才去執行 控制元件自身的onTouchEvent. public boolean dispatchTouchEvent(MotionEvent event) { if (!onFilterTouchEventForSecurity(event)) { //處理當視窗處於模糊狀態下的事件.---返回true表示, 事件應當處理; 為false時, 表示事件不處理. return false; } if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { //如果 ENABLE, 並且該view註冊了OnTouchListener監聽器, 則執行這個監聽器的onTouch, 處理完直接返回true return true; } return onTouchEvent(event); //如果沒有設定監聽器, 則執行 onTouchEvent方法. }
View 中的 onTouchEvent 方法:
這個呢, 在其它的筆記中已做了說明, 這裡就不列出來了.