跳至主要內容

Android 事件分发

JI,XIAOYONG...大约 5 分钟

Android 事件分发,指手指点击屏幕后,从 Activity、ViewGroup 到 View 的一系列过程。

简介

Android 系统的窗口机制如下图:

Activity 内有一个 Window 对象,其实现类是 PhoneWindow;

DecorView 为顶层 View,DecorView 是一个 FrameLayout,其中有 TitleView 和 ContentView;

Android 系统窗口管理机制
Android 系统窗口管理机制

TitleView 为标题栏,ContentView 就是平时在 Activity 的 onCreate() 方法中设置的视图,TitleView 可以用this.requestWindowFeature(Window.FEATURE_NO_TITLE);隐藏掉,但是必须注意要在 setContentView() 之前,原因如下所示:

public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}

点击事件 Activity --> ViewGroup

点击事件发生后,首先被调用的是Activity.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看到,其内部先调用了getWindow().superDispatchTouchEvent(ev)这个方法,getWindow() 返回的 mWindow 是 PhoneWindow 的对象。

mWindow = new PhoneWindow(this, window, activityConfigCallback);

再看看 PhoneWindow.superDispatchTouchEvent() 方法,显然又调用了 DecorView 的 superDispatchTouchEvent() 方法,在该方法中,调用了 FrameLayout.dispatchKeyEvent(event),此时点击事件从 Activity 转到了 ViewGroup 中

//PhoneWindow
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
  }

//DecorView extends FrameLayout
public boolean superDispatchKeyEvent(KeyEvent event) {
    // Give priority to closing action modes if applicable.
    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
        final int action = event.getAction();
        // Back cancels action modes first.
        if (mPrimaryActionMode != null) {
            if (action == KeyEvent.ACTION_UP) {
                mPrimaryActionMode.finish();
            }
            return true;
        }
    }
    return super.dispatchKeyEvent(event);
}

点击事件 ViewGroup --> View

ViewGroup 与事件分发的方法有三个:

  • dispatchTouchEvent() 分发事件,每次都会被调用
  • onInterceptTouchEvent() 拦截事件,如果当前 ViewGroup 已经决定拦截事件,那么不会再调用
  • onTouchEvent() 处理点击事件,如果设置了mOnTouchListener的话,则不会回调本方法

这三个主要方法关系如下(伪代码,来自《Android 开发艺术探索》):

//每次点击事件回调该方法
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    var result = false
    if (onInterceptTouchEvent(event)) {//viewGroup 会回调该方法,确认是否拦截点击事件
        result = onTouchEvent(event)//对点击事件进行处理
    } else {
        result = child.dispatchTouchEvent(event)
        }
    }
    return result
}

当 ViewGroup.dispatchTouchEvent() 被调用后,会通过一系列条件判断是由 ViewGroup 拦截该事件,还是由子 View 消耗该事件。

主要流程分为两部分

1.检查是否需要拦截

// http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/ViewGroup.java line2567-2582
    public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
            // 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);//在这里调用了 onInterceptTouchEvent() 方法,如果已经拦截了
                    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;
            }
     ...
     }

     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;
    }
  • 每次 ACTION_DOWN 事件都需要调用onInterceptTouchEvent()方法判断是否需要拦截
  • 其他 MotionEvent 事件,如果有能处理点击事件的子 View(mFirstTouchTarget != null)且disallowIntercept为 false 也需要调用onInterceptTouchEvent()方法判断是否需要拦截,否则不需要拦截
  • 其余情况都需要拦截(没有可以处理点击事件的子 View,并且不是 ACTION_DOWN 事件)

如果 ViewGroup 判断要拦截该事件,则会调用dispatchTransformedTouchEvent()(后面会再讲到)通过他调用继承自 View 的dispatchTouchEvent(MotionEvent event)方法:

 // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
            ...
            }

否则就需要遍历其子 View

2.遍历 ViewGroup 的所有子 View,寻找一个可以处理点击事件的子 View

  • dispatchTransformedTouchEvent() 调用了子 View 的dispatchTouchEvent()
  • addTouchTarget()mFirstTouchTarget 进行更新
public boolean dispatchTouchEvent(MotionEvent ev) {

        // 1. Check for interception.判断是否需要拦截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {//mFirstTouchTarget 表示能处理点击事件的子 View
            //FLAG_DISALLOW_INTERCEPT 每次 ACTION_DOWN 都会被重置
            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;
        }

    	//2.遍历子 View,寻找可以处理点击事件的子 View
    	if (!canceled && !intercepted) {
       		 for (int i = childrenCount - 1; i >= 0; i--) {
                 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                     // Child wants to receive touch within its bounds.
                     ...
                     newTouchTarget = addTouchTarget(child, idBitsToAssign);
                     alreadyDispatchedToNewTouchTarget = true;
                     break;
                  }
        	 }
        }
}

dispatchTransformedTouchEvent() 方法如下,由于child != null其内部调用child.dispatchTouchEvent(event)方法,如此循环直到子 View 是一个 View(单就 ViewGroup 和 View 而论)即将点击事件从 ViewGroup 分发到了 View

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    ...
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
}

如果有子 View 可以处理点击事件,在addTouchTarget()方法内部对mFirstTouchTarget进行更新

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

点击事件 View 内部

View 的点击事件分发主要涉及到两个方法:

  • dispatchTouchEvent()
  • onTouchEvent()

其点击事件分发用伪代码表示如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    if(mListenerInfo.mOnTouchListener.onTouch(this, event)){
        return true;
    }else{
        return onTouchEvent(event);
    }
}

可见 View 的 dispatchTouchEvent() 方法中,如果 View 注册了 OnTouchListener 则会先执行mOnTouchListener.onTouch()方法,如果该方法返回 false 才会执行onTouchEvent()

在看 onTouchEvent() 方法:

  • 如果 View 处于不可用状态下,也会消耗点击事件,只不过没有反应
  • 如果注册了 OnClickListener 会在 ACTION_UP 的时候调用mOnClickListener.onClick(this)
public boolean onTouchEvent(MotionEvent event) {
    ...
    if(CLICKABLE&&LONG_CLICKABLE){//LONG_CLICKABLE 默认为 false,CLICKABLE、LONG_CLICKABLE 会在设置点击事件时被设置为 true
        switch (action) {
            case MotionEvent.ACTION_UP:{
                ...
                performClick();//如果注册了 OnClickListener 则会调用其 onClick() 方法
            }
        }
    }
}

public boolean performClick() {
    ...
    mListenerInfo.mOnClickListener.onClick(this);
    ...
}

总结

整个 Android 的时间分发始于 Activity,经过 PhoneWindow、DecorView 到达 ViewGroup,再逐层分发到 View 中。

如果底层没有处理点击事件,则又一层层向上返回,直到最顶层消耗掉点击事件。

参考资料

《Android 开发艺术探索》

Android 源代码open in new window

文章标题:《Android 事件分发》
本文作者: JI,XIAOYONG
发布时间: 2018/04/24 20:25:33 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/c0fefed0.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8