Fling gesture detection on grid layout

412,629

Solution 1

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.

Solution 2

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.

Solution 3

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!

Solution 4

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

Solution 5

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);
Share:
412,629
gav
Author by

gav

British Software Developer living in Williamsburg, Brooklyn

Updated on May 11, 2020

Comments

  • gav
    gav about 4 years

    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.

  • Cdsboy
    Cdsboy over 14 years
    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.
  • Thane Anthem
    Thane Anthem about 13 years
    +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.
  • lomza
    lomza about 13 years
    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?
  • Thomas Ahle
    Thomas Ahle almost 13 years
    I use swipeMinDistance = vc.getScaledPagingTouchSlop() and swipeMaxOffPath = vc.getScaledTouchSlop().
  • Jono
    Jono almost 13 years
    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 view
  • JWL
    JWL almost 13 years
    This actually made it much easier for me to apply gesture functionality, and required "less" wiring :D Thanks @Thomas
  • Someone Somewhere
    Someone Somewhere over 12 years
    This looks like a neat utility class - but I think your four on...swipe() methods should be interfaces
  • Marek Sebera
    Marek Sebera over 12 years
    these returns shouldn't be there (line "we dont consume the event"), isn't it? It disables vertical scrolling feature.
  • IgorGanapolsky
    IgorGanapolsky over 12 years
    Where did you get the number 160.0f?
  • IgorGanapolsky
    IgorGanapolsky over 12 years
    Iomza: did you try putting break statements and stepping through your code?
  • IgorGanapolsky
    IgorGanapolsky over 12 years
    Kudos for using an inner class! Very clean approach.
  • Piotr
    Piotr over 12 years
    Isn't keeping reference to activity causing memory leaks?
  • Admin
    Admin over 12 years
    Yes.Really It works well..nice code to implement for ontouch event in android
  • paiego
    paiego over 12 years
    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)
  • Sandy
    Sandy over 12 years
    I was looking all over for this. NO example of onFling() on the Internet has this, which will lead to poor UX. Thanks!
  • Marek Sebera
    Marek Sebera over 12 years
    I slightly modified it again, see the v.performClick();, which is used to not consume event to OnClickListener, if set on same view
  • Admin
    Admin over 12 years
    What is this onClick function, it is giving me errors.. please help
  • Jon O
    Jon O about 12 years
    @JeffreyBlattman Could you elaborate and/or edit the post to fix them?
  • Jeffrey Blattman
    Jeffrey Blattman about 12 years
    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.
  • Jeffrey Blattman
    Jeffrey Blattman about 12 years
    @Piotr 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.
  • Mikey
    Mikey almost 12 years
    how do you get the value for this SWIPE_MAX_OFF_PATH?
  • almalkawi
    almalkawi almost 12 years
    One problem with ViewPager is that you have no control of distance and velocity parameters for the fling gesture.
  • paiego
    paiego over 11 years
    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)
  • Gaurav Arora
    Gaurav Arora over 11 years
    I want to implement swipe gesture via 2 fingers. Please help me out!
  • Chocolava
    Chocolava over 11 years
    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.
  • Marek Sebera
    Marek Sebera over 11 years
    @Chocolava create new question, comment is not a fine place to ask like this.
  • Duc Tran
    Duc Tran over 11 years
    @MarekSebera this doesn't work with ScrollView & ListView? how to handle them?
  • Marek Sebera
    Marek Sebera over 11 years
    @silentbang again, this is not place to ask such questions. please create new question thread.
  • bad_keypoints
    bad_keypoints over 11 years
    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/59785
  • bad_keypoints
    bad_keypoints over 11 years
    actually this is my full project setup. ge.tt/9P9t7sU/v/0 can you run and see if it's working for you?
  • Tushar
    Tushar over 11 years
    @ThaneAnthem I think you meant DisplayMetrics, not DensityMetrics. :P
  • Lo-Tan
    Lo-Tan over 11 years
    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.
  • Phil
    Phil over 11 years
    @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).
  • Lo-Tan
    Lo-Tan over 11 years
    I am actually trying that at the very moment. Or possible cancelling the click event if they start a drag :) Thanks much.
  • superuser
    superuser almost 11 years
    If i want to have a fragment on top of my activity, would I do fragment.setOnClickListener(SelectFilterActivity.this); fragment.setOnTouchListener(gestureListener);
  • android developer
    android developer over 10 years
    suppose i need to have the functionality of handling touches by myself too (for example for dragging) , what should i do?
  • WonderCsabo
    WonderCsabo over 10 years
    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. :S
  • abdfahim
    abdfahim over 10 years
    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);
  • Stan
    Stan over 10 years
    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?
  • Jibran Khan
    Jibran Khan over 10 years
    Got it working for DoubleTap by following the implementation of SimpleGestureListener.
  • Alessandro Roaro
    Alessandro Roaro over 10 years
    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.
  • linuxjava
    linuxjava over 10 years
    My setOnClickListener did not work. So I had to override onSingleTapConfirmed and it worked as expected.
  • Anton Kashpor
    Anton Kashpor about 10 years
    @AbdullahFahim ActivitySwipeDetector(this, YourActivity.this);
  • Pararth
    Pararth almost 10 years
    this is a classic example with the basic swiping options, thank you for the elementary! dr watson!
  • user280109
    user280109 over 9 years
    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?
  • qix
    qix over 9 years
    I find that a MAX_OFF_PATH = 5 * vc.getScaledPagingTouchSlop() is more comfortable for a thumb swipe naturally traveling in a slight arc.
  • Ralphilius
    Ralphilius about 9 years
    Using this code but it's not working with my child fragment. Would be great if you guys can take a look at it stackoverflow.com/questions/29848754/…
  • Edward Falk
    Edward Falk almost 9 years
    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 stackoverflow.com/questions/31504065/…
  • Edward Falk
    Edward Falk almost 9 years
    Also, see ViewConfiguration which provides a number of standard values, adjusted for pixel density.
  • Edward Falk
    Edward Falk almost 9 years
    One solution is to attach the swipe detector to every view in your app. Another is to implement onInterceptTouchEvent in your SwipeInterceptorView.
  • Erum
    Erum almost 9 years
    @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 ?
  • Elia12345
    Elia12345 almost 9 years
    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.
  • Elia12345
    Elia12345 almost 9 years
    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).
  • Anthony
    Anthony almost 9 years
    ViewPager is not used in the gallery.
  • ERJAN
    ERJAN almost 9 years
    still, nobody explained what this onclick() doing and what is inside it
  • JoxTraex
    JoxTraex over 4 years
    You should probably credit Code Shogun, as I don't see a reference of credit link in your post. Where did this come from?
  • SkorpEN
    SkorpEN about 4 years
    In my case MotionEvent.ACTION_CANCEL same as MotionEvent.ACTION_UP make it work as expected.