关于android:如何在onTouchEvent()中区分移动和单击?

How to distinguish between move and click in onTouchEvent()?

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

点击是一个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,因为它没有给出点击的位置。 因此,如何检测点击事件并将其与移动区别?


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
    }
});


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

  • 首先,距离在ACTION_DOWNACTION_UP事件之间移动。我想以与密度无关的像素而不是像素指定最大允许距离,以更好地支持不同的屏幕。例如15 DP。
  • 其次,事件之间的持续时间。一秒钟似乎是很好的最大值。 (有些人非常"彻底"地(即缓慢地)"点击";我仍然想识别出来。)
  • 例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /**
     * 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的解决方案相同,但有以下区别:

    • 这将计算两点之间的实际欧几里得距离。
    • 这使用dp而不是px。

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


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

    所以这是我的解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /**
     * 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;
    }

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

    领域:

    1
    private GestureDetector mTapDetector;

    初始化:

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

    内部舱位:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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:

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

    请享用 :)


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

  • ACTION_DOWN和ACTION_UP之间的时差。
  • 当用户触摸和松开手指时,x,y之间的差异。
  • 实际上,我结合了Stimsoni和Neethirajan给出的逻辑

    所以这是我的解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
            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;
            }
        });


    使用Gil SH答案,我通过实现onSingleTapUp()而不是onSingleTapConfirmed()对其进行了改进。速度更快,并且如果拖动/移动,将不会单击视图。

    手势点击:

    1
    2
    3
    4
    5
    6
    7
    public class GestureTap extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            button.performClick();
            return true;
        }
    }

    像这样使用它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;
        }
    });


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
        @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); //right
                        else if (dx == 0) move(5); //click
                        else move(2); //left
                    }
                    else
                    {
                        if(dy>0) move(3); // down
                        else if (dy == 0) move(5); //click
                        else move(4); //up
                    }
                    }
                }
                return true;
            }

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
         // 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
    2
    3
    if (event.getAction() == MotionEvent.ACTION_UP) {

    }