Fling gesture detection on grid layout
Asked Answered
C

18

1162

I want to get fling gesture detection working in my Android application.

What I have is a GridLayout that contains 9 ImageViews. The source can be found here: Romain Guys's Grid Layout.

That file I take is from Romain Guy's Photostream application and has only been slightly adapted.

For the simple click situation I need only set the onClickListener for each ImageView I add to be the main activity which implements View.OnClickListener. It seems infinitely more complicated to implement something that recognizes a fling. I presume this is because it may span views?

  • If my activity implements OnGestureListener I don't know how to set that as the gesture listener for the Grid or the Image views that I add.

    public class SelectFilterActivity extends Activity implements
       View.OnClickListener, OnGestureListener { ...
    
  • If my activity implements OnTouchListener then I have no onFling method to override (it has two events as parameters allowing me to determine if the fling was noteworthy).

    public class SelectFilterActivity extends Activity implements
        View.OnClickListener, OnTouchListener { ...
    
  • If I make a custom View, like GestureImageView that extends ImageView I don't know how to tell the activity that a fling has occurred from the view. In any case, I tried this and the methods weren't called when I touched the screen.

I really just need a concrete example of this working across views. What, when and how should I attach this listener? I need to be able to detect single clicks also.

// Gesture detection
mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        int dx = (int) (e2.getX() - e1.getX());
        // don't accept the fling if it's too short
        // as it may conflict with a button push
        if (Math.abs(dx) > MAJOR_MOVE && Math.abs(velocityX) > Math.absvelocityY)) {
            if (velocityX > 0) {
                moveRight();
            } else {
                moveLeft();
            }
            return true;
        } else {
            return false;
        }
    }
});

Is it possible to lay a transparent view over the top of my screen to capture flings?

If I choose not to inflate my child image views from XML can I pass the GestureDetector as a constructor parameter to a new subclass of ImageView that I create?

This is the very simple activity that I'm trying to get the fling detection to work for: SelectFilterActivity (Adapted from photostream).

I've been looking at these sources:

Nothing has worked for me so far and I was hoping for some pointers.

Cutlet answered 1/6, 2009 at 23:35 Comment(1)
How to solve this problem? Please answer #60465412Maraud
C
840

Thanks to Code Shogun, whose code I adapted to my situation.

Let your activity implementOnClickListener as usual:

public class SelectFilterActivity extends Activity implements OnClickListener {

  private static final int SWIPE_MIN_DISTANCE = 120;
  private static final int SWIPE_MAX_OFF_PATH = 250;
  private static final int SWIPE_THRESHOLD_VELOCITY = 200;
  private GestureDetector gestureDetector;
  View.OnTouchListener gestureListener;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    /* ... */

    // Gesture detection
    gestureDetector = new GestureDetector(this, new MyGestureDetector());
    gestureListener = new View.OnTouchListener() {
      public boolean onTouch(View v, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
      }
    };

  }

  class MyGestureDetector extends SimpleOnGestureListener {
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
      try {
        if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)
          return false;
        // right to left swipe
        if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
          Toast.makeText(SelectFilterActivity.this, "Left Swipe", Toast.LENGTH_SHORT).show();
        } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
          Toast.makeText(SelectFilterActivity.this, "Right Swipe", Toast.LENGTH_SHORT).show();
        }
      } catch (Exception e) {
         // nothing
      }
      return false;
    }

    @Override
    public boolean onDown(MotionEvent e) {
      return true;
    }
  }
}

Attach your gesture listener to all the views you add to the main layout;

// Do this for each view added to the grid
imageView.setOnClickListener(SelectFilterActivity.this); 
imageView.setOnTouchListener(gestureListener);

Watch in awe as your overridden methods are hit, both the onClick(View v) of the activity and the onFling of the gesture listener.

public void onClick(View v) {
  Filter f = (Filter) v.getTag();
  FilterFullscreenActivity.show(this, input, f);
}

The post 'fling' dance is optional but encouraged.

Cutlet answered 1/6, 2009 at 23:35 Comment(16)
Thank you for this code! It was very helpful. However, I ran into one very very frustrating catch while trying to get gestures working. In my SimpleOnGestureListener, I have to override onDown for any of my gestures to register. It can just return true but i has to be defined. P.S: I don't know if its my api revision or my hardware, but i'm using 1.5 on a HTC Droid Eris.Faculty
I tried your code and it doesn't matter if I swipe or click (with my mouse, cause I work in emulator), I always get a Toast I defined in onClick method, so the emulator detects only clicks, without swipes. Why is it so?Cyclo
I tried this code and it did not work. still was unable to scroll at all when i apply a onClick listener to one of the child views inside my gallery viewGuidotti
Iomza: did you try putting break statements and stepping through your code?Poirier
Kudos for using an inner class! Very clean approach.Poirier
What is this onClick function, it is giving me errors.. please helpSabinesabino
how do you get the value for this SWIPE_MAX_OFF_PATH?Costrel
If i want to have a fragment on top of my activity, would I do fragment.setOnClickListener(SelectFilterActivity.this); fragment.setOnTouchListener(gestureListener);Rahman
suppose i need to have the functionality of handling touches by myself too (for example for dragging) , what should i do?Antecedent
Got it working for DoubleTap by following the implementation of SimpleGestureListener.Oxcart
My setOnClickListener did not work. So I had to override onSingleTapConfirmed and it worked as expected.Audette
this is a classic example with the basic swiping options, thank you for the elementary! dr watson!Rheo
Using this code but it's not working with my child fragment. Would be great if you guys can take a look at it #29849254Desiderata
@Sarvesh can some one pls tell me how to enable siwpe left or right in button while button is at viewpager position's 0 and i m unable to do both siwping on button and page swipe ?Viipuri
still, nobody explained what this onclick() doing and what is inside itAssumptive
You should probably credit Code Shogun, as I don't see a reference of credit link in your post. Where did this come from?Neoimpressionism
C
215

One of the answers above mentions handling different pixel density but suggests computing the swipe parameters by hand. It is worth noting that you can actually obtain scaled, reasonable values from the system using ViewConfiguration class:

final ViewConfiguration vc = ViewConfiguration.get(getContext());
final int swipeMinDistance = vc.getScaledPagingTouchSlop();
final int swipeThresholdVelocity = vc.getScaledMinimumFlingVelocity();
final int swipeMaxOffPath = vc.getScaledTouchSlop();
// (there is also vc.getScaledMaximumFlingVelocity() one could check against)

I noticed that using these values causes the "feel" of fling to be more consistent between the application and rest of system.

Captivate answered 20/4, 2011 at 17:26 Comment(2)
I use swipeMinDistance = vc.getScaledPagingTouchSlop() and swipeMaxOffPath = vc.getScaledTouchSlop().Spiroid
getScaledTouchSlop gives me very little offset result, awkwardly. For example only 24 pixels on a 540 high screen, that's very hard to keep it in range with the finger. :SEton
M
152

I do it a little different, and wrote an extra detector class that implements the View.onTouchListener

onCreateis simply add it to the lowest layout like this:

ActivitySwipeDetector activitySwipeDetector = new ActivitySwipeDetector(this);
lowestLayout = (RelativeLayout)this.findViewById(R.id.lowestLayout);
lowestLayout.setOnTouchListener(activitySwipeDetector);

where id.lowestLayout is the id.xxx for the view lowest in the layout hierarchy and lowestLayout is declared as a RelativeLayout

And then there is the actual activity swipe detector class:

public class ActivitySwipeDetector implements View.OnTouchListener {

static final String logTag = "ActivitySwipeDetector";
private Activity activity;
static final int MIN_DISTANCE = 100;
private float downX, downY, upX, upY;

public ActivitySwipeDetector(Activity activity){
    this.activity = activity;
}

public void onRightSwipe(){
    Log.i(logTag, "RightToLeftSwipe!");
    activity.doSomething();
}

public void onLeftSwipe(){
    Log.i(logTag, "LeftToRightSwipe!");
    activity.doSomething();
}

public void onDownSwipe(){
    Log.i(logTag, "onTopToBottomSwipe!");
    activity.doSomething();
}

public void onUpSwipe(){
    Log.i(logTag, "onBottomToTopSwipe!");
    activity.doSomething();
}

public boolean onTouch(View v, MotionEvent event) {
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

       // swipe horizontal?
        if(Math.abs(deltaX) > Math.abs(deltaY))
        {
            if(Math.abs(deltaX) > MIN_DISTANCE){
                // left or right
                if(deltaX > 0) { this.onRightSwipe(); return true; }
                if(deltaX < 0) { this.onLeftSwipe(); return true; }
            }
            else {
                    Log.i(logTag, "Horizontal Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                    return false; // We don't consume the event
            }
        }
        // swipe vertical?
        else 
        {
            if(Math.abs(deltaY) > MIN_DISTANCE){
                // top or down
                if(deltaY < 0) { this.onDownSwipe(); return true; }
                if(deltaY > 0) { this.onUpSwipe(); return true; }
            }
            else {
                    Log.i(logTag, "Vertical Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                    return false; // We don't consume the event
            }
        }

            return true;
        }
    }
    return false;
}

}

Works really good for me!

Mishandle answered 21/4, 2011 at 10:24 Comment(13)
This actually made it much easier for me to apply gesture functionality, and required "less" wiring :D Thanks @ThomasPhilippine
This looks like a neat utility class - but I think your four on...swipe() methods should be interfacesCarpentry
these returns shouldn't be there (line "we dont consume the event"), isn't it? It disables vertical scrolling feature.Combustor
Isn't keeping reference to activity causing memory leaks?Bassist
Yes.Really It works well..nice code to implement for ontouch event in androidMeter
@JeffreyBlattman Could you elaborate and/or edit the post to fix them?Palacios
specifically, the onTouch() method. first, if the delta X is not big enough, it returns without checking the delta Y. the result is that is never detects the left-right swipes. second, it also shouldn't return true if it drops through finding no swipe. third, it shouldn't return true on action down. this prevents any other listener like onClick from working.Underlie
@Bassist it's not a problem as long a the object holding the reference is the same scope as the activity itself. the problem occurs when you keep a reference to an activity in a place that has a larger scope than the activity ... like from a static member for example.Underlie
i'm trying to use it in a camera app, for gestures related to camera operations. I'm not getting any registered events occuring. My outermost layout is RelativeLayout, in the XML of which I put its ID = relative_layout Now the three lines in onCreate() I put are: paste.org/59785Ragman
actually this is my full project setup. ge.tt/9P9t7sU/v/0 can you run and see if it's working for you?Ragman
This piece of code is very useful, but passing the activity as a parameter and then calling its methods isn't good design. A better choice would be setting the swipe callbacks as abstract methods and let the activity implement them.Cinquain
Just one note: if the activity has widgets that will be accepting input themselves, swipe gestures that start on such widgets will not work. See #31504565Jimenez
In my case MotionEvent.ACTION_CANCEL same as MotionEvent.ACTION_UP make it work as expected.Athletic
C
97

I slightly modified and repaired solution from Thomas Fankhauser

Whole system consists from two files, SwipeInterface and ActivitySwipeDetector


SwipeInterface.java

import android.view.View;

public interface SwipeInterface {

    public void bottom2top(View v);

    public void left2right(View v);

    public void right2left(View v);

    public void top2bottom(View v);

}

Detector

import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class ActivitySwipeDetector implements View.OnTouchListener {

    static final String logTag = "ActivitySwipeDetector";
    private SwipeInterface activity;
    static final int MIN_DISTANCE = 100;
    private float downX, downY, upX, upY;

    public ActivitySwipeDetector(SwipeInterface activity){
        this.activity = activity;
    }

    public void onRightToLeftSwipe(View v){
        Log.i(logTag, "RightToLeftSwipe!");
        activity.right2left(v);
    }

    public void onLeftToRightSwipe(View v){
        Log.i(logTag, "LeftToRightSwipe!");
        activity.left2right(v);
    }

    public void onTopToBottomSwipe(View v){
        Log.i(logTag, "onTopToBottomSwipe!");
        activity.top2bottom(v);
    }

    public void onBottomToTopSwipe(View v){
        Log.i(logTag, "onBottomToTopSwipe!");
        activity.bottom2top(v);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            // swipe horizontal?
            if(Math.abs(deltaX) > MIN_DISTANCE){
                // left or right
                if(deltaX < 0) { this.onLeftToRightSwipe(v); return true; }
                if(deltaX > 0) { this.onRightToLeftSwipe(v); return true; }
            }
            else {
                Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
            }

            // swipe vertical?
            if(Math.abs(deltaY) > MIN_DISTANCE){
                // top or down
                if(deltaY < 0) { this.onTopToBottomSwipe(v); return true; }
                if(deltaY > 0) { this.onBottomToTopSwipe(v); return true; }
            }
            else {
                Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                v.performClick();
            }
        }
        }
        return false;
    }

}

it is used like this:

ActivitySwipeDetector swipe = new ActivitySwipeDetector(this);
LinearLayout swipe_layout = (LinearLayout) findViewById(R.id.swipe_layout);
swipe_layout.setOnTouchListener(swipe);

And in implementing Activity you need to implement methods from SwipeInterface, and you can find out on which View the Swipe Event was called.

@Override
public void left2right(View v) {
    switch(v.getId()){
        case R.id.swipe_layout:
            // do your stuff here
        break;
    }       
}
Combustor answered 10/1, 2012 at 16:11 Comment(6)
I slightly modified it again, see the v.performClick();, which is used to not consume event to OnClickListener, if set on same viewCombustor
Hi, I'm a total beginner so this question might be really obvious or trivial but please answer. The part where you have written, it is used as: ActivitySwipeDetector swipe = new ActivitySwipeDetector(this); This statement will be a part of MainActivity, correct? Then, "this" will be an activity of MainActivity. Whereas the constructor takes an instance of SwipeInterface. Kindly help me out here. Thanks a lot.Blackstock
@Blackstock create new question, comment is not a fine place to ask like this.Combustor
@MarekSebera this doesn't work with ScrollView & ListView? how to handle them?Quintinquintina
@silentbang again, this is not place to ask such questions. please create new question thread.Combustor
the layout i am using is a RelativeLayout, can i just replace the text "LinearLayout" with "RelativeLayout"? Also when i try and implement your code with the following : "ActivitySwipeDetector swipe = new ActivitySwipeDetector(this)" I get the error "The Constructor ActivitySwipeDetector(MainActivity) is undefined" anyone know how to fix this?Unnecessary
A
68

The swipe gesture detector code above is very useful! You may however wish to make this solution density agnostic by using the following relative values (REL_SWIPE) rather than the absolute values (SWIPE_)

DisplayMetrics dm = getResources().getDisplayMetrics();

int REL_SWIPE_MIN_DISTANCE = (int)(SWIPE_MIN_DISTANCE * dm.densityDpi / 160.0f);
int REL_SWIPE_MAX_OFF_PATH = (int)(SWIPE_MAX_OFF_PATH * dm.densityDpi / 160.0f);
int REL_SWIPE_THRESHOLD_VELOCITY = (int)(SWIPE_THRESHOLD_VELOCITY * dm.densityDpi / 160.0f);
Allottee answered 18/2, 2011 at 9:45 Comment(7)
+1 for bringing this up. Note that DensityMetrics.densityDpi was introduced in API 4. For backward compatibility with API 1, use DensityMetrics.density instead. This then changes the calculation to be just SWIPE_MIN_DISTANCE * dm.density.Weinshienk
Where did you get the number 160.0f?Poirier
developer.android.com/guide/practices/screens_support.html Density-independent pixel (dp) The conversion of dp units to screen pixels is simple: px = dp * (dpi / 160)Allottee
I was looking all over for this. NO example of onFling() on the Internet has this, which will lead to poor UX. Thanks!Huggermugger
160.0f is the comes from the 160 DPI which is standard density upon which DP (density independent pixels) is based. public static final int DENSITY_MEDIUM Added in API level 4 Standard quantized DPI for medium-density screens. Constant Value: 160 (0x000000a0)Allottee
@ThaneAnthem I think you meant DisplayMetrics, not DensityMetrics. :PFrequent
Also, see ViewConfiguration which provides a number of standard values, adjusted for pixel density.Jimenez
L
38

My version of solution proposed by Thomas Fankhauser and Marek Sebera (does not handle vertical swipes):

SwipeInterface.java

import android.view.View;

public interface SwipeInterface {

    public void onLeftToRight(View v);

    public void onRightToLeft(View v);
}

ActivitySwipeDetector.java

import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public class ActivitySwipeDetector implements View.OnTouchListener {

    static final String logTag = "ActivitySwipeDetector";
    private SwipeInterface activity;
    private float downX, downY;
    private long timeDown;
    private final float MIN_DISTANCE;
    private final int VELOCITY;
    private final float MAX_OFF_PATH;

    public ActivitySwipeDetector(Context context, SwipeInterface activity){
        this.activity = activity;
        final ViewConfiguration vc = ViewConfiguration.get(context);
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        MIN_DISTANCE = vc.getScaledPagingTouchSlop() * dm.density;
        VELOCITY = vc.getScaledMinimumFlingVelocity();
        MAX_OFF_PATH = MIN_DISTANCE * 2;            
    }

    public void onRightToLeftSwipe(View v){
        Log.i(logTag, "RightToLeftSwipe!");
        activity.onRightToLeft(v);
    }

    public void onLeftToRightSwipe(View v){
        Log.i(logTag, "LeftToRightSwipe!");
        activity.onLeftToRight(v);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            Log.d("onTouch", "ACTION_DOWN");
            timeDown = System.currentTimeMillis();
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("onTouch", "ACTION_UP");
            long timeUp = System.currentTimeMillis();
            float upX = event.getX();
            float upY = event.getY();

            float deltaX = downX - upX;
            float absDeltaX = Math.abs(deltaX); 
            float deltaY = downY - upY;
            float absDeltaY = Math.abs(deltaY);

            long time = timeUp - timeDown;

            if (absDeltaY > MAX_OFF_PATH) {
                Log.i(logTag, String.format("absDeltaY=%.2f, MAX_OFF_PATH=%.2f", absDeltaY, MAX_OFF_PATH));
                return v.performClick();
            }

            final long M_SEC = 1000;
            if (absDeltaX > MIN_DISTANCE && absDeltaX > time * VELOCITY / M_SEC) {
                if(deltaX < 0) { this.onLeftToRightSwipe(v); return true; }
                if(deltaX > 0) { this.onRightToLeftSwipe(v); return true; }
            } else {
                Log.i(logTag, String.format("absDeltaX=%.2f, MIN_DISTANCE=%.2f, absDeltaX > MIN_DISTANCE=%b", absDeltaX, MIN_DISTANCE, (absDeltaX > MIN_DISTANCE)));
                Log.i(logTag, String.format("absDeltaX=%.2f, time=%d, VELOCITY=%d, time*VELOCITY/M_SEC=%d, absDeltaX > time * VELOCITY / M_SEC=%b", absDeltaX, time, VELOCITY, time * VELOCITY / M_SEC, (absDeltaX > time * VELOCITY / M_SEC)));
            }

        }
        }
        return false;
    }

}
Lookout answered 18/7, 2012 at 1:0 Comment(2)
can anybody please tell me how to call the class. ActivitySwipeDetector swipe = new ActivitySwipeDetector(this); obviously is giving error, as no such constructor. Should I give ActivitySwipeDetector swipe = new ActivitySwipeDetector(this,null);Cayenne
@AbdullahFahim ActivitySwipeDetector(this, YourActivity.this);Frazee
B
28

This question is kind of old and in July 2011 Google released the Compatibility Package, revision 3) which includes the ViewPager that works with Android 1.6 upwards. The GestureListener answers posted for this question don't feel very elegant on Android. If you're looking for the code used in switching between photos in the Android Gallery or switching views in the new Play Market app then it's definitely ViewPager.

Here's some links for more info:

Burdett answered 23/4, 2012 at 19:54 Comment(2)
One problem with ViewPager is that you have no control of distance and velocity parameters for the fling gesture.Nardone
ViewPager is not used in the gallery.Theologize
F
22

There's a built-in interface that you can use directly for all gestures:
Here is an explanation for a basic level user: enter image description here There is 2 imports be careful in choosing that both are diferent enter image description here enter image description here

Fridge answered 5/4, 2013 at 14:28 Comment(1)
And what are the next steps? How to set that listener to a particular view? And what if this view is a part of a fragment?Daube
M
19

There's some proposition over the web (and this page) to use ViewConfiguration.getScaledTouchSlop() to have a device-scaled value for SWIPE_MIN_DISTANCE.

getScaledTouchSlop() is intended for the "scrolling threshold" distance, not swipe. The scrolling threshold distance has to be smaller than a "swing between page" threshold distance. For example, this function returns 12 pixels on my Samsung GS2, and the examples quoted in this page are around 100 pixels.

With API Level 8 (Android 2.2, Froyo), you've got getScaledPagingTouchSlop(), intended for page swipe. On my device, it returns 24 (pixels). So if you're on API Level < 8, I think "2 * getScaledTouchSlop()" should be the "standard" swipe threshold. But users of my application with small screens told me that it was too few... As on my application, you can scroll vertically, and change page horizontally. With the proposed value, they sometimes change page instead of scrolling.

Mullis answered 9/2, 2012 at 21:33 Comment(0)
C
15

Also as a minor enhancement.

The main reason for the try/catch block is that e1 could be null for the initial movement. in addition to the try/catch, include a test for null and return. similar to the following

if (e1 == null || e2 == null) return false;
try {
...
} catch (Exception e) {}
return false;
Curley answered 12/4, 2011 at 21:14 Comment(0)
A
15

This is a combined answer of the two answers at top, if anyone wants a working implementation.

package com.yourapplication;

import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public abstract class OnSwipeListener implements View.OnTouchListener {

    private final GestureDetector gestureDetector;

    public OnSwipeListener(Context context){
        gestureDetector = new GestureDetector(context, new OnSwipeGestureListener(context));
        gestureDetector.setIsLongpressEnabled(false);
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }

    private final class OnSwipeGestureListener extends GestureDetector.SimpleOnGestureListener {

        private final int minSwipeDelta;
        private final int minSwipeVelocity;
        private final int maxSwipeVelocity;

        private OnSwipeGestureListener(Context context) {
            ViewConfiguration configuration = ViewConfiguration.get(context);
            // We think a swipe scrolls a full page.
            //minSwipeDelta = configuration.getScaledTouchSlop();
            minSwipeDelta = configuration.getScaledPagingTouchSlop();
            minSwipeVelocity = configuration.getScaledMinimumFlingVelocity();
            maxSwipeVelocity = configuration.getScaledMaximumFlingVelocity();
        }

        @Override
        public boolean onDown(MotionEvent event) {
            // Return true because we want system to report subsequent events to us.
            return true;
        }

        // NOTE: see https://mcmap.net/q/45579/-fling-gesture-detection-on-grid-layout
        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX,
                               float velocityY) {

            boolean result = false;
            try {
                float deltaX = event2.getX() - event1.getX();
                float deltaY = event2.getY() - event1.getY();
                float absVelocityX = Math.abs(velocityX);
                float absVelocityY = Math.abs(velocityY);
                float absDeltaX = Math.abs(deltaX);
                float absDeltaY = Math.abs(deltaY);
                if (absDeltaX > absDeltaY) {
                    if (absDeltaX > minSwipeDelta && absVelocityX > minSwipeVelocity
                            && absVelocityX < maxSwipeVelocity) {
                        if (deltaX < 0) {
                            onSwipeLeft();
                        } else {
                            onSwipeRight();
                        }
                    }
                    result = true;
                } else if (absDeltaY > minSwipeDelta && absVelocityY > minSwipeVelocity
                        && absVelocityY < maxSwipeVelocity) {
                    if (deltaY < 0) {
                        onSwipeTop();
                    } else {
                        onSwipeBottom();
                    }
                }
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    public void onSwipeLeft() {}

    public void onSwipeRight() {}

    public void onSwipeTop() {}

    public void onSwipeBottom() {}
}
Avant answered 7/12, 2014 at 7:49 Comment(2)
Thank you for a really good implementation. Additionally I would suggest to check absDeltaY > minSwipeDelta, absVelocityY > minSwipeVelocity, absVelocityY < maxSwipeVelocity only in case if minSwipeDelta != getScaledTouchSlop, minSwipeVelocity != getScaledMinimumFlingVelocity, maxSwipeVelocity != getScaledMaximumFlingVelocity, i.e. to check only if these so-called “default” (I mean getScaledTouchSlop, getScaledMinimumFlingVelocity, getScaledMaximumFlingVelocity) values are scaled or changed according to your own wish.Embellishment
The point is that according to the source code, the mentioned "default" values are already checked by GestureDetector, and OnFling is triggered only if they are confirmed (by the way the triggering takes place only in case of ACTION_UP, not ACTION_MOVE or ACTION_POINTER_UP, i.e. only as a result of the fully realized gesture). (I have not checked other API versions, so comments are appreciated).Embellishment
U
14

There is a lot of excellent information here. Unfortunately a lot of this fling-processing code is scattered around on various sites in various states of completion, even though one would think this is essential to many applications.

I've taken the time to create a fling listener that verifies that the appropriate conditions are met. I've added a page fling listener that adds more checks to ensure that flings meet the threshold for page flings. Both of these listeners allow you to easily restrict flings to the horizontal or vertical axis. You can see how it's used in a view for sliding images. I acknowledge that the people here have done most of the research---I've just put it together into a usable library.

These last few days represent my first stab at coding on Android; expect much more to come.

Ulbricht answered 2/11, 2011 at 23:14 Comment(3)
I want to implement swipe gesture via 2 fingers. Please help me out!Li
Site is not working anymoreSpiegel
Krzysiulele thanks for pointing that out. I've updated the links now that the code is on GitHub. It's a bit old and growing stale at the moment, but at least the links aren't broken anymore.Ulbricht
D
13

You can use my droidQuery library to handle flings, clicks, long clicks, and custom events. The implementation is built on my previous answer below, but droidQuery provides a slick, simple syntax:

//global variables    private boolean isSwiping = false;
private SwipeDetector.Direction swipeDirection = null;
private View v;//must be instantiated before next call.

//swipe-handling code
$.with(v).swipe(new Function() {
    @Override
    public void invoke($ droidQuery, Object... params) {
        if (params[0] == SwipeDetector.Direction.START)
            isSwiping = true;
        else if (params[0] == SwipeDetector.Direction.STOP) {
            if (isSwiping) {                    isSwiping = false;
                if (swipeDirection != null) {
                    switch(swipeDirection) {
                        case DOWN :                                //TODO: Down swipe complete, so do something
                            break;
                        case UP :
                            //TODO: Up swipe complete, so do something
                            break;
                        case LEFT :
                            //TODO: Left swipe complete, so do something
                            break;
                        case RIGHT :
                            //TODO: Right swipe complete, so do something
                            break;
                        default :                                break;
                    }
                }                }
        }
        else {
            swipeDirection = (SwipeDetector.Direction) params[0];
        }
    }
});

Original Answer

This answer uses a combination of components from the other answers here. It consists of the SwipeDetector class, which has an inner interface for listening for events. I also provide a RelativeLayout to show how to override a View's onTouch method to allow both swipe events and other detected events (such as clicks or long clicks).

SwipeDetector

package self.philbrown;

import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

/**
 * Detect Swipes on a per-view basis. Based on original code by Thomas Fankhauser on StackOverflow.com,
 * with adaptations by other authors (see link).
 * @author Phil Brown
 * @see <a href="https://mcmap.net/q/45579/-fling-gesture-detection-on-grid-layout">android-basic-gesture-detection</a>
 */
public class SwipeDetector implements View.OnTouchListener
{
    /**
     * The minimum distance a finger must travel in order to register a swipe event.
     */
    private int minSwipeDistance;

    /** Maintains a reference to the first detected down touch event. */
    private float downX, downY;

    /** Maintains a reference to the first detected up touch event. */
    private float upX, upY;

    /** provides access to size and dimension contants */
    private ViewConfiguration config;

    /**
     * provides callbacks to a listener class for various swipe gestures.
     */
    private SwipeListener listener;

    public SwipeDetector(SwipeListener listener)
    {
        this.listener = listener;
    }


    /**
     * {@inheritDoc}
     */
    public boolean onTouch(View v, MotionEvent event)
    {
        if (config == null)
        {
                config = ViewConfiguration.get(v.getContext());
                minSwipeDistance = config.getScaledTouchSlop();
        }

        switch(event.getAction())
        {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            downY = event.getY();
            return true;
        case MotionEvent.ACTION_UP:
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            // swipe horizontal?
            if(Math.abs(deltaX) > minSwipeDistance)
            {
                // left or right
                if (deltaX < 0)
                {
                        if (listener != null)
                        {
                                listener.onRightSwipe(v);
                                return true;
                        }
                }
                if (deltaX > 0)
                {
                        if (listener != null)
                        {
                                listener.onLeftSwipe(v);
                                return true;
                        }
                }
            }

            // swipe vertical?
            if(Math.abs(deltaY) > minSwipeDistance)
            {
                // top or down
                if (deltaY < 0)
                {
                        if (listener != null)
                        {
                                listener.onDownSwipe(v);
                                return true;
                        }
                }
                if (deltaY > 0)
                {
                        if (listener != null)
                        {
                                listener.onUpSwipe(v);
                                return true;
                        }
                }
            }
        }
        return false;
    }

    /**
     * Provides callbacks to a registered listener for swipe events in {@link SwipeDetector}
     * @author Phil Brown
     */
    public interface SwipeListener
    {
        /** Callback for registering a new swipe motion from the bottom of the view toward its top. */
        public void onUpSwipe(View v);
        /** Callback for registering a new swipe motion from the left of the view toward its right. */
        public void onRightSwipe(View v);
        /** Callback for registering a new swipe motion from the right of the view toward its left. */
        public void onLeftSwipe(View v);
        /** Callback for registering a new swipe motion from the top of the view toward its bottom. */
        public void onDownSwipe(View v);
    }
}

Swipe Interceptor View

package self.philbrown;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

import com.npeinc.module_NPECore.model.SwipeDetector;
import com.npeinc.module_NPECore.model.SwipeDetector.SwipeListener;

/**
 * View subclass used for handling all touches (swipes and others)
 * @author Phil Brown
 */
public class SwipeInterceptorView extends RelativeLayout
{
    private SwipeDetector swiper = null;

    public void setSwipeListener(SwipeListener listener)
    {
        if (swiper == null)
            swiper = new SwipeDetector(listener);
    }

    public SwipeInterceptorView(Context context) {
        super(context);
    }

    public SwipeInterceptorView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SwipeInterceptorView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e)
    {
        boolean swipe = false, touch = false;
        if (swiper != null)
            swipe = swiper.onTouch(this, e);
        touch = super.onTouchEvent(e);
        return swipe || touch;
    }
}
Dunsinane answered 24/1, 2013 at 17:49 Comment(4)
I tried implementing this on a view that contains clickable elements. When a swipe starts over a clickable element (e.g. a list view which has onItemClick listener registered), then onTouchEvent is never invoked. Thus, the user cannot start a swipe over a clickable element, which is unfortunate for me and I am still trying to figure out how to work around this, as our clickable elements take up quite a bit of view space and we still want swipe support for the entire view. If a swipe doesn't start over a clickable element, then it works perfectly.Applejack
@Lo-Tan, this occurs because your clickable item is a child view, and is thus on top of the SwipeInterceptorView, so its click is handled first. You can fix this by implementing your own clicking mechanism by implementing onTouchListener, or as a work-around you can listen for long clicks instead of clicks (see View.setOnLongClickListener).Dunsinane
I am actually trying that at the very moment. Or possible cancelling the click event if they start a drag :) Thanks much.Applejack
One solution is to attach the swipe detector to every view in your app. Another is to implement onInterceptTouchEvent in your SwipeInterceptorView.Jimenez
B
9

I know its too late to answer but Still I am posting Swipe Detection for ListView that How to use Swipe Touch Listener in ListView Item.

Refrence: Exterminator13(one of answer in this page)

Make one ActivitySwipeDetector.class

package com.example.wocketapp;

import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public class ActivitySwipeDetector implements View.OnTouchListener 
{
    static final String logTag = "SwipeDetector";
    private SwipeInterface activity;
    private float downX, downY;
    private long timeDown;
    private final float MIN_DISTANCE;
    private final int VELOCITY;
    private final float MAX_OFF_PATH;

    public ActivitySwipeDetector(Context context, SwipeInterface activity)
    {
        this.activity = activity;
        final ViewConfiguration vc = ViewConfiguration.get(context);
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        MIN_DISTANCE = vc.getScaledPagingTouchSlop() * dm.density;
        VELOCITY = vc.getScaledMinimumFlingVelocity();
        MAX_OFF_PATH = MIN_DISTANCE * 2;
    }

    public void onRightToLeftSwipe(View v) 
    {
        Log.i(logTag, "RightToLeftSwipe!");
        activity.onRightToLeft(v);
    }

    public void onLeftToRightSwipe(View v) 
    {
        Log.i(logTag, "LeftToRightSwipe!");
        activity.onLeftToRight(v);
    }

    public boolean onTouch(View v, MotionEvent event) 
    {
        switch (event.getAction()) 
        {
            case MotionEvent.ACTION_DOWN:
            {
                Log.d("onTouch", "ACTION_DOWN");
                timeDown = System.currentTimeMillis();
                downX = event.getX();
                downY = event.getY();
                v.getParent().requestDisallowInterceptTouchEvent(false);
                return true;
            }

        case MotionEvent.ACTION_MOVE:
            {
                float y_up = event.getY();
                float deltaY = y_up - downY;
                float absDeltaYMove = Math.abs(deltaY);

                if (absDeltaYMove > 60) 
                {
                    v.getParent().requestDisallowInterceptTouchEvent(false);
                } 
                else
                {
                    v.getParent().requestDisallowInterceptTouchEvent(true);
                }
            }

            break;

            case MotionEvent.ACTION_UP: 
            {
                Log.d("onTouch", "ACTION_UP");
                long timeUp = System.currentTimeMillis();
                float upX = event.getX();
                float upY = event.getY();

                float deltaX = downX - upX;
                float absDeltaX = Math.abs(deltaX);
                float deltaY = downY - upY;
                float absDeltaY = Math.abs(deltaY);

                long time = timeUp - timeDown;

                if (absDeltaY > MAX_OFF_PATH) 
                {
                    Log.e(logTag, String.format(
                            "absDeltaY=%.2f, MAX_OFF_PATH=%.2f", absDeltaY,
                            MAX_OFF_PATH));
                    return v.performClick();
                }

                final long M_SEC = 1000;
                if (absDeltaX > MIN_DISTANCE && absDeltaX > time * VELOCITY / M_SEC) 
                {
                     v.getParent().requestDisallowInterceptTouchEvent(true);
                    if (deltaX < 0) 
                    {
                        this.onLeftToRightSwipe(v);
                        return true;
                    }
                    if (deltaX > 0) 
                    {
                        this.onRightToLeftSwipe(v);
                        return true;
                    }
                }
                else 
                {
                    Log.i(logTag,
                            String.format(
                                    "absDeltaX=%.2f, MIN_DISTANCE=%.2f, absDeltaX > MIN_DISTANCE=%b",
                                    absDeltaX, MIN_DISTANCE,
                                    (absDeltaX > MIN_DISTANCE)));
                    Log.i(logTag,
                            String.format(
                                    "absDeltaX=%.2f, time=%d, VELOCITY=%d, time*VELOCITY/M_SEC=%d, absDeltaX > time * VELOCITY / M_SEC=%b",
                                    absDeltaX, time, VELOCITY, time * VELOCITY
                                            / M_SEC, (absDeltaX > time * VELOCITY
                                            / M_SEC)));
                }

                 v.getParent().requestDisallowInterceptTouchEvent(false);

            }
        }
        return false;
    }
    public interface SwipeInterface 
    {

        public void onLeftToRight(View v);

        public void onRightToLeft(View v);
    }

}

Call it from your activity class like this:

yourLayout.setOnTouchListener(new ActivitySwipeDetector(this, your_activity.this));

And Don't forget to implement SwipeInterface which will give you two @override methods:

    @Override
    public void onLeftToRight(View v) 
    {
        Log.e("TAG", "L to R");
    }

    @Override
    public void onRightToLeft(View v) 
    {
        Log.e("TAG", "R to L");
    }
Boser answered 6/8, 2014 at 9:27 Comment(1)
I find that a MAX_OFF_PATH = 5 * vc.getScaledPagingTouchSlop() is more comfortable for a thumb swipe naturally traveling in a slight arc.Hypallage
D
6

Gestures are those subtle motions to trigger interactions between the touch screen and the user. It lasts for the time between the first touch on the screen to the point when the last finger leaves the surface.

Android provides us with a class called GestureDetector using which we can detect common gestures like tapping down and up, swiping vertically and horizontally (fling), long and short press, double taps, etc. and attach listeners to them.

Make our Activity class implement GestureDetector.OnDoubleTapListener (for double tap gesture detection) and GestureDetector.OnGestureListener interfaces and implement all the abstract methods.For more info. you may visit https://developer.android.com/training/gestures/detector.html . Courtesy

For Demo Test.GestureDetectorDemo

Darill answered 23/7, 2015 at 5:50 Comment(0)
B
6

If you dont like to create a separate class or make code complex,
You can just create a GestureDetector variable inside OnTouchListener and make your code more easier

namVyuVar can be any name of the View on which you need to set the listner

namVyuVar.setOnTouchListener(new View.OnTouchListener()
{
    @Override
    public boolean onTouch(View view, MotionEvent MsnEvtPsgVal)
    {
        flingActionVar.onTouchEvent(MsnEvtPsgVal);
        return true;
    }

    GestureDetector flingActionVar = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener()
    {
        private static final int flingActionMinDstVac = 120;
        private static final int flingActionMinSpdVac = 200;

        @Override
        public boolean onFling(MotionEvent fstMsnEvtPsgVal, MotionEvent lstMsnEvtPsgVal, float flingActionXcoSpdPsgVal, float flingActionYcoSpdPsgVal)
        {
            if(fstMsnEvtPsgVal.getX() - lstMsnEvtPsgVal.getX() > flingActionMinDstVac && Math.abs(flingActionXcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Right to Left fling

                return false;
            }
            else if (lstMsnEvtPsgVal.getX() - fstMsnEvtPsgVal.getX() > flingActionMinDstVac && Math.abs(flingActionXcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Left to Right fling

                return false;
            }

            if(fstMsnEvtPsgVal.getY() - lstMsnEvtPsgVal.getY() > flingActionMinDstVac && Math.abs(flingActionYcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Bottom to Top fling

                return false;
            }
            else if (lstMsnEvtPsgVal.getY() - fstMsnEvtPsgVal.getY() > flingActionMinDstVac && Math.abs(flingActionYcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Top to Bottom fling

                return false;
            }
            return false;
        }
    });
});
Barolet answered 30/5, 2017 at 18:51 Comment(0)
N
4

To all: don't forget about case MotionEvent.ACTION_CANCEL:

it calls in 30% swipes without ACTION_UP

and its equal to ACTION_UP in this case

Norby answered 28/12, 2013 at 12:13 Comment(0)
P
4

I nedded a more generic Class , I took Tomas's class and added an Interface that send events to your Activity or Fragment. it will register the listener on the constructor so be sure you implement the interface or an ClassCastException will be thorwn. the interface returns one of the four final int defined in the class and will return the view on which it was activated upon.

import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class SwipeDetector implements View.OnTouchListener{

    static final int MIN_DISTANCE = 100;
    private float downX, downY, upX, upY;
    public final static int RIGHT_TO_LEFT=1;
    public final static int LEFT_TO_RIGHT=2;
    public final static int TOP_TO_BOTTOM=3;
    public final static int BOTTOM_TO_TOP=4;
    private View v;

    private onSwipeEvent swipeEventListener;


    public SwipeDetector(Activity activity,View v){
        try{
            swipeEventListener=(onSwipeEvent)activity;
        }
        catch(ClassCastException e)
        {
            Log.e("ClassCastException",activity.toString()+" must implement SwipeDetector.onSwipeEvent");
        } 
        this.v=v;
    }
    public SwipeDetector(Fragment fragment,View v){
        try{
            swipeEventListener=(onSwipeEvent)fragment;
        }
        catch(ClassCastException e)
        {
            Log.e("ClassCastException",fragment.toString()+" must implement SwipeDetector.onSwipeEvent");
        } 
        this.v=v;
    }


    public void onRightToLeftSwipe(){   
        swipeEventListener.SwipeEventDetected(v,RIGHT_TO_LEFT);
    }

    public void onLeftToRightSwipe(){   
        swipeEventListener.SwipeEventDetected(v,LEFT_TO_RIGHT);
    }

    public void onTopToBottomSwipe(){   
        swipeEventListener.SwipeEventDetected(v,TOP_TO_BOTTOM);
    }

    public void onBottomToTopSwipe(){
        swipeEventListener.SwipeEventDetected(v,BOTTOM_TO_TOP);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            //HORIZONTAL SCROLL
            if(Math.abs(deltaX) > Math.abs(deltaY))
            {
                if(Math.abs(deltaX) > MIN_DISTANCE){
                    // left or right
                    if(deltaX < 0) 
                    {
                        this.onLeftToRightSwipe();
                        return true;
                    }
                    if(deltaX > 0) {
                        this.onRightToLeftSwipe();
                        return true; 
                    }
                }
                else {
                    //not long enough swipe...
                    return false; 
                }
            }
            //VERTICAL SCROLL
            else 
            {
                if(Math.abs(deltaY) > MIN_DISTANCE){
                    // top or down
                    if(deltaY < 0) 
                    { this.onTopToBottomSwipe();
                    return true; 
                    }
                    if(deltaY > 0)
                    { this.onBottomToTopSwipe(); 
                    return true;
                    }
                }
                else {
                    //not long enough swipe...
                    return false;
                }
            }

            return true;
        }
        }
        return false;
    }
    public interface onSwipeEvent
    {
        public void SwipeEventDetected(View v , int SwipeType);
    }

}
Praemunire answered 31/7, 2014 at 5:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.