Wie kann man zwischen Verschieben und Klicken in onTouchEvent () unterscheiden?

78

In meiner Anwendung muss ich sowohl Verschiebungs- als auch Klickereignisse verarbeiten.

Ein Klick ist eine Folge von einer ACTION_DOWN-Aktion, mehreren ACTION_MOVE-Aktionen und einer ACTION_UP-Aktion. Wenn Sie theoretisch ein ACTION_DOWN-Ereignis und dann ein ACTION_UP-Ereignis erhalten, bedeutet dies, dass der Benutzer gerade auf Ihre Ansicht geklickt hat.

In der Praxis funktioniert diese Sequenz jedoch auf einigen Geräten nicht. Auf meinem Samsung Galaxy Gio erhalte ich solche Sequenzen, wenn ich einfach auf meine Ansicht klicke: ACTION_DOWN, mehrmals ACTION_MOVE, dann ACTION_UP. Dh ich erhalte einige unerwartete OnTouchEvent-Zündungen mit dem Aktionscode ACTION_MOVE. Ich bekomme nie (oder fast nie) die Sequenz ACTION_DOWN -> ACTION_UP.

Ich kann OnClickListener auch nicht verwenden, da es nicht die Position des Klicks angibt. Wie kann ich also ein Klickereignis erkennen und vom Verschieben unterscheiden?

Anastasia
quelle
Haben Sie versucht, onTouch anstelle von onTouchEvent zu verwenden? Auf diese Weise erhalten Sie auch einen Verweis auf die Ansicht, sodass Sie die Werte abmelden und sehen können, ob der Klick ACTION_DOWN und ACTION_UP aufgerufen wird oder nicht ...
Arif Nadeem

Antworten:

116

Hier ist eine weitere Lösung, die sehr einfach ist und bei der Sie sich keine Sorgen machen müssen, dass der Finger bewegt wird. Wenn Sie einen Klick einfach auf die zurückgelegte Entfernung stützen, wie können Sie dann einen Klick und einen langen Klick unterscheiden?

Sie könnten mehr Intelligenz in diese Sache stecken und die zurückgelegte Entfernung einbeziehen, aber ich bin noch nicht auf eine Instanz gestoßen, bei der die Entfernung, die ein Benutzer in 200 Millisekunden zurücklegen kann, eine Bewegung im Gegensatz zu einem Klick darstellen sollte.

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;
    }
});
Stimsoni
quelle
Warum nicht ViewConfiguration.getLongPressTimeout()als maximale Dauer für einen Klick verwenden?
Flo
1
Für ein langes Drücken könnten Sie definitiv diese Methode verwenden, aber dies ist nur für einen Standardklick.
Stimsoni
9
Eigentlich könnte es besser sein, event.getEventTime () -event.getDownTime () zu verwenden, um die Klickdauer zu berechnen
Grimmy
@Grimmy Warum sollte das besser sein?
RestInPeace
4
@RestInPeace, da Sie kein separates Feld benötigen und mit dem Kalender arbeiten müssen. Sie müssen nur eine Zahl von einer anderen subtrahieren.
Grimmy
55

Ich habe die besten Ergebnisse erzielt, indem ich Folgendes berücksichtigt habe:

  1. In erster Linie die Entfernung zwischen ACTION_DOWNund ACTION_UPEreignissen. Ich wollte den maximal zulässigen Abstand in dichteunabhängigen Pixeln anstelle von Pixeln angeben , um verschiedene Bildschirme besser unterstützen zu können. Zum Beispiel 15 DP .
  2. Zweitens die Dauer zwischen den Ereignissen. Eine Sekunde schien maximal gut zu sein. (Einige Leute "klicken" ziemlich "gründlich", dh langsam; das möchte ich immer noch erkennen.)

Beispiel:

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

Die Idee hier ist dieselbe wie in Gems Lösung , mit folgenden Unterschieden:

  • Dies berechnet den tatsächlichen euklidischen Abstand zwischen den beiden Punkten.
  • Dies verwendet dp anstelle von px.

Update (2015): Schauen Sie sich auch Gabriels fein abgestimmte Version davon an .

Jonik
quelle
Hervorragende Antwort! Das hat einiges an Recherche gespart, danke!
2Dee
Eine Frage für diese Antwort: Halten Sie es für besser, die Entfernung innerhalb des Bewegungsereignisses aufzuzeichnen? Wenn ich mit dem Finger in der Ansicht beginne, kann ich innerhalb einer Sekunde nach oben und unten nach unten wischen, was immer noch als Klick zählt. Wenn dies innerhalb der Verschiebungsaktion aufgezeichnet wurde, könnte ein Boolescher Wert festgelegt werden, falls das Ereignis jemals den Klickbereich überschreitet.
Gabriel
@ Gabriel: Es ist eine Weile her, aber ich denke, ich fand das gerade "gut genug" für meine Bedürfnisse. Sie können
gerne
Ich glaube, ich hatte einen etwas spezifischeren Anwendungsfall als die meisten Menschen. Ich habe eine Ansicht, die von unten nach oben auf dem Bildschirm gezogen werden kann, oder die Kopfzeile kann angeklickt werden, um die Position umzuschalten. Dies bedeutete, dass das Klicken immer noch erkannt wurde, wenn Sie es schnell nach oben und dann wieder nach unten zogen. Vielen Dank, Ihre Lösung war fast 100% von dem, was ich brauchte. Ich habe nur ein paar geringfügige Änderungen daran vorgenommen, um mich dorthin zu bringen. Ich werde meinen Code später heute Abend als alternative Antwort für Leute veröffentlichen, die etwas mehr Präzision benötigen.
Gabriel
2
Oh, und Ihnen onTouchEvent()fehlt ein Rückgabewert. Es wäre hilfreich , wenn Sie uns zeigen , was dort zu schreiben ( return true;? return false;? return super.onTouchEvent(e);?)
Pang
29

Unter Joniks Führung habe ich eine etwas feinere Version erstellt, die sich nicht als Klick registriert, wenn Sie Ihren Finger bewegen und dann zur Stelle zurückkehren, bevor Sie loslassen:

Also hier ist meine Lösung:

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

Verwenden Sie den Detektor. Er funktioniert und wird beim Ziehen nicht angehoben

Feld:

private GestureDetector mTapDetector;

Initialisieren:

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

Innere Klasse:

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

auf Berührung:

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

Genießen :)

Gil SH
quelle
Lieblingslösung. Wenn ich jedoch einen Touch / Gesture-Listener für mehrere Ansichten verwenden möchte, muss ich die letzte Ansicht während des letzten onTouch-Ereignisses verfolgen, damit ich weiß, aus welcher Ansicht das onSingleTapConfirmed-Ereignis stammt. Es wäre schön, wenn das MotionEvent die Ansicht für Sie speichern würde.
AlanKley
Für diejenigen, die möglicherweise Probleme beim Ausführen dieses Codes haben, muss ich den Rückgabewert der Funktion onTouch (View v, MotionEvent-Ereignis) auf "true" setzen, damit dies funktioniert. Eigentlich denke ich, wir konsumieren die Ereignisse und sollten true zurückgeben.
5.
6

Um eine optimale Optimierung des Klickereignisses zu erzielen, müssen wir zwei Dinge berücksichtigen:

  1. Zeitdifferenz zwischen ACTION_DOWN und ACTION_UP.
  2. Unterschied zwischen x und y, wenn der Benutzer den Finger berührt und loslässt.

Eigentlich kombiniere ich die Logik von Stimsoni und Neethirajan

Also hier ist meine Lösung:

        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;
        }
    });
Juwel
quelle
+1, guter Ansatz. Ich habe mich für eine ähnliche Lösung entschieden , außer dass ich dp anstelle von px für verwendet MAX_CLICK_DISTANCEund die Entfernung etwas anders berechnet habe.
Jonik
Diese oben deklarierte Variable sollte global deklariert werden.
Rushi Ayyappa
@RushiAyyappa In den meisten Fällen setzen Sie den onTouchListener einmal in der Ansicht. Das setOnTouchListener()nimmt anonyme Klassenreferenz. Wo die Variablen innerhalb der Klasse global sind. Ich denke, das wird funktionieren, wenn nicht jemand setOnTouchListener()mit einer anderen Instanz des Hörers erneut anruft.
Gem
Mein touchListener ist inner und diese Variablen wurden zum zweiten Mal zerstört. Also musste ich sie global deklarieren und das hat funktioniert.
Rushi Ayyappa
6

Mit Gil SH Antwort habe ich es verbessert, indem ich onSingleTapUp()eher implementiert als onSingleTapConfirmed(). Es ist viel schneller und klickt beim Ziehen / Verschieben nicht auf die Ansicht.

GestureTap:

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

Verwenden Sie es wie:

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;
    }
});
Hussein El Feky
quelle
Diese Antwort hat mir geholfen, eine Berührung von einer Schriftrolle in einer Textansicht zu unterscheiden. Eine noch weiter abgespeckte Version dieser Antwort finden Sie hier (Kotlin)
Matteljay
4

Der folgende Code löst Ihr Problem

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

                if (Math.abs (dx)> Math.abs (dy)) 
                {
                    wenn (dx> 0) bewege (1); //Recht
                    sonst wenn (dx == 0) bewegen (5); //klicken
                    sonst bewege dich (2); //links
                }} 
                sonst 
                {
                    wenn (dy> 0) bewege (3); // Nieder
                    sonst wenn (dy == 0) bewegen (5); //klicken
                    sonst bewege dich (4); //oben
                }}
                }}
            }}
            return true;
        }}

Neethirajan
quelle
3

Es ist sehr schwierig, dass ein ACTION_DOWN auftritt, ohne dass ein ACTION_MOVE auftritt. Das geringste Zucken Ihres Fingers auf dem Bildschirm an einer anderen Stelle als an der Stelle, an der die erste Berührung stattgefunden hat, löst das MOVE-Ereignis aus. Ich glaube auch, dass eine Änderung des Fingerdrucks auch das MOVE-Ereignis auslösen wird. Ich würde eine if-Anweisung in der Action_Move-Methode verwenden, um zu versuchen, die Entfernung zu bestimmen, in der die Bewegung von der ursprünglichen DOWN-Bewegung entfernt war. Wenn die Bewegung außerhalb eines festgelegten Radius erfolgt, wird Ihre MOVE-Aktion ausgeführt. Es ist wahrscheinlich nicht die beste und ressourceneffiziente Methode, um das zu tun, was Sie versuchen, aber es sollte funktionieren.

Testingtester
quelle
Danke, ich habe auch so etwas geplant, dachte aber, dass jemand andere Lösungen kennt. Haben Sie auch ein solches Verhalten erlebt (Abwärts-> Verschieben-> Aufwärts beim Klicken)?
Anastasia
Ja, ich arbeite derzeit an einem Spiel mit Multitouch und habe einige umfangreiche Tests in Bezug auf Poitners und und Move_Events durchgeführt. Dies war eine der Schlussfolgerungen, zu denen ich beim Testen gekommen bin (dieses Action_Down-Ereignis wird schnell in Action_Move geändert. Ich bin froh, dass ich helfen konnte.
Testingtester
Ok, das ist gut, dass ich nicht alleine bin :) Ich habe niemanden mit einem solchen Problem im Stackoverflow oder anderswo gefunden. Vielen Dank.
Anastasia
1
Auf welchen Geräten haben Sie ein solches Verhalten gesehen? Ich habe dies bei Samsung Galaxy Ace und Gio bemerkt.
Anastasia
Ich habe es persönlich auf einem Samsung Captivate und einem Galaxy S II gesehen, aber ich stelle mir vor, dass es wahrscheinlich für fast alle Geräte so ist
Testingtester
1

Wenn Sie nur auf einen Klick reagieren möchten, verwenden Sie:

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

}
YTerle
quelle
1

Wenn Sie zusätzlich zu den obigen Antworten sowohl onClick- als auch Drag-Aktionen implementieren möchten, können Sie meinen folgenden Code verwenden. Nehmen Sie die Hilfe von @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;
    }
Rushi Ayyappa
quelle