事件分发源码分析

对事件分发还不太熟悉,且看完博客还一头雾水的朋友,不妨从面试题下手,带着问题去看源码,再由点到面,辅以xmind和流程图,看能否得以突破。

参考链接
参考链接

面试题:

1.view和viewgroup的事件分发?(事件分发的原理)

流程如下

2.onTouch和onClick冲突问题?

onTouch和onClick冲突的原因是因为,onTouch设置为true,onClick就不会执行。详细案例和源码分析如下。

2.1案例:

button.setOnTouchListener(this);
button.setOnClickListener(this);

 @Override
   public void onClick(View v) {
    Log.i(TAG, "onClick: ");
}

  @Override
    public boolean onTouch(View v, MotionEvent event) {
      Log.i(TAG, "onTouch: ");
    //button.performClick();假设加入这个,即便是写true,也会触发onclick.
     return true;//关键是这里,如果设置为true则onclick不打印,设置为false,onclick就打印
 }

2.2源码分析:

用户触摸屏幕时,将产生Touch事件,Touch事件的相关细节(包括触摸位置,时间等,)而封装的对象,不是Java来做的,而是工作activity的驱动来做的,驱动做完之后,触摸到屏幕时,第一时间会调用到activity的dispatchTouchEvent(),

起点则为Activity的dispatchTouchEvent()

//Activity类
 public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {//为down时触发
        onUserInteraction();//作用:实现屏保功能
 //当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
    }
    if (getWindow().superDispatchTouchEvent(ev)) {//PhoneWindow的superDispatchTouchEvent()着重分析。
        return true;
    }
    return onTouchEvent(ev);
}

//PhoneWindow类
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);//这里是DecorView
}

//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);  //ViewGroup的dispatchTouchEvent()
}
1
2
3
4
5
6
流程:
起点:底层驱动
==>Activity的dispatchTouchEvent()
==>PhoneWindow的superDispatchTouchEvent()
==>DecorView的superDispatchTouchEvent()
==>ViewGroup的superDispatchTouchEvent()(核心分发)

vysor实现原理

 ViewGroup的DispatchTouchEvent()
 //触摸屏幕 不一定是用手指去按手机屏幕,也有可能是通过vysor等辅助功能来触摸
//而ViewGroup的DispatchTouchEvent()里面 前几段代码的判断。 
//则是专门针对vysor等辅助功能软件进行判断
//是手指亲自点击 还是用辅助功能点击
//涉及到的就不是Java层的了,而是驱动层
if (mInputEventConsistencyVerifier != null) {
     mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//Accessibility就是在判断,是否是辅助功能。
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
}

boolean handled = false;//方法最终返回的就是handled.
if (onFilterTouchEventForSecurity(ev)) {
    boolean handled = false;//方法最终返回的就是handled.
    if (onFilterTouchEventForSecurity(ev)) {//过滤,是否拦截,由requestDisallowInterceptTouchEvent()给flgs赋值。从而决定是true还是false
      //下面的判断操作 总体是三个部分   
        //1. 是否是第一次按下的操作,是的话,重启touch状态,touch是针对单指操作
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);//清理touch的target
            resetTouchState();//重置状态
        }

        // 2.是否进行拦截 
        final boolean intercepted;
     if (!disallowIntercept) {
    //disallowIntercept这个值决定了 拦截还是不拦截,而赋值操作,则是在requestDisallowInterceptTouchEvent()进行赋值
    //只有viewgroup才可以拦截,view是没有拦截的。view只有分发和处理
    intercepted = onInterceptTouchEvent(ev);
 //是否取消操作,取消就为true,默认是为false,代表手指没有到屏幕外面
   final boolean canceled = resetCancelNextUpFlag(this)
   if (!canceled && !intercepted) {//第一次触发,实际上在这里,默认都是false,因为手指没有到屏幕外面,所以canceled为false,默认没有被拦截,也为false。实际上是进入到这里
           final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
             //3.以下则是属于对多控件,对子view按照层级进行排列
              final float x = ev.getX(actionIndex);
              final float y = ev.getY(actionIndex);   
              final ArrayList<View> preorderedList = buildTouchDispatchChildList();//这里则是将子view按照层级来保存
              final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
              final View[] children = mChildren;
              for (int i = childrenCount - 1; i >= 0; i--) {
                  //倒序遍历,获取子控件
                  //省略部分业务代码(提取当前的view和按下时间以及坐标等
              )

  if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
  //这里的dispatchTransformedTouchEvent才是核心代码。内部分发给view的dispatchTouchEvent()
    return handled;
}

dispatchTransformedTouchEvent方法 源码分析:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                              View child, int desiredPointerIdBits) {
    final boolean handled;
    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);//因为child不为空,则走到这里,child实际上就是view。
        }
        event.setAction(oldAction);
        return handled;
    }

也就是说,从现在为止,调用顺序则为

1
2
3
4
5
6
7
流程:
起点:底层驱动
==>Activity的dispatchTouchEvent()
==>PhoneWindow的superDispatchTouchEvent()
==>DecorView的superDispatchTouchEvent()
==>ViewGroup的superDispatchTouchEvent()(核心分发)
==>View的dispatchTouchEvent()(核心分发)

View的dispatchTouchEvent()的部分重点源码:

  if (li != null  && li.mOnTouchListener != null
      && (mViewFlags & ENABLED_MASK) == ENABLED//所有控件默认都是ENABLED
      && li.mOnTouchListener.onTouch(this, event)) {
      //主要看view调用的onTouch为true还是false
      result = true; 
   }
  if (!result && onTouchEvent(event)) {
            result = true;
  }


//关键是这里是设置true还是false。如果是false,则会走result=true,到!result时,就不会执行ontouchEvent()方法,反之就会走onTouchEvent,不执行OntouchEvent,则不会执行其中的performClick(),就不会执行onclick,所以onClick事件是不执行的。

从这里的源码,则可以回答刚才的现象,为什么设置false就会执行onTouchEvent,设置true则不会执行。

当然 核心点就是performClick()方法,如果在onTouchListener中,直接用view去调用performClick(),那么即便onTouchListener返回true,onclick也会执行。

3.onClick和onLongClick事件能同事发生吗?

案例代码:

MainActivity的代码

    View viewById = findViewById(R.id.myview);
    viewById.setOnTouchListener(this);
    viewById.setOnClickListener(this);
    viewById.setOnLongClickListener(this);

@Override
public void onClick(View v) {
    Log.i(TAG, "onClick: ");
}

@Override
public boolean onLongClick(View v) {
    Log.i(TAG, "onLongClick: ");
    return false;
}

@Override
public boolean onTouch(View v, MotionEvent event) {
    Log.i(TAG, "onTouch: ");
    return false;
}

自定义view的代码

private static final String TAG = MeView.class.getSimpleName();

public MeView(Context context) {
    super(context);
}

public MeView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

public MeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.i(TAG, "我下发任务了 dispatchTouchEvent :");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.i(TAG, "我处理任务 onTouchEvent :");
    return super.onTouchEvent(event);
}

可以同时发生,只要onLongClick设置为false就可以同时发生,设置为true的话,onClick就不会同时发生。

通过这个打log来看
onLongClick应该是在onTouchEvent()中的down时执行的
onClick应该是在onTouchEvent()中的up时执行的

onTouchEvent()源码分析:
分别针对down和up的情况进行分析。

//找到up的主要源码
case MotionEvent.ACTION_UP:
    if (!post(mPerformClick)) {
          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;
}
// 找到down的主要源码。
 case MotionEvent.ACTION_DOWN:
        if (!clickable) {
             checkForLongClick(0, x, y);
             break;
        }

当然,这其中还有很多流程性的原理。例如 postDelay()方法和removeLongClick()方法

结论 :
1.从down到up,100毫秒内是点击事件 onclick,
          超过500毫秒则是长按事件,onLongClick()
 2.move时,未离开控件,则将onclick和onLongClick()移除掉

关键代码

if (((viewFlags & CLICKABLE) == CLICKABLE ||  
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
    //这里只截取部分onTouchEvent()代码
    switch (event.getAction()) {  
        case MotionEvent.ACTION_UP:  
           break;   
        case MotionEvent.ACTION_DOWN:    
            break;  
        case MotionEvent.ACTION_CANCEL:  
            break;  
        case MotionEvent.ACTION_MOVE:  
            break;  
    }  
    return true;  
}  
  return false;  
}  

只要是进入到down move up事件,onTouchEvent()return就为true,否则为false。

也就是说

down move up,任意一个return为true,下面就会执行,如果为false,则不会执行。

调用默认的super的话,因为符合CLICKABLE,所以是可以执行。
如果是继承imageview,则不会执行onTouchEvent(),因为imageview默认是不可点击的,必须要设置点击属性才可以

上面只是分析了viewgroup的dispatchTouchEvent和view的dispatchTouchEvent,,那么viewgroup的流程还会继续走,事件则会自己消耗掉。

if (mFirstTouchTarget == null) {
  //如果view的dispatchTouchEvent为false,则mFirstTouchTarget为空
   handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
  //事件会自己消费掉
        } 

核心点在于:

责任链模式

事件分发uml流程图

事件分发的整体流程

实际案例bug

典型案例:

1.关于popupwindow的bug。
https://blog.csdn.net/qq402164452/article/details/53353798
2.https://blog.csdn.net/Dota_wy/article/details/77451011

当然 这里还要结合实际的滑动冲突的bug,才能真正理解

igding wechat
2018 依计行事 持续精进
坚持原创技术分享,您的支持将鼓励我继续创作!