Як відрізнити переміщення та клацання в onTouchEvent ()?


78

У моїй програмі мені потрібно обробляти події переміщення та натискання.

Клацання - це послідовність однієї дії ACTION_DOWN, декількох дій ACTION_MOVE та однієї дії ACTION_UP. Теоретично, якщо ви отримуєте подію ACTION_DOWN, а потім подію ACTION_UP - це означає, що користувач щойно клацнув ваш перегляд.

Але на практиці ця послідовність не працює на деяких пристроях. На своєму Samsung Galaxy Gio я отримую такі послідовності, просто натискаючи мій Перегляд: ACTION_DOWN, кілька разів ACTION_MOVE, потім ACTION_UP. Тобто я отримую кілька непередбачуваних стрільб OnTouchEvent з кодом дії ACTION_MOVE. Я ніколи (або майже ніколи) не отримую послідовність ACTION_DOWN -> ACTION_UP.

Я також не можу використовувати OnClickListener, оскільки він не надає позиції клацання. То як я можу виявити подію кліку та відрізнити її від переміщення?


ви намагалися використовувати onTouch замість onTouchEvent, зробивши це, у вас також буде посилання на подання, щоб ви могли вийти зі значень і подивитися, чи викликається клік ACTION_DOWN та ACTION_UP чи ні ...
Arif Nadeem

Відповіді:


116

Ось ще одне рішення, яке дуже просте і не вимагає від вас турботи про переміщення пальця. Якщо ви базуєте клацання як просто переміщену відстань, то як ви можете розрізнити клацання та довгий клік.

Ви можете вкласти в це більше розуму і включити відстань, але я ще не натрапив на випадок, коли відстань, яку користувач може пройти за 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()як максимальну тривалість клацання?
Фло

1
При тривалому натисканні ви точно можете скористатися цим методом, але це для звичайного клацання.
Стімсоні,

9
Насправді може бути краще використовувати event.getEventTime () -event.getDownTime () для обчислення тривалості кліків
Grimmy

@Grimmy Чому це може бути краще?
RestInPeace

4
@RestInPeace, тому що вам не потрібно мати відокремлене поле та працювати з календарем. Вам потрібно просто відняти одне число від іншого.
Grimmy

55

Я отримав найкращі результати, врахувавши:

  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): також ознайомтеся з досконалою версією Габріеля .


Відмінна відповідь! Це заощадило чимало досліджень, дякую!
2

Одне питання для цієї відповіді, чи вважаєте ви, що було б краще записати відстань всередині події переміщення? Якщо я починаю пальцем у поданні, я можу провести пальцем угору до екрана і назад донизу протягом секунди, і це все одно буде вважатися клацанням. Якщо це було записано всередині дії переміщення, це може встановити логічне значення, якщо подія коли-небудь перевершила область кліку.
Габріель

@ Габріель: Минув якийсь час, але я думаю, що я просто виявив, що це "досить добре" для моїх потреб. Не соромтеся експериментувати, якщо ви отримаєте кращі результати з таким підходом, і якщо так, то розгляньте можливість розміщення нової відповіді :-)
Йонік,

Думаю, у мене був дещо конкретніший варіант використання, ніж у більшості людей, у мене є погляд, який можна перетягнути знизу до верху екрана, або натиснути на заголовок, щоб змінити позицію. Це означало, що якщо ви перетягнули його вгору, а потім швидко повернули вниз, він все ще розпізнавав клацання. Дякую, однак, ваше рішення було майже на 100% від того, що мені потрібно, я просто зробив деякі незначні зміни в ньому, щоб я потрапив туди. Я опублікую свій код пізніше сьогодні ввечері як альтернативну відповідь для людей, яким потрібна трохи більша точність.
Габріель

2
Ох, і у вас onTouchEvent()відсутнє повернене значення. Було б корисно , якби ви показати нам , що там писати ( return true;? return false;? return super.onTouchEvent(e);?)
Pang

29

Взявши на себе керівництво Джонік я створив трохи більш довершену версію, яка не реєструється як клацання, якщо поворушити пальцем, а потім повернутися до місця, перш ніж відпустити:

Тож ось моє рішення:

/**
 * 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) у "true", щоб це працювало. Власне, я думаю, що ми споживаємо події і повинні повернутися істиною.
黄锐铭

6

Щоб отримати найбільш оптимізоване розпізнавання події кліку, ми повинні врахувати дві речі:

  1. Різниця в часі між ACTION_DOWN та ACTION_UP.
  2. Різниця між х, у, коли користувач торкається і коли відпускає палець.

Насправді я поєдную логіку Стімсоні та Нітіраджана

Тож ось моє рішення:

        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 для MAX_CLICK_DISTANCE, і обчислення відстані трохи інакше.
Джонік

зазначені вище змінні повинні бути оголошені глобально.
Руші Айяппа,

@RushiAyyappa У більшості випадків ви один раз встановлюєте onTouchListener у поданні. setOnTouchListener()Приймає посилання на анонімне клас. Де змінні є глобальними всередині класу. Я думаю, що це спрацює, якщо хтось не зателефонує setOnTouchListener()ще раз з іншим екземпляром слухача.
Gem

мій 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. Ще більш урізану версію цієї відповіді можна знайти тут (Котлін)
Маттеляй

4

Нижче код вирішить вашу проблему

    @Override
        public boolean onTouchEvent (подія MotionEvent) {
            switch (event.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;

                if (Math.abs (dx)> Math.abs (dy)) 
                {
                    якщо (dx> 0) перемістити (1); // праворуч
                    інакше якщо (dx == 0) перемістити (5); // клацаємо
                    ще рухатись (2); // зліва
                } 
                ще 
                {
                    якщо (dy> 0) перемістити (3); // вниз
                    інакше якщо (dy == 0) перемістити (5); // клацаємо
                    ще рухатись (4); // вгору
                }
                }
            }
            повернути істинно;
        }


3

Дуже складно для ACTION_DOWN відбутися без ACTION_MOVE. Найменший поштовх пальця на екрані в іншому місці, ніж те, де стався перший дотик, спричинить подію MOVE. Крім того, я вважаю, що зміна тиску пальцем також спричинить подію MOVE. Я б використав оператор if у методі Action_Move, щоб спробувати визначити відстань, коли переміщення відбулося від початкового руху ВНИЗ. якщо переміщення відбулося поза деяким заданим радіусом, відбудеться ваша дія MOVE. Це, мабуть, не найкращий ресурсозберігаючий спосіб зробити те, що намагаєтесь, але він повинен працювати.


Дякую, я також планував щось подібне, але думав, що хтось знає деякі інші рішення. Ви також мали досвід такої поведінки (вниз-> переміщення-> вгору під час клацання)?
Анастасія

Так, зараз я працюю над грою, яка включає мультитач, і я провів кілька обширних тестів щодо пуансерів та та Move_Events. Це був один із висновків, до якого я прийшов під час тестування (що подія Action_Down швидко змінюється на Action_Move. Радий, що міг допомогти.
testingtester

Гаразд, добре, що я не один :) Я не знайшов когось із такою проблемою на stackoverflow чи десь ще. Дякую.
Анастасія

1
На яких пристроях ви бачили таку поведінку? Я помітив це на Samsung Galaxy Ace та Gio.
Анастасія

Я бачив це особисто на Samsung Captivate та Galaxy S II, але, думаю, це, мабуть, подібне майже для всіх пристроїв
testingtester,

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;
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.