Android——View的事件分發

View的事件分發機制

參考《Android進階之光》

解析Activity構成

點擊事件用MotionEvent表示。

當一個點擊事件產生後,事件最先傳遞個Activity。首先看一下setContentView()方法。

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

調用了getWindow()對應的方法

    public Window getWindow() {
        return mWindow;
    }

在Activity的attach()方法中發現了mWindow

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        ······
    }

原來mWindow指的就是PhoneWindow,PhoneWindow是繼承抽象類Window的,這樣就知道了getWindow()得到的是一個PhoneWindow,因爲Activity中setContentView()方法調用的是 getWindow().setContentView(layoutResID)。

    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

調用了installDecor() 方法

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            ······

跟進註釋1處的generateDecor()方法

    protected DecorView generateDecor(int featureId) {
        return new DecorView(context, featureId, this, getAttributes());
    }

這裏創建了一個DecorView,這個DecorView就是Activity中的根View。接着查看DecorView的源碼,發現DecorView是PhoneWindow類的內部類,並且繼承了FrameLayout。我們再看看generateLayout(mDecor)做了什麼:

        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            ······

PhoneWindow的generateLayout()方法比較長,這裏只截取了一小部分關鍵的代碼,其主要內容就是根據不同的情況加載不同的佈局給layoutResource。現在查看上面代碼註釋1處的佈局R.layout.screen_title,這個文件在frameworks,它的代碼如下所示:

在這裏插入圖片描述
查看源碼,可得到以下結論:

  • 上面的ViewStub是用來顯示Actionbar的。下面的兩個FrameLayout:一個是title,用來顯示標題;另一個是content,用來顯示內容。看到上面的源碼,大家就知道了一個Activity包含一個Window對象,這個對象是由PhoneWindow來實現的
  • PhoneWindow將DecorView作爲整個應用窗口的根 View,而這個 DecorView 又將屏幕劃分爲兩個區域:一個是 TitleView,另一個是ContentView,而我們平常做應用所寫的佈局正是展示在ContentView中的
  • 在這裏插入圖片描述

源碼解析View的事件分發機制

概述

  • 當我們點擊屏幕時,就產生了點擊事件,這個事件被封裝成了一個類:MotionEvent
  • MotionEvent產生後,那麼系統就會將這個MotionEvent傳遞給View的層級MotionEvent在View中的層級傳遞過程就是點擊事件分發

點擊事件有3個重要的方法:

  • dispatchTouchEvent(MotionEvent ev)——用來進行事件的分發
  • onInterceptTouchEvent(MotionEvent ev)——用來進行事件的攔截在dispatchTouchEvent()中調用,需要注意的是View沒有提供該方法。
  • onTouchEvent(MotionEvent ev)——用來處理點擊事件,在dispatchTouchEvent()方法中進行調用。

分發機制

  • 當點擊事件產生後,事件首先會傳遞給當前的 Activity,這會調用 Activity 的dispatchTouchEvent()方法,具體工作都是交由Activity中的PhoneWindow來完成的,然後PhoneWindow再把事件處理工作交給DecorView,之後再由DecorView將事件處理工作交給根ViewGroup

ViewGroup的dispatchTouchEvent()

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 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();
            }

            // Check for interception.
            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;
            }


  • 首先判斷事件是否爲DOWN事件,如果是,則進行初始化,resetTouchState方法中會把mFirstTouchTarget的值置爲null,爲什麼要進行初始化呢?原因就是一個完整的事件序列是以DOWN開始,以UP結束的。所以如果是DOWN事件,那麼說明這是一個新的事件序列,故而需要初始化之前的狀態。
  • 代碼註釋1處的條件如果滿足,則執行下面的句子,mFirstTouchTarget 的意義是:當前 ViewGroup 是否攔截了事件,如果攔截了,mFirstTouchTarget=null;如果沒有攔截並交由子View來處理,則mFirstTouchTarget!=null假設當前的 ViewGroup 攔截了此事件,mFirstTouchTarget!=null 則爲 false,如果這時觸發ACTION_DOWN 事件則會執 行 onInterceptTouchEvent(ev) 方法如 果 觸發的 是ACTION_MOVE、ACTION_UP事件,則不再執行onInterceptTouchEvent(ev)方法而是直接設置intercepted=true,此後的一個事件序列均由這個ViewGroup處理
  • 註釋2 處出現了一個FLAG_DISALLOW_INTERCEPT 標誌位,它主要是禁止ViewGroup 攔截除了DOWN之外的事件一般通過子View的requestDisallowInterceptTouchEvent來設置

總結一下:

當ViewGroup要攔截事件的時候,那麼後續的事件序列都將交給它處理,而不用再調用onInterceptTouchEvent()方法了。所以,onInterceptTouchEvent()方法並不是每次事件都會調用的。

onInterceptTouchEvent()方法:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
  • onInterceptTouchEvent()方法默認返回false,不進行攔截。如果想要讓ViewGroup攔截事件,那麼應該在自定義的ViewGroup中重寫這個方法

dispatchTouchEvent()方法剩餘的部分

  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 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);
  }
  • 註釋1處我們看到了for循環。首先遍歷ViewGroup的子元素,判斷子元素是否能夠接收到點擊事件,如果子元素能夠接收到點擊事件,則交由子元素來處理。需要注意這個for循環是倒序遍歷的,即從最上層的子View開始往內層遍歷。
  • 註釋2處的代碼,其意思是判斷觸摸點位置是否在子View的範圍內或者子View是否在播放動畫。如果均不符合則執行continue語句,表示這個子View不符合條件,開始遍歷下一個子View。

註釋3處的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的dispatchTouchEvent(event),如果子view的dispatchTouchEvent方法返回爲true,dispatchTransformedTouchEvent方法返回爲true,for循環結束

當子view的dispatchTouchEvent方法返回爲false,然後這個循環不會結束會接着調用dispatchTransformedTouchEvent方法

   handled = dispatchTransformedTouchEvent(ev, canceled, null,
           TouchTarget.ALL_POINTER_IDS);

此時傳入的child爲null,會調用

handled = super.dispatchTouchEvent(event);

也就是View的dispatchTouchEvent方法(如下),也就意味值子View不處理,我自己處理,如果我自己的方法還沒有處理返回給我的父親爲false,那麼我的父親和我一樣也會調用他的super.dispatchTouchEvent(event); 接着調用onTouch或onTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) { 
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
            }
        }
  • 如果OnTouchListener不爲null並且onTouch方法返回true,則表示事件被消費就不會執行onTouchEvent(event),否則就會執行onTouchEvent(event)。
  • 可以看出 OnTouchListener中的onTouch()方法優先級要高於onTouchEvent(event) 方法。

onTouchEvent()方法的部分源碼:

在這裏插入圖片描述
在這裏插入圖片描述

  • 從上面的代碼可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼onTouchEvent()就會返回true消耗這個事件。CLICKABLE和LONG_CLICKABLE代表View可以被點擊和長按點擊,可以通過View的setClickable和setLongClickable方法來設置,也可以通過View的setOnClickListenter和setOnLongClickListener來設置,它們會自動將View設置爲CLICKABLE和LONG_CLICKABLE。

接着在ACTION_UP事件中會調用performClick()方法:

在這裏插入圖片描述

  • 上面代碼註釋 1 處可以看出,如果 View 設置了點擊事件 OnClickListener,那麼它的onClick()方法就會被執行。View事件分發機制的源碼分析就講到這裏了,接下來介紹點擊事件分發的傳遞規則。

事件分發的傳遞規則

由前面事件分發機制的源碼分析可知點擊事件分發的這3個重要方法的關係,用僞代碼來簡單表示:

在這裏插入圖片描述
onInterceptTouchEvent方法和onTouchEvent方法都在dispatchTouchEvent方法中調用。現在我們根據這段僞代碼來分析一下點擊事件分發的傳遞規則。

點擊事件由上而下的傳遞規則

  • 點擊事件產生後會由 Activity 來處理,傳遞給PhoneWindow,再傳遞給DecorView,最後傳遞給頂層的ViewGroup。一般在事件傳遞中只考慮 ViewGroup 的 onInterceptTouchEvent 方法,因爲一般情況下我們不會重寫 dispatchTouch-Event()方法。對於根ViewGroup,點擊事件首先傳遞給它的dispatchTouchEvent()方法,如果該ViewGroup的onInterceptTouchEvent()方法返回true,則表示它要攔截這個事件,這個事件就會交給它的onTouchEvent()方法處理;如果onInterceptTouchEvent()方法返回false,則表示它不攔截這個事件,則這個事件會交給它的子元素的dispatchTouchEvent()來處理,如此反覆下去。如果傳遞給底層的View,View是沒有子View的,就會調用View的dispatchTouchEvent()方法,一般情況下最終會調用View的onTouchEvent()方法。

點擊事件由下而上的傳遞

  • 當點擊事件傳給底層的 View 時,如果其onTouchEvent()方法返回true,則事件由底層的View消耗並處理;如果返回false則表示該View不做處理,則傳遞給父View的onTouchEvent()處理;如果父View的onTouchEvent()仍舊返回false,則繼續傳遞給該父View的父View處理,如此反覆下去。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章