View事件分发

Posted by CoXier on November 11, 2016

Android Dispatches Touch Event

在分析Android系统是如何分发触摸事件之前,我们应该先对整个Android View的层次结构有个清晰的认识

本次分析的源码为Android API 23

Android View

先看图:

ActivityThread#handleLaunchActivity中启动Activity,之后调用Activity#onCreate方法,一般我们的Activity中会有:

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
   }

我们关注的当然是和View有关的方法:setContentView(...),具体实现:

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

从注释中可以看出该方法设置了Activity的内容。getWindow返回的是一个WindowWindow是一个abstract class,文档解释:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */

可以知道PhoneWindowWindow唯一的实现,源码在Android Studio上看不到,传送门

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

mContentParent是个ViewGroup类型,进入installDecor(),由于代码量太大,简化:

private void installDecor() {
    if (mContentParent == null) {
         mContentParent = generateLayout(mDecor);
      }
}

传入的参数是DecorView,它是PhoneWindow的一个内部类,继承FrameLayout。

generateLayout(mDecor):

    // Apply data from current theme.

      TypedArray a = getWindowStyle();

      if (false) {
          System.out.println("From style:");
          String s = "Attrs:";
          for (int i = 0; i < R.styleable.Window.length; i++) {
              s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
                      + a.getString(i);
          }
          System.out.println(s);
      }

      ····
      // Inflate the window decor.
       ···
       int layoutResource;
       int features = getLocalFeatures();
       if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
           layoutResource = R.layout.screen_swipe_dismiss;
       }

       ···
       mDecor.startChanging();

       View in = mLayoutInflater.inflate(layoutResource, null);
       decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
       mContentRoot = (ViewGroup) in;

       ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
       if (contentParent == null) {
           throw new RuntimeException("Window couldn't find content container view");
       }
       ···

获取theme的相关值后,根据features的值来给layoutResource赋相应的值,例如平常我们设置Theme.AppCompat.Light.NoActionBar之后,这里就会给layoutResource传相应的layout id。 然后加载layoutResource对应的View in,并把in添加到decor中。contentParent对应的Id是ID_ANDOIRD_CONTENT。

再回到 PhoneWindow的setContentView的方法中,看到如下一行:

mLayoutInflater.inflate(layoutResID, mContentParent);

这里也表明了mContentParent承载了整个Activity的内容视图

How Android Handles Touches

MotionEvent

用户所有的触摸操作都被包装成 MotionEventMotionEvent用来描述用户的动作的几个常量:

  • MotionEvent.ACTION_DOWN
  • MotionEvent.ACTION_MOVE
  • MotionEvent.ACTION_UP
  • MotionEvent.ACTION_CANCEL

MotionEvent描述了 触摸的位置触摸的时间 以及 触摸点的个数

任何一个gesture都是从MotionEvent.ACTION_DOWN开始,以MotionEvent.ACTION_UP结束

Event Flow

The touch event is dispatched from top to bottom but handled from bottom to top. The event is dispatched by calling dispatchTouchEvent and handled by onTouchEvent.

这一部分我们只关注事件流向,而不关注具体的逻辑。 在Android Touch System中,事件分发从ActivitydispatchTouchEvent开始。

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

如果事件被消费掉了,则返回true。方法中调用了PhoneWindowsuperDispatchTouchEvent,紧接着PhoneWindow调用DecorViewsuperDispatchTouchEvent

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

由于DecorView继承FrameLayout,FrameLayout并没有重写dispatchTouchEvent,所以这里看ViewGroupdispatchTouchEvent,代码太长,就不贴了,此方法可能会调用:

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits)

这个方法就会向 child view传递此事件,循环上述的过程直到View不是ViewGroup,而是单个View。

再看View的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent event) {

        ···

        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;
    }

到这里分发结束,如果mOnTouchListener不是null,先调用mOnTouchListener.onTouch(),之后有可能再调用onTouchEvent(event),在onTouchEvent中会调用performClick或者间接调用performLongClick,这个时候就会回调clickListener或者longClickListener。最后返回result给他的ViewGroup

同时这段代码也向我们展示了,mOnTouchListener.onTouchonTouchEvent之前调用,而且如果前者返回true,则onTouchEvent就不会调用。 总的来说事件流是大致按照这样的顺序来的:

Counsume Touch Handling

上面的流程只是某一个情景下的一个示意图,在实际的开发中,如果其中某一个方法(当然还有touchListener的方法)返回true,就会中断之后的传递链。

  • 如何处理Touch Event:有两种方法,一种是利用setOnTouchListener、setOnClickListener、setOnLongClickListener,第二种重写onTouchEvent,注意如果使用第一种方法,应该注意返回值。

  • 如何消费掉Touch Event:在接收到MotionEvent.ACTION_DOWN时就返回true。

Intercept Touch Event

ViewGroup中有onInterceptTouchEvent可以用来截断事件,让自己来处理事件。这一部分可以看官网