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


78

在我的应用程序中,我需要处理move和click事件。

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

但实际上,此序列在某些设备上不起作用。在我的Samsung Galaxy Gio上,只要单击我的视图,我就会得到这样的序列:ACTION_DOWN,几次ACTION_MOVE,然后是ACTION_UP。即我收到了带有ACTION_MOVE操作代码的意外OnTouchEvent触发。我从未(或几乎从未)获得序列ACTION_DOWN-> ACTION_UP。

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


您是否尝试过使用onTouch而不是onTouchEvent,这样您还将对视图进行引用,因此您可以注销值并查看是否调用了ACTION_DOWN和ACTION_UP的点击...
Arif Nadeem

Answers:


115

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

您可以在其中加入更多的技巧,并包括移动的距离,但是我还没有遇到一个实例,即用户可以在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()点击的最长持续时间?
Flo 2015年

1
长按肯定可以使用该方法,但这仅是标准单击。
Stimsoni

8
实际上,使用event.getEventTime()-event.getDownTime()来计算点击时间可能会更好
Grimmy

@Grimmy为什么会更好?
RestInPeace

4
@RestInPeace,因为您无需使用单独的字段即可使用日历。您只需要从另一个数字中减去一个数字即可。
Grimmy

55

考虑到以下因素,我获得了最佳结果:

  1. 首先,事件与事件之间的距离。我想以与密度无关的像素而不是像素指定最大允许距离,以更好地支持不同的屏幕。例如15 DPACTION_DOWNACTION_UP
  2. 其次,事件之间的持续时间一秒钟似乎是很好的最大值。(有些人非常“彻底”地(即缓慢地)“单击”;我仍然想认识到这一点。)

例:

/**
 * 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解决方案中的想法相同,但有以下区别:

更新(2015):还请查看Gabriel对此的微调版本


很好的答案!这节省了很多研究工作,谢谢!
2Dee 2013年

这个答案的一个问题,您认为记录移动事件中的距离会更好吗?如果从手指进入视图开始,我可以在一秒钟内向上滑动到屏幕顶部,然后向下滑动到底部,这仍然算作一次点击。如果将其记录在move动作中,则如果事件超过了click区域,则可以将其设置为布尔值。
加百利

@Gabriel:已经有一段时间了,但是我认为我发现这对于我的需求来说已经足够了。如果可以用这种方法获得更好的结果,请随时尝试,如果可以,请考虑发布一个新答案:-)
Jonik 2015年

我认为我的案例比大多数人要具体一些,我可以将视图从屏幕的底部向上拖动到顶部,也可以单击标题来切换位置。这意味着,如果您将其向上拖动然后快速向下拖动,它仍会识别出该点击。不过,谢谢您,您的解决方案几乎是我需要的解决方案的100%,我只是对其进行了一些小改动以使我到达那里。我将在今晚晚些时候发布我的代码,作为需要更多精度的人的替代答案。
加百利

2
哦,您onTouchEvent()缺少返回值。(如果你告诉我们什么写有这将是有益的return true;return false;return super.onTouchEvent(e);?)

29

Jonik的带领下,我构建了一个稍微调整的版本,如果您动动手指然后放回原位,则该点击不会注册为单击:

所以这是我的解决方案:

/**
 * 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;
}

21

使用检测器,它可以正常工作,并且在拖动时不会升高

领域:

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;
    }
}

onTouch:

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

请享用 :)


最喜欢的解决方案。但是,如果要对一个视图使用一个Touch / Gesture侦听器,则必须在上一个onTouch事件期间跟踪最后一个视图,以便知道onSingleTapConfirmed事件来自哪个视图。如果MotionEvent为您存储了视图,那就太好了。
AlanKley

对于谁可能在运行此代码时遇到问题,我必须将功能onTouch(View v,MotionEvent event)的返回值设置为“ true”以使此工作有效。实际上,我认为我们消耗了事件并且应该返回true。
黄锐铭

6

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

  1. ACTION_DOWN和ACTION_UP之间的时差。
  2. 当用户触摸和松开手指时,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,好方法。我使用了类似的解决方案,除了使用dp代替px代替px MAX_CLICK_DISTANCE,并且计算距离略有不同。
Jonik

上面声明的这些变量应全局声明。
Rushi Ayyappa

@RushiAyyappa在大多数情况下,只需在视图上设置一次onTouchListener。将setOnTouchListener()采取匿名类参考。变量在类内部是全局的。我认为这将是有效的,除非有人setOnTouchListener()再次与其他侦听器实例通话。
宝石

我的touchListener是内部的,这些变量第二次被销毁了。
Rushi Ayyappa

5

使用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的滚动区分开。此答案的进一步简化版本可以在此处找到(Kotlin)
Matteljay

4

下面的代码将解决您的问题

    @Override
        public boolean onTouchEvent(MotionEvent event){
            switch(event.getAction()){
                case(MotionEvent.ACTION_DOWN):
                    x1 = event.getX();
                    y1 = event.getY();
                    打破;
                case(MotionEvent.ACTION_UP):{
                    x2 = event.getX();
                    y2 = event.getY();
                    dx = x2-x1;
                    dy = y2-y1;

                if(Math.abs(dx)> Math.abs(dy)) 
                {
                    if(dx> 0)move(1); //对
                    否则(dx == 0)move(5); //点击
                    否则move(2); //剩下
                } 
                其他 
                {
                    if(dy> 0)move(3); // 下
                    否则(dy == 0)move(5); //点击
                    否则move(4); //向上
                }
                }
            }
            返回true;
        }


3

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


谢谢,我也计划过类似的事情,但以为有人知道其他解决方案。您是否也体验过这种行为(单击时向下->上移->上移)?
阿纳斯塔西娅

是的,我目前正在从事涉及多点触控的游戏,并且我对Poitners和Move_Events进行了广泛的测试。这是我来到测试时(即ACTION_DOWN事件很快得到改变,以ACTION_MOVE的结论之一高兴能帮上忙。
testingtester

好的,我并不孤单是很好的。:)我还没有找到有人在stackoverflow或其他地方遇到这样的问题。谢谢。
阿纳斯塔西娅2012年

1
您在哪些设备上看到过这种行为?我在三星Galaxy Ace和Gio上注意到了这一点。
阿纳斯塔西娅

我已经在Samsung Captivate和Galaxy S II上亲眼看到了它,但我想几乎所有设备都可能会看到这样的样子
testingtester 2012年

1

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

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

}

1

除了上述答案外,如果您想同时执行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;
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.