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

84

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

一个点击事件是由一个ACTION_DOWN事件、多个ACTION_MOVE事件和一个ACTION_UP事件组成的序列。理论上,如果你获取了一个ACTION_DOWN事件然后是一个ACTION_UP事件 - 这意味着用户刚刚单击了你的View。

但在实践中,这种序列在某些设备上不起作用。在我的三星Galaxy Gio手机上,当我只点击我的View时,我得到这样的序列: ACTION_DOWN, 多次ACTION_MOVE, 然后是ACTION_UP。也就是说,我会得到一些意外的带有ACTION_MOVE操作码的OnTouchEvent触发。我从来没有(或者几乎没有)得到过序列ACTION_DOWN -> ACTION_UP。

我也无法使用OnClickListener,因为它不提供点击位置。那么我该如何检测点击事件并将其与移动事件区分开来?


你尝试使用onTouch而不是onTouchEvent了吗?这样做可以让你引用到视图,从而记录值并查看是否调用了ACTION_DOWN和ACTION_UP的点击事件。 - Arif Nadeem
11个回答

123

这里有另一种解决方案,非常简单,不需要你担心手指移动的问题。如果你将点击定义为简单的移动距离,那么如何区分单击和长按呢。

你可以加入更多智能功能,包括所移动的距离,但我还没有遇到过用户在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;
    }
});

1
为什么不使用 ViewConfiguration.getLongPressTimeout() 作为点击的最大持续时间? - Flo
2
对于长按,您肯定可以使用该方法,但这仅适用于标准单击。 - Stimsoni
11
建议使用 event.getEventTime() - event.getDownTime() 计算点击持续时间。 - Grimmy
4
因为你不需要使用独立的领域或者与日历一起工作,所以可以安心地休息了。你只需要从一个数字中减去另一个数字即可。 - Grimmy
使用 ACTION_DOWN 和 ACTION_UP 之间的距离更可靠。 - Ray Lionfang
显示剩余3条评论

60

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

  1. 首先是在 ACTION_DOWN 事件和 ACTION_UP 事件之间移动的距离。我想指定允许的最大距离为密度无关像素而不是像素,以更好地支持不同的屏幕。例如,15 DP
  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
这个答案有一个问题,您认为从移动事件中记录距离会更好吗?如果我手指在视图中开始,我可以在一秒钟内向上滑到屏幕顶部,然后再滑回底部,它仍然会被计算为一次点击。如果在移动操作中记录这个,它可以设置一个布尔值,如果事件超过了点击区域,就可以将其设置为真。 - Gabriel
@Gabriel:好久不见了,但我认为我刚刚发现这对我的需求来说已经“足够好了”。如果你用那种方法得到更好的结果,请随意尝试,并考虑发布一个新答案 :-) - Jonik
我认为我的用例比大多数人更具体,我有一个视图可以从屏幕底部拖到顶部,或者单击标题切换位置。这意味着如果您快速将其向上拖动然后向下拖动,它仍然会识别点击。不过还是谢谢你,你的解决方案几乎满足了我所有需求,我只是稍微修改了一下就达到了目的。今晚稍后我会发布我的代码作为另一种答案,供那些需要更精确的人参考。 - Gabriel
2
哦,你的 onTouchEvent() 缺少一个返回值。如果您可以告诉我们该写什么 (return true;return false;return super.onTouchEvent(e); ?),那将非常有帮助。 - Pang
显示剩余3条评论

33

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

28

使用探测器,它能有效工作,并且在被拖动时不会触发

领域:

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

享受 :)


喜爱的解决方案。但是,如果我想为多个视图使用一个Touch/Gesture监听器,我必须在上一个onTouch事件期间跟踪最后一个视图,以便我知道onSingleTapConfirmed事件来自哪个视图。如果MotionEvent为您存储视图,则会很好。 - AlanKley
对于那些在运行此代码时遇到问题的人,我必须将函数onTouch(View v, MotionEvent event)的返回值设置为“true”才能使其正常工作。实际上,我认为我们应该消费这些事件并返回true。 - 黄锐铭

7
为了获得最优化的点击事件识别,我们需要考虑两件事情:
  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,好的方法。我选择了一个类似的解决方案(https://dev59.com/fmkw5IYBdhLWcg3wVpFi#19788836),只是用dp代替px作为“MAX_CLICK_DISTANCE”,并且计算距离的方式有些不同。 - Jonik
这些变量应该在全局范围内声明。 - Rushi Ayyappa
在大多数情况下,您只需在视图上设置一次onTouchListenersetOnTouchListener()接受匿名类引用。其中变量在类内部是全局的。我认为这样做可以,除非有人使用其他侦听器实例再次调用setOnTouchListener() - Gem
我的touchListener是内部的,这些变量被第二次销毁。所以我不得不在全局声明它们,这样就解决了问题。 - Rushi Ayyappa

6

使用Gil SH的答案,我改善了它,并实现了onSingleTapUp()而不是onSingleTapConfirmed()。这样做更快,如果被拖动/移动也不会单击视图。

GestureTap:

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();
                    break;
                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); // 向右移动 else if (dx == 0) move(5); // 点击事件 else move(2); // 向左移动 } else { if(dy>0) move(3); // 向下移动 else if (dy == 0) move(5); // 点击事件 else move(4); // 向上移动 } } } return true; }

3

如果没有ACTION_MOVE事件发生,很难出现ACTION_DOWN事件。即使您在屏幕上轻微地移动手指到与第一次触摸不同的位置,也会触发MOVE事件。此外,我认为手指压力的改变也会触发MOVE事件。我建议在Action_Move方法中使用if语句来确定移动距离与原始DOWN动作的距离。如果移动超出某个设定的半径范围,您的MOVE动作将会发生。这可能不是最好的、资源有效的方法,但应该可以解决您的问题。


谢谢,我也计划做些类似的事情,但是想知道是否有其他的解决方案。你也遇到过这样的行为吗(点击时向下->移动->向上)? - some.birdie
是的,我目前正在开发一个涉及多点触控的游戏,并对指针和Move_Events进行了广泛测试。这是我在测试中得出的结论之一(即Action_Down事件很快就会变成Action_Move)。很高兴我能帮到你。 - testingtester
好的,很好,我不是一个人 :) 我在stackoverflow或其他地方没有找到有这样问题的人。谢谢。 - some.birdie
1
你在哪些设备上看到这种行为?我注意到这在三星 Galaxy Ace 和 Gio 上发生过。 - some.birdie
我个人在三星Captivate和Galaxy S II上见过这种情况,但我想对于几乎所有设备来说都是这样的。 - testingtester

2

除了以上的答案,如果你想要实现单击和拖动两个动作,则可以使用我下面的代码。参考了@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;
    }

1
如果你只想在点击时作出反应,请使用:

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

}

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