Android: onInterceptTouchEvent和dispatchTouchEvent之间的区别是什么?

277
在Android中,onInterceptTouchEventdispatchTouchEvent这两个方法都可以用来拦截触摸事件(MotionEvent),但它们有何区别呢?
在一个视图层级结构中(ViewGroup),onInterceptTouchEventdispatchTouchEventonTouchEvent三个方法是如何相互作用的呢?
14个回答

300

最好解决这个问题的方法是查看源代码。文档对此的解释非常不足。

dispatchTouchEvent实际上是在Activity、View和ViewGroup中定义的。将其视为控制器,决定如何路由触摸事件。

例如,最简单的情况是View.dispatchTouchEvent,它将把触摸事件路由到OnTouchListener.onTouch(如果已定义),否则将路由到扩展方法onTouchEvent。

对于ViewGroup.dispatchTouchEvent,情况要复杂得多。它需要找出哪个子视图应该获得事件(通过调用child.dispatchTouchEvent)。这基本上是一个命中测试算法,您可以确定哪个子视图的边界矩形包含触摸点坐标。

但在将事件分派给适当的子视图之前,父级可以窥视和/或截取整个事件。这就是onInterceptTouchEvent存在的目的。因此,在执行命中测试之前,它首先调用此方法。如果事件被劫持(通过从onInterceptTouchEvent返回true),则会向子视图发送ACTION_CANCEL,以便它们可以放弃其触摸事件处理(来自先前的触摸事件)。从那时起,所有父级别的触摸事件都将分派到onTouchListener.onTouch(如果已定义)或onTouchEvent()。此外,在这种情况下,不再调用onInterceptTouchEvent。

除非您正在进行一些自定义路由,否则您甚至希望覆盖[Activity | ViewGroup | View] .dispatchTouchEvent吗?

主要的扩展方法是ViewGroup.onInterceptTouchEvent,如果您想在父级别上窥视和/或截取触摸事件;以及View.onTouchListener / View.onTouchEvent,用于主要事件处理。

总的来说,我认为这是一个过度复杂的设计,但android api更倾向于灵活性而不是简单性。


12
这是一个精简明了的答案。想要更详细的例子,请参考培训课程 "Managing Touch Events in a ViewGroup" - TalkLittle
1
@numan salati:“从那时起,父级别的所有触摸事件都会被分派到onTouchListener.onTouch” - 我想在我的视图组中覆盖dispatch touch方法,并使其将事件分派给onTouchListener。但我不知道如何做到这一点。就像View.getOnTouchListener().onTouch()那样,没有提供此类API。虽然有setOnTouchListener()方法,但没有getOnTouchListener()方法。那么该怎么办呢? - Ashwin
@Ashwin 我也是这么想的,确实没有 setOnInterceptTouchEvent。你可以重写子类视图以在布局/代码中使用它,但是你不能搞乱片段/活动级别的 rootView,因为你不能在不子类化片段/活动本身的情况下子类化这些视图(就像大多数兼容性 ProgressActivitiy 实现一样)。API 需要一个 setOnInterceptTouchEvent 来简化操作。每个人在半复杂的应用程序中都会在 rootView 上使用 interceptTouch。 - leRobot
我原以为我理解了这个机制,直到我发现对于dispatchTouchEvent()和onInterceptTouchEvent()的调用依赖于哪些ViewGroups被设置为可点击。 - Alexey Ozerov

264
因为这是谷歌搜索结果中的第一个,所以我想与您分享Dave Smith的Youtube:主导Android触摸系统演讲,并且幻灯片可以在这里找到。它给了我对Android触摸系统的深刻理解:
如何处理触摸事件的Activity
  • Activity.dispatchTouchEvent()
    • 始终首先被调用
    • 将事件发送到连接到Window的根视图
    • onTouchEvent()
      • 如果没有视图消耗事件,则调用
      • 始终最后被调用
如何处理触摸事件的View
  • View.dispatchTouchEvent()
    • 如果存在监听器,则先将事件发送到监听器。
      • View.OnTouchListener.onTouch()
    • 如果未被消耗,则自行处理触摸事件。
      • View.onTouchEvent()

ViewGroup 如何处理触摸事件:

  • ViewGroup.dispatchTouchEvent()
    • onInterceptTouchEvent()
      • 检查是否应该替代子视图
      • ACTION_CANCEL 传递给活动子视图
      • 如果它返回 true 一次,则 ViewGroup 消耗所有后续事件
    • 对于每个子视图(以添加的相反顺序)
      • 如果触摸是相关的(在视图内部),则 child.dispatchTouchEvent()
      • 如果它不是被之前处理的,则将其分派到下一个视图
    • 如果没有子视图处理事件,则侦听器有机会
      • OnTouchListener.onTouch()
    • 如果没有侦听器,或者它未被处理
      • onTouchEvent()
  • 拦截的事件跳过了子级步骤
他还在github.com/devunwired/上提供了自定义触摸的示例代码。
基本上,dispatchTouchEvent()会在每个View层上调用,以确定View是否对正在进行的手势感兴趣。在ViewGroup中,ViewGroup有能力在其dispatchTouchEvent()方法中窃取触摸事件,然后再调用子级的dispatchTouchEvent()。只有当ViewGrouponInterceptTouchEvent()方法返回true时,ViewGroup才会停止分发。 区别在于,dispatchTouchEvent()正在分发MotionEvents,而onInterceptTouchEvent则告诉它是否应该拦截(不将MotionEvent分发给子级)或不拦截(分发给子级)
你可以想象一个 ViewGroup 的代码 大致上会做这些事情(非常简化):
public boolean dispatchTouchEvent(MotionEvent ev) {
    if(!onInterceptTouchEvent()){
        for(View child : children){
            if(child.dispatchTouchEvent(ev))
                return true;
        }
    }
    return super.dispatchTouchEvent(ev);
}

99

补充答案

以下是其他答案的可视化补充。我的完整答案在这里

输入图像描述

输入图像描述

ViewGroupdispatchTouchEvent()方法使用onInterceptTouchEvent()来选择它是否应立即处理触摸事件(使用onTouchEvent())或继续通知其子项的dispatchTouchEvent()方法。


onInterceptTouchEvent方法可以在Activity中调用吗?我认为只能在ViewGroup中调用,不知道我对吗?@Suragch - Federico Rizzo
@FedericoRizzo,你说得对!非常感谢!我更新了图表和我的答案。 - Suragch

23
这些方法很容易让人感到困惑,但实际上并不复杂。大部分的混淆是因为:
1. 如果你的View/ViewGroup或其任何子项在onTouchEvent中没有返回true,则dispatchTouchEventonInterceptTouchEvent仅将被调用一次,即MotionEvent.ACTION_DOWN。如果onTouchEvent没有返回true,则父视图会认为你的视图不需要MotionEvents。
2. 当一个ViewGroup的所有子项在onTouchEvent中都没有返回true时,即使你的ViewGroup在onTouchEvent中返回true,onInterceptTouchEvent也仅将被调用一次,即MotionEvent.ACTION_DOWN
处理顺序如下:
1. 调用dispatchTouchEvent
2. 当ViewGroup的任何子项在onTouchEvent中返回true,或者MotionEvent.ACTION_DOWN时,将调用onInterceptTouchEvent
3. 首先在ViewGroup的子项上调用onTouchEvent,当没有子项返回true时,将在View/ViewGroup上调用onTouchEvent
如果你想预览TouchEvents/MotionEvents而不禁用子项上的事件,你必须做两件事:
1. 重写dispatchTouchEvent以预览事件并返回super.dispatchTouchEvent(ev)
2. 重写onTouchEvent并返回true,否则你将无法获得任何MotionEvent,除了MotionEvent.ACTION_DOWN
如果你想检测一些手势,比如滑动事件,在没有禁用其他子项事件的情况下,只要你没有检测到手势,你可以这样做:
1. 如上所述预览MotionEvents,并在检测到手势时设置一个标志。
2. 当你的标志被设置为取消通过你的子项处理MotionEvent时,在onInterceptTouchEvent中返回true。这也是重置标志的方便位置,因为onInterceptTouchEvent不会再次调用,直到下一个MotionEvent.ACTION_DOWN
FrameLayout中覆盖的示例(我的示例是C#,因为我正在使用Xamarin Android进行编程,但逻辑相同):
public override bool DispatchTouchEvent(MotionEvent e)
{
    // Preview the touch event to detect a swipe:
    switch (e.ActionMasked)
    {
        case MotionEventActions.Down:
            _processingSwipe = false;
            _touchStartPosition = e.RawX;
            break;
        case MotionEventActions.Move:
            if (!_processingSwipe)
            {
                float move = e.RawX - _touchStartPosition;
                if (move >= _swipeSize)
                {
                    _processingSwipe = true;
                    _cancelChildren = true;
                    ProcessSwipe();
                }
            }
            break;
    }
    return base.DispatchTouchEvent(e);
}

public override bool OnTouchEvent(MotionEvent e)
{
    // To make sure to receive touch events, tell parent we are handling them:
    return true;
}

public override bool OnInterceptTouchEvent(MotionEvent e)
{
    // Cancel all children when processing a swipe:
    if (_cancelChildren)
    {
        // Reset cancel flag here, as OnInterceptTouchEvent won't be called until the next MotionEventActions.Down:
        _cancelChildren = false;
        return true;
    }
    return false;
}

3
我不知道为什么这个回答的点赞数不多。在我看来,这是一个很好的回答,我觉得它非常有帮助。 - Mark Ormesher

10

简短回答: 首先会调用dispatchTouchEvent()

简短建议: 不要重写dispatchTouchEvent(),因为它很难控制,有时会降低性能。我建议覆盖onInterceptTouchEvent()


由于大多数答案已经比较清楚地提到了在activity/view group/view上的触摸事件流程,因此我只添加一些在ViewGroup这些方法中的代码细节(忽略dispatchTouchEvent()):

onInterceptTouchEvent()将首先被调用,依次调用ACTION事件down -> move -> up。有两种情况:

  1. 如果您在3个情况下(ACTION_DOWN、ACTION_MOVE、ACTION_UP)返回false,则视为父项不需要此触摸事件,因此父项的onTouch()从未被调用,但是子项的onTouch()将代替调用;但请注意:

    • 只要其子项不调用requestDisallowInterceptTouchEvent(true)onInterceptTouchEvent()仍将继续接收触摸事件。
    • 如果没有子项接收该事件(有两种情况:用户触摸的位置没有子项,或者有子项但在ACTION_DOWN时返回了false),则父项将该事件发送回其父项的onTouch()
  2. 反之,如果您返回true,则父项将立即窃取此触摸事件,并且onInterceptTouchEvent()将立即停止,而是调用父项的onTouch()以及所有子项的onTouch()将接收到最后的操作事件-ACTION_CANCEL (因此它意味着父项窃取了触摸事件,从那时起子项无法处理它)。onInterceptTouchEvent()返回false的流程很正常,但是对于返回true的情况有点混乱,因此我在这里列出:

    • 在ACTION_DOWN处返回true,父项的onTouch()将再次接收到ACTION_DOWN和后续动作(ACTION_MOVE、ACTION_UP)。
    • 在ACTION_MOVE事件中返回true,父元素的onTouch()方法将接收到下一个ACTION_MOVE事件(而不是在onInterceptTouchEvent()中的同一个ACTION_MOVE事件)以及随后的操作(ACTION_MOVE, ACTION_UP)。
    • 在ACTION_UP事件中返回true,由于父元素夺取触摸事件的时间已经过去,因此不会再调用父元素的onTouch()方法。

还有一件重要的事情是,在onTouch()方法中的ACTION_DOWN事件将决定视图是否希望接收来自该事件的更多操作。如果视图在onTouch()方法的ACTION_DOWN事件中返回true,则表示该视图愿意从该事件中接收更多操作。否则,在onTouch()方法的ACTION_DOWN事件中返回false将意味着该视图不会再从该事件中接收任何操作。


9

我在这个网页http://doandroids.com/blogs/tag/codeexample/上找到了非常直观的解释。从那里得到:

  • boolean onTouchEvent(MotionEvent ev) - 每当检测到以此View为目标的触摸事件时调用
  • boolean onInterceptTouchEvent(MotionEvent ev) - 每当检测到以此ViewGroup或其子项为目标的触摸事件时调用。如果此函数返回true,则MotionEvent将被拦截,意味着它不会传递给子项,而是传递给此View的onTouchEvent。

2
问题涉及 onInterceptTouchEvent 和 dispatchTouchEvent。这两个方法在 onTouchEvent 之前被调用。但在该示例中,您看不到 dispatchTouchEvent。 - Dayerman

9

dispatchTouchEvent在onInterceptTouchEvent之前执行。

这里有一个简单的例子:

   main = new LinearLayout(this){
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            System.out.println("Event - onInterceptTouchEvent");
            return super.onInterceptTouchEvent(ev);
            //return false; //event get propagated
        }
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            System.out.println("Event - dispatchTouchEvent");
            return super.dispatchTouchEvent(ev);
            //return false; //event DONT get propagated
        }
    };

    main.setBackgroundColor(Color.GRAY);
    main.setLayoutParams(new LinearLayout.LayoutParams(320,480));    


    viewA = new EditText(this);
    viewA.setBackgroundColor(Color.YELLOW);
    viewA.setTextColor(Color.BLACK);
    viewA.setTextSize(16);
    viewA.setLayoutParams(new LinearLayout.LayoutParams(320,80));
    main.addView(viewA);

    setContentView(main);

你可以看到日志会是这样的:

I/System.out(25900): Event - dispatchTouchEvent
I/System.out(25900): Event - onInterceptTouchEvent

如果您正在使用这两个处理程序,请使用dispatchTouchEvent来处理事件的第一个实例,该事件将传递到onInterceptTouchEvent。

另一个区别是,如果dispatchTouchEvent返回“false”,则事件不会传播到子项(在此情况下为EditText),而如果您在onInterceptTouchEvent中返回false,则事件仍将被分派到EditText。


4

这些视频真的很棒,我看了几个视频,解决了我的问题。 - Simon

4
以下代码位于ViewGroup子类中,可以防止其父容器接收触摸事件:
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    // Normal event dispatch to this container's children, ignore the return value
    super.dispatchTouchEvent(ev);

    // Always consume the event so it is not dispatched further up the chain
    return true;
  }

我使用它与自定义覆盖层一起,以防止背景视图响应触摸事件。

2
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume =false;
    if(onInterceptTouchEvent(ev){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
}

1
你能添加一些解释吗? - Paul Floyd
3
虽然这段代码片段可能解决了问题,但包括解释真的有助于提高您帖子的质量。请记住,您正在回答未来读者的问题,而这些人可能不知道您建议使用该代码的原因。 - Rosário Pereira Fernandes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接