文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android View事件分发机制详解

2022-06-06 07:06

关注

准备了一阵子,一直想写一篇事件分发的文章总结一下,这个知识点实在是太重要了。

一个应用的布局是丰富的,有TextView,ImageView,Button等,这些子View的外层还有ViewGroup,如RelativeLayout,LinearLayout。作为一个开发者,我们会思考,当点击一个按钮,Android系统是怎样确定我点的就是按钮而不是TextView的?然后还正确的响应了按钮的点击事件。内部经过了一系列什么过程呢?

先铺垫一些知识能更加清晰的理解事件分发机制:
1. 通过setContentView设置的View就是DecorView的子view,即DecorView是父容器。
2. 点击屏幕时,在手指按下和抬起间,会产生很多事件,down…move…move…up,中间会有很多的move事件,这一系列的事件为一个事件序列
3. dispatchTouchEvent方法用于分发事件
4. onInterceptTouchEvent方法用于拦截事件
5. onTouchEvent方法用于处理事件

当一个点击事件(MotionEvent)产生后,事件最先传递给当前的界面(Activity),这点是很好理解的。 Activity再将事件传递给窗口(Window),然后Window将事件传递给顶级View(DecorView)。此时,事件已经到达了View了。之后顶级View就会按照事件分发机制去分发事件。具体是这样的:

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用。如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
如果一个View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法会被调用,如果它的父容器的onTouchEvent方法还是返回false,那就继续往上抛,当所有的元素都不处理这个事件,那么这个事件会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。

好了,现在已经铺垫了基础,那么接下来就从源码的角度来分析事件分发机制。

当然是从Activity的dispatchTouchEvent方法开始分析。源码如下:


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

如果当前事件是down的话,就调用onUserInteraction方法,onUserInteraction是一个空方法,我们可以暂时不搭理。然后调用getWindow方法获取到当前Activity关联的Window,Window再调用superDispatchTouchEvent方法将事件传入进行分发。
如果superDispatchTouchEvent方法返回true的话, view已经处理了事件。整个事件循环结束。如果返回false,没有view处理这个事件。事件往上抛,那就Activity自己处理了,即Activity的onTouchEvent方法会被调用。

因为想要知道事件的整个分发过程,现在关注的是Window的superDispatchTouchEvent方法,那么就跟进去看看:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window是一个抽象类,superDispatchTouchEvent是一个抽象的方法,那么我们必须要找到window的实现类才行,可是茫茫人海怎么找呢?看到window类的说明就明白了


* <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window

意思是Window存在唯一的实现是android.view.PhoneWindow

那么PhoneWindow里的superDispatchTouchEvent方法就是我们要找的信息,如下:


@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
}

直接将事件传递给了DecorView。这时事件已经是到达View了哦。

那么跟进DecorView的superDispatchTouchEvent方法看看,如下:


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

内部调用了父类的dispatchTouchEvent方法,那么DecorView的父类是什么呢?DecorView肯定是View的,那么刚才开篇提到,我们通过setContentView设置的View,是DecorView的子View。那么更加准确的说DecorView是一个ViewGroup。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker

可以看到DecorView是继承自FrameLayout,FrameLayout是ViewGroup,也就是说DecorView是一个ViewGroup。

那么现在只需要关注ViewGroup的dispatchTouchEvent方法。继续前进

ViewGroup的事件分发

ViewGroup的dispatchTouchEvent方法如下:


@Override
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);
        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;
    }
    //代码省略
    if (!canceled && !intercepted) {
    //代码省略
        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
          final float x = ev.getX(actionIndex);
          final float y = ev.getY(actionIndex);
          // Find a child that can receive the event.
          // Scan children from front to back.
          final ArrayList<View> preorderedList = buildOrderedChildList();
          final boolean customOrder = preorderedList == null
              && isChildrenDrawingOrderEnabled();
          final View[] children = mChildren;
          for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(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);
          }
          if (preorderedList != null) preorderedList.clear();
        }
        if (newTouchTarget == null && mFirstTouchTarget != null) {
          // Did not find a child to receive the event.
          // Assign the pointer to the least recently added target.
          newTouchTarget = mFirstTouchTarget;
          while (newTouchTarget.next != null) {
            newTouchTarget = newTouchTarget.next;
          }
          newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
      }
    }
    // 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);
    }
    //代码省略
    return handled;
}

代码比较长,一点一点分析,先看到一开始的判断

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)

mFirstTouchTarget != null的意义是ViewGroup不拦截事件并将事件交由子元素处理,先这样记着,这从后面的addTouchTarget方法可以得出结论的。

然后又会来到这个if判断。


if (!disallowIntercept) {
  intercepted = onInterceptTouchEvent(ev);
  ev.setAction(action); // restore action in case it was changed
}

那我们看看disallowIntercept。而disallowIntercept的赋值过程中,有一个 FLAG_DISALLOW_INTERCEPT 标记位

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

这个 FLAG_DISALLOW_INTERCEPT 标记位是可以通过requestDisallowInterceptTouchEvent方法来设置的。

回到if (!disallowIntercept)的判断,进入这个if判断后,就会来到

intercepted = onInterceptTouchEvent(ev);

调用onInterceptTouchEvent方法,询问ViewGroup是否拦截事件。

读到这里,可以回忆下开篇时铺垫的结论,对于ViewGroup,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 方法就会被调用,接着会调用它的onInterceptTouchEvent方法,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用。如果返回false表示不拦截,通常ViewGroup也是不拦截事件的。

那现在先分析不拦截的情况,不拦截那就好办了的。经过一系列的判断,就会来到一个for循环遍历。

for (int i = childrenCount - 1; i >= 0; i--)

这时ViewGroup开始分发传递事件,遍历子元素了。

首先肯定需要过滤掉一些无关点击事件的子元素的,判断子元素是否能够接收点击事件,点击事件的坐标是否落在子元素区域内。


if (!canViewReceivePointerEvents(child)
    || !isTransformedTouchPointInView(x, y, child, null)) {
  ev.setTargetAccessibilityFocus(false);
  continue;
}

如果不能够接收点击事件或者点击事件的坐标没有落在子元素区域,就会跳出当前循环,继续遍历下一个子元素。这下就知道了Android系统为什么能够知道点击的是Button而不是TextView,其实内部就只是做了一个判断嘛。

那么继续分析,子元素符合以上两个条件后,就将事件传递给这个子元素。会来到了这个判断。

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))

执行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;
  }
  //代码省略
}

我们看到child!=null的情况,如果子元素不为空,调用子元素的dispatchTouchEvent方法继续分发事件,同时返回处理结果布尔值,这时就将事件传递到了子View处理。完成了一轮的事件分发。这个方法先到这里就好。

再看回ViewGroup的dispatchTouchEvent方法,如果dispatchTransformedTouchEvent方法返回true的话,这时事件已经传递给子元素处理,ViewGroup已经不管这个事件了。
那么就会进入if语句,最后会来到addTouchTarget方法,这个方法之前是提到过的,用于mFirstTouchTarget标记位的赋值。

那跟进这个方法看看



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

其实就是让mFirstTouchTarget指向子元素。

执行完这个addTouchTarget方法后,最终会到break语句,那么就会跳出整个for循环体。ViewGroup结束分发过程!

又回到dispatchTransformedTouchEvent方法,如果dispatchTransformedTouchEvent方法返回false,那么if语句的一大段代码都不执行了,而是回到for循环继续遍历子元素进行分发。如此重复完成事件的传递过程。

现在分析ViewGroup拦截事件的情况,如果ViewGroup拦截事件的话,那么就会进入以下这个判断


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

注意到dispatchTransformedTouchEvent方法的第三个参数child传入的是null,那么就是在dispatchTransformedTouchEvent方法中走以下的语句


if (child == null) {
  handled = super.dispatchTouchEvent(event);
}

而ViewGroup是继承自View的,那么就是ViewGroup自己处理事件了。这点我们以下分析了View的事件分发过程就能搞明白了。

以上就是ViewGroup的事件分发

那么现在分析已经将事件传递给了子View的情况,View继续调用dispatchTouchEvent方法,那我们看看View的dispatchTouchEvent方法。

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;
    }
  }
  if (!result && mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
  }
  // Clean up after nested scrolls if this is the end of a gesture;
  // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
  // of the gesture.
  if (actionMasked == MotionEvent.ACTION_UP ||
      actionMasked == MotionEvent.ACTION_CANCEL ||
      (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    stopNestedScroll();
  }
  return result;
}

相比于ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法代码量少了。也相对简单些了。
首先会来到如下判断:


if (li != null && li.mOnTouchListener != null
    && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnTouchListener.onTouch(this, event))

li变量在哪里被赋值的呢?通常是在setOnClickListener方法或setOnTouchListener方法的时候。


public void setOnClickListener(@Nullable OnClickListener l) {
  if (!isClickable()) {
    setClickable(true);
  }
  getListenerInfo().mOnClickListener = l;
}
public void setOnTouchListener(OnTouchListener l) {
  getListenerInfo().mOnTouchListener = l;
}

而这个getListenerInfo()如下:


ListenerInfo getListenerInfo() {
  if (mListenerInfo != null) {
    return mListenerInfo;
  }
  mListenerInfo = new ListenerInfo();
  return mListenerInfo;
}

ListenerInfo是一个内部类,里面存放的是各种监听事件的引用。

之后会判断如下条件:

li.mOnTouchListener != null

同理只要setOnTouchListener方法设置了,这个引用就不空。这些都是好理解的。那关键到了,

li.mOnTouchListener.onTouch(this, event)

到了最后一个条件。这个onTouch方法是我们去实现的,它也返回一个布尔值,如果返回true的话,那么就会进入这个if判断最终返回true,跳出整个方法,那么我们可以看到接下来的onTouchEvent方法是不会得到执行的。
也就是onTouch的执行在onTouchEvent之前。那么如果我们也调用了setOnClickListener方法监听点击事件的话,onClick方法是在哪里调用的呢?我们有理由相信是在onTouchEvent方法里调用的。那么就跟进看看。


public boolean onTouchEvent(MotionEvent event) {
  //代码省略
  if (((viewFlags & CLICKABLE) == CLICKABLE ||
      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
      (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
      case MotionEvent.ACTION_UP:
        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  //代码省略
            if (!focusTaken) {
              // Use a Runnable and post this rather than calling
              // performClick directly. This lets other visual state
              // of the view update before click actions start.
              if (mPerformClick == null) {
                mPerformClick = new PerformClick();
              }
              if (!post(mPerformClick)) {
                performClick();
              }
            }
          }
       //代码省略 
        break;
    }
    return true;
  }
  return false;
}

只要CLICKABLE或LONG_CLICKABLE不空, 就会处理这个事件,然而怎么保证CLICKABLE或LONG_CLICKABLE不空呢?其实细心的你会发现,刚才上面贴出的setOnClickListener源代码中,会将CLICKABL属性设置会true


public void setOnClickListener(@Nullable OnClickListener l) {
  if (!isClickable()) {
    setClickable(true);
  }
  getListenerInfo().mOnClickListener = l;
}

这样就能进入if判断去处理这个事件了,之后就会来到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;
  }
  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  return result;
}

熟悉接口回调机制的你,一定也读懂了performClick()方法的源码,

li.mOnClickListener.onClick(this);

是在执行这行代码时,调用了我们熟悉的onClick方法

以上就是View的事件分发机制。

此时已经将事件分发机制分析完了,由于我的技术的原因,驾驭的不好,有些关键点还是没分析清楚,但我相信学完了这篇文章能让我和你都对事件分发机制的实现有一个大致的认识,有这个已经可以了,之后还可以一点点去强化锻炼,深入理解事件分发机制。才能为自定义控件铺垫良好的基础。

您可能感兴趣的文章:Android View 事件分发机制详解30分钟搞清楚Android Touch事件分发机制Android事件分发机制(上) ViewGroup的事件分发Android View的事件分发机制谈谈对Android View事件分发机制的理解Android事件分发机制(下) View的事件处理android事件分发机制的实现原理Android事件分发机制的详解Android从源码的角度彻底理解事件分发机制的解析(上)Android从源码的角度彻底理解事件分发机制的解析(下)


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯