onTouchEvent()中如何区分移动和点击?

Posted

技术标签:

【中文标题】onTouchEvent()中如何区分移动和点击?【英文标题】:How to distinguish between move and click in onTouchEvent()? 【发布时间】:2012-04-15 11:30:52 【问题描述】:

在我的应用程序中,我需要同时处理移动和点击事件。

点击是一个 ACTION_DOWN 动作、几个 ACTION_MOVE 动作和一个 ACTION_UP 动作的序列。理论上,如果您收到一个 ACTION_DOWN 事件,然后是一个 ACTION_UP 事件 - 这意味着用户刚刚单击了您的视图。

但实际上,此序列不适用于某些设备。在我的三星 Galaxy Gio 上,只需单击我的视图就会得到这样的序列:ACTION_DOWN,几次 ACTION_MOVE,然后是 ACTION_UP。 IE。我收到一些带有 ACTION_MOVE 操作代码的意外 OnTouchEvent 触发。我从来没有(或几乎从来没有)得到序列 ACTION_DOWN -> ACTION_UP。

我也不能使用 OnClickListener,因为它没有给出点击的位置。那么如何检测点击事件并将其与移动区别开来呢?

【问题讨论】:

您是否尝试过使用 onTouch 而不是 onTouchEvent,这样做您也将获得对视图的引用,因此您可以注销这些值并查看是否调用了 ACTION_DOWN 和 ACTION_UP 的点击还是不... 【参考方案1】:

这是另一个非常简单的解决方案,不需要您担心手指被移动。如果您将点击作为简单移动距离的基础,那么您如何区分点击和长点击。

您可以在其中添加更多智能并包括移动的距离,但我还没有遇到一个实例,即用户可以在 200 毫秒内移动的距离应该构成移动而不是点击。

setOnTouchListener(new OnTouchListener() 
    private static final int MAX_CLICK_DURATION = 200;
    private long startClickTime;

    @Override
    public boolean onTouch(View v, MotionEvent event) 
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN: 
                startClickTime = Calendar.getInstance().getTimeInMillis();
                break;
            
            case MotionEvent.ACTION_UP: 
                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                if(clickDuration < MAX_CLICK_DURATION) 
                    //click event has occurred
                
            
        
        return true;
    
);

【讨论】:

为什么不使用ViewConfiguration.getLongPressTimeout() 作为点击的最长持续时间? 长按当然可以使用该方法,但这只是标准点击。 其实用 event.getEventTime() -event.getDownTime() 来计算点击时长会更好 @Grimmy 为什么会更好? @RestInPeace 因为您不需要单独的字段并使用日历。您只需从另一个数字中减去一个数字。【参考方案2】:

考虑到以下因素,我得到了最好的结果:

    主要是,距离ACTION_DOWNACTION_UP 事件之间移动。我想在density-indepenent pixels 中指定最大允许距离而不是像素,以更好地支持不同的屏幕。例如,15 DP。 其次,事件之间的持续时间一秒似乎是最好的最大值。 (有些人非常“彻底”地“点击”,即缓慢;我仍然想认识到这一点。)

例子:

/**
 * Max allowed duration for a "click", in milliseconds.
 */
private static final int MAX_CLICK_DURATION = 1000;

/**
 * Max allowed distance to move during a "click", in DP.
 */
private static final int MAX_CLICK_DISTANCE = 15;

private long pressStartTime;
private float pressedX;
private float pressedY;

@Override
public boolean onTouchEvent(MotionEvent e) 
     switch (e.getAction()) 
        case MotionEvent.ACTION_DOWN: 
            pressStartTime = System.currentTimeMillis();                
            pressedX = e.getX();
            pressedY = e.getY();
            break;
        
        case MotionEvent.ACTION_UP: 
            long pressDuration = System.currentTimeMillis() - pressStartTime;
            if (pressDuration < MAX_CLICK_DURATION && distance(pressedX, pressedY, e.getX(), e.getY()) < MAX_CLICK_DISTANCE) 
                // Click event has occurred
            
             
    


private static float distance(float x1, float y1, float x2, float y2) 
    float dx = x1 - x2;
    float dy = y1 - y2;
    float distanceInPx = (float) Math.sqrt(dx * dx + dy * dy);
    return pxToDp(distanceInPx);


private static float pxToDp(float px) 
    return px / getResources().getDisplayMetrics().density;

这里的想法与Gem's solution中的相同,但有以下区别:

这会计算两点之间的实际Euclidean distance。 这使用 dp 而不是 px。

更新(2015 年):另请查看 Gabriel's fine-tuned version of this。

【讨论】:

优秀的答案!这节省了相当多的研究,谢谢! 这个答案的一个问题,你认为记录移动事件中的距离会更好吗?如果我从视图中的手指开始,我可以在一秒钟内向上滑动到屏幕顶部,然后再向下滑动到底部,这仍然算作一次点击。如果这被记录在移动动作中,如果事件超过点击区域,它可以设置一个布尔值。 @Gabriel:已经有一段时间了,但我想我发现这对我的需求来说“足够好”了。如果您使用该方法获得更好的结果,请随意尝试,如果是,请考虑发布新答案:-) 我认为我有一个比大多数人更具体的用例,我有一个可以从屏幕底部向上拖动到顶部的视图,或者可以单击标题来切换位置。这意味着,如果您将其向上拖动然后快速向下拖动,它仍然可以识别点击。不过谢谢你,你的解决方案几乎是我需要的 100%,我只是对其进行了一些细微的改动以使我到达那里。我将在今晚晚些时候发布我的代码,作为需要更精确的人们的替代答案。 哦,您的 onTouchEvent() 缺少返回值。如果你告诉我们在那里写什么会很有帮助(return true;?return false;?return super.onTouchEvent(e);?)【参考方案3】:

以Jonik's lead 为例,我构建了一个稍微微调的版本,如果您移动手指然后在松开之前返回到该位置,它不会注册为点击:

所以这是我的解决方案:

/**
 * Max allowed duration for a "click", in milliseconds.
 */
private static final int MAX_CLICK_DURATION = 1000;

/**
 * Max allowed distance to move during a "click", in DP.
 */
private static final int MAX_CLICK_DISTANCE = 15;

private long pressStartTime;
private float pressedX;
private float pressedY;
private boolean stayedWithinClickDistance;

@Override
public boolean onTouchEvent(MotionEvent e) 
     switch (e.getAction()) 
        case MotionEvent.ACTION_DOWN: 
            pressStartTime = System.currentTimeMillis();                
            pressedX = e.getX();
            pressedY = e.getY();
            stayedWithinClickDistance = true;
            break;
        
        case MotionEvent.ACTION_MOVE: 
            if (stayedWithinClickDistance && distance(pressedX, pressedY, e.getX(), e.getY()) > MAX_CLICK_DISTANCE) 
                stayedWithinClickDistance = false;
            
            break;
             
        case MotionEvent.ACTION_UP: 
            long pressDuration = System.currentTimeMillis() - pressStartTime;
            if (pressDuration < MAX_CLICK_DURATION && stayedWithinClickDistance) 
                // Click event has occurred
            
             
    


private static float distance(float x1, float y1, float x2, float y2) 
    float dx = x1 - x2;
    float dy = y1 - y2;
    float distanceInPx = (float) Math.sqrt(dx * dx + dy * dy);
    return pxToDp(distanceInPx);


private static float pxToDp(float px) 
    return px / getResources().getDisplayMetrics().density;

【讨论】:

【参考方案4】:

使用探测器,它可以工作,并且在拖动时不会升起

字段:

private GestureDetector mTapDetector;

初始化:

mTapDetector = new GestureDetector(context,new GestureTap());

内部类:

class GestureTap extends GestureDetector.SimpleOnGestureListener 
    @Override
    public boolean onDoubleTap(MotionEvent e) 

        return true;
    

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) 
        // TODO: handle tap here
        return true;
    

触摸:

@Override
public boolean onTouch(View v, MotionEvent event) 
    mTapDetector.onTouchEvent(event);
    return true;

享受:)

【讨论】:

最喜欢的解决方案。但是,如果我想为多个视图使用一个触摸/手势侦听器,我必须在最后一个 onTouch 事件期间跟踪最后一个视图,以便我知道 onSingleTapConfirmed 事件来自哪个视图。如果 MotionEvent 为您存储视图会很好。 对于可能在运行此代码时遇到问题的人,我必须将函数 onTouch(View v, MotionEvent event) 的返回值设置为“true”才能使其工作。实际上,我认为我们消费了这些事件并且应该返回 true。【参考方案5】:

为了获得最优化的点击事件识别我们必须考虑两件事:

    ACTION_DOWN 和 ACTION_UP 之间的时间差。 用户触摸和松开手指时 x、y 的差异。

其实我结合了 Stimsoni 和 Neethirajan 给出的逻辑

所以这是我的解决方案:

        view.setOnTouchListener(new OnTouchListener() 

        private final int MAX_CLICK_DURATION = 400;
        private final int MAX_CLICK_DISTANCE = 5;
        private long startClickTime;
        private float x1;
        private float y1;
        private float x2;
        private float y2;
        private float dx;
        private float dy;

        @Override
        public boolean onTouch(View view, MotionEvent event) 
            // TODO Auto-generated method stub

                    switch (event.getAction()) 
                    
                        case MotionEvent.ACTION_DOWN: 
                        
                            startClickTime = Calendar.getInstance().getTimeInMillis();
                            x1 = event.getX();
                            y1 = event.getY();
                            break;
                        
                        case MotionEvent.ACTION_UP: 
                        
                            long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                            x2 = event.getX();
                            y2 = event.getY();
                            dx = x2-x1;
                            dy = y2-y1;

                            if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) 
                                Log.v("","On Item Clicked:: ");

                        
                    

            return  false;
        
    );

【讨论】:

+1,好方法。我选择了similar solution,除了MAX_CLICK_DISTANCE 使用 dp 而不是 px,并且计算距离略有不同。 上面声明的这些变量应该全局声明。 @RushiAyyappa 在大多数情况下,您在视图上设置一次 onTouchListener。 setOnTouchListener() 采用匿名类引用。变量在类中是全局的。我认为除非有人再次使用其他侦听器实例调用setOnTouchListener(),否则这将起作用。 我的 touchListener 是内部的,这些变量第二次被破坏了。所以我不得不全局声明它们,这很有效。【参考方案6】:

使用 Gil SH 答案,我通过实现 onSingleTapUp() 而不是 onSingleTapConfirmed() 对其进行了改进。它的速度要快得多,并且在拖动/移动时不会单击视图。

手势点击:

public class GestureTap extends GestureDetector.SimpleOnGestureListener 
    @Override
    public boolean onSingleTapUp(MotionEvent e) 
        button.performClick();
        return true;
    

像这样使用它:

final GestureDetector gestureDetector = new GestureDetector(getApplicationContext(), new GestureTap());
button.setOnTouchListener(new View.OnTouchListener() 
    @Override
    public boolean onTouch(View v, MotionEvent event) 
        gestureDetector.onTouchEvent(event);
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN:
                return true;
            case MotionEvent.ACTION_UP:
                return true;
            case MotionEvent.ACTION_MOVE:
                return true;
        
        return false;
    
);

【讨论】:

这个答案帮助我区分了触摸和 TextView 上的滚动。可以在here (Kotlin) 中找到此答案的更精简版本【参考方案7】:

以下代码将解决您的问题

@覆盖 公共布尔 onTouchEvent(MotionEvent 事件) 开关(事件.getAction()) 案例(MotionEvent.ACTION_DOWN): x1 = event.getX(); y1 = event.getY(); 休息; 案例(MotionEvent.ACTION_UP): x2 = event.getX(); y2 = event.getY(); dx = x2-x1; dy = y2-y1; 如果(数学.abs(dx)>数学.abs(dy)) 如果(dx>0)移动(1); //正确的 否则 if (dx == 0) move(5); //点击 否则移动(2); //剩下 别的 如果(dy>0)移动(3); // 下 否则 if (dy == 0) move(5); //点击 否则移动(4); //向上 返回真;

【讨论】:

【参考方案8】:

如果没有 ACTION_MOVE 发生,就很难发生 ACTION_DOWN。手指在屏幕上与第一次触摸不同的位置的最轻微抽动将触发 MOVE 事件。另外,我相信手指压力的变化也会触发 MOVE 事件。我会在 Action_Move 方法中使用 if 语句来尝试确定移动发生与原始 DOWN 运动的距离。如果移动发生在某个设定的半径之外,你的 MOVE 动作就会发生。这可能不是您尝试的最佳、资源高效的方法,但它应该有效。

【讨论】:

谢谢,我也计划过这样的事情,但认为有人知道其他解决方案。你是否也经历过这样的行为(点击时向下->移动->向上)? 是的,我目前正在开发涉及多点触控的游戏,并且我对 poitners 和 Move_Events 进行了一些广泛的测试。这是我在测试时得出的结论之一(Action_Down 事件很快变为 Action_Move。很高兴我能提供帮助。 好的,我并不孤单,这很好 :) 我还没有在 *** 或其他地方找到遇到此类问题的人。谢谢。 您在哪些设备上看到过这种行为?我在三星 Galaxy Ace 和 Gio 上注意到了这一点。 我在 Samsung Captivate 和 Galaxy S II 上亲眼见过它,但我想几乎所有设备都可能是这样【参考方案9】:

除了上面的答案,如果你想同时实现 onClick 和 Drag 动作,那么我下面的代码可以。从@Stimsoni 获得一些帮助:

     // assumed all the variables are declared globally; 

    public boolean onTouch(View view, MotionEvent event) 

      int MAX_CLICK_DURATION = 400;
      int MAX_CLICK_DISTANCE = 5;


        switch (event.getAction())
        
            case MotionEvent.ACTION_DOWN: 
                long clickDuration1 = Calendar.getInstance().getTimeInMillis() - startClickTime;


                    startClickTime = Calendar.getInstance().getTimeInMillis();
                    x1 = event.getX();
                    y1 = event.getY();


                    break;

            
            case MotionEvent.ACTION_UP:
            
                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                x2 = event.getX();
                y2 = event.getY();
                dx = x2-x1;
                dy = y2-y1;

                if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) 
                    Toast.makeText(getApplicationContext(), "item clicked", Toast.LENGTH_SHORT).show();
                    Log.d("clicked", "On Item Clicked:: ");

               //    imageClickAction((ImageView) view,rl);
                

            
            case MotionEvent.ACTION_MOVE:

                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                x2 = event.getX();
                y2 = event.getY();
                dx = x2-x1;
                dy = y2-y1;

                if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) 
                    //Toast.makeText(getApplicationContext(), "item clicked", Toast.LENGTH_SHORT).show();
                  //  Log.d("clicked", "On Item Clicked:: ");

                    //    imageClickAction((ImageView) view,rl);
                
                else 
                    ClipData clipData = ClipData.newPlainText("", "");
                    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);

                    //Toast.makeText(getApplicationContext(), "item dragged", Toast.LENGTH_SHORT).show();
                    view.startDrag(clipData, shadowBuilder, view, 0);
                
                break;
        

        return  false;
    

【讨论】:

【参考方案10】:

如果您只想对点击做出反应,请使用:

if (event.getAction() == MotionEvent.ACTION_UP) 


【讨论】:

以上是关于onTouchEvent()中如何区分移动和点击?的主要内容,如果未能解决你的问题,请参考以下文章

在Activity中同时重写onTouchEvent()和onClick()方法,该怎样获得被点击的

android中利用View中的onTouchEvent捕捉长按事件

Android入门第38天-使用随鼠标移动的圆点来熟悉onTouchEvent

如何让 onTouchEvent、长按和上下文菜单协同工作?

看看Android源码中View是如何实现LongPress的

看看Android源码中View是如何实现LongPress的