Home > Android, Mobile > How to implement Swipe action in Android

How to implement Swipe action in Android

April 16th, 2009

To implement swipe action in Android, such as the one used in iPhone homescreen unlock, actually Android SDK provides native support for gestures. Swipe is actually called Fling ^_^

You will need to extend SimpleOnGestureListener to implement your own handling on swipe/fling action:

class MyGestureDetector extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
}

To determine if it’s a valid swipe, you will need to make sure the fling falls into an almost straight path, with at least certain length of touch duration, so it’s a continuous touch, and with certain velocity of course.

So let’s define some constants:

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;

The max off path is to make sure the fling still falls within a somewhat straight path.

If onFling() method, you can do this:

if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
viewFlipper.setInAnimation(slideLeftIn);
viewFlipper.setOutAnimation(slideLeftOut);
viewFlipper.showNext();
} else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
viewFlipper.setInAnimation(slideRightIn);
viewFlipper.setOutAnimation(slideRightOut);
viewFlipper.showPrevious();
}

The viewFlipper is used to show how fling/swipe make the view animation/transition from one to the other.

At last, you need to make sure in your activity, you catch the gesture event by overriding onTouch() method:

@Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event))
return true;
else
return false;
}

Here’s a screenshot of the view in transition:

Android Swipe View Flipper in Action

Android Swipe View Flipper in Action

The sample source is attached here.

Android Shogun Android, Mobile , , , ,

  1. Jan
    July 12th, 2009 at 13:31 | #1

    Hello,

    thanks this works perfectly. But is there a way to extend the usual onTouchListener instead of overriding it? I want to extend a webview by longPress and doubleTap but keep the usual behavior of flinging and handling links. Thanks in advance.

    Jan

  2. Android Shogun
    August 8th, 2009 at 18:25 | #2

    Conceivably you can override any method by extending the class.
    But webview is a bit special and involves lots of methods, good luck!

  3. Maarten
    August 17th, 2009 at 06:48 | #3

    This example is very nice and informative, but still it acts diffrent then e.g. the Android homescreen. With the android homescreen the view “sticks” to the fingertouch following small left/right movements instead of just detecting a fling-event and starting the animation. Is it possible to implement such behaviour similar to the homescreen?

  4. August 17th, 2009 at 07:00 | #4

    For swipe action like the home screen, the best place you can look into is the android source code. The basically idea is that when your finger moves, the generated MotionEvent will have respective x and y coordinates, you just need to move your view accordingly. It sounds easy but kind of complicated to implement.

  5. November 14th, 2009 at 15:11 | #5

    Does someone has a working example like the “home screen”? So the view follows your touch gestures? Really want to have something like this in my app but doesnt find how to do. I looked in the android source code (app Launcher, but indeed it’s really complicated..)

  6. Android Shogun
    December 19th, 2009 at 13:36 | #6

    The launcher.java is indeed complicated. sorry developing android UI is still a pain…

  7. Derek
    April 15th, 2010 at 19:25 | #7

    I am trying to implement the gesture listener on top of a table whose rows are clickable. the rows all have a clickListener and a GestureListener applied to them. However, when I swipe across the table I set off both overridden methods (onClick, and onFling). How do I stop the onClick from happening when I fling?

  8. Dai Hoang
    May 13th, 2010 at 01:57 | #8

    hi, It’s a great tut, I’ve try with webview but it replace by onTouch of Webview and didn’t work. Do you have any solution :D Thanks :D

  9. Android Shogun
    May 13th, 2010 at 09:29 | #9

    webview is hard as webview has its own ontouch handling.

    i won’t suggest you to override webview and implement your own swipe handling.

  10. Adam
    May 25th, 2010 at 01:20 | #10

    @Dai Hoang
    I agree this tut was very helpful. I’ve made webview with fling detection. But my WV never has horizontal scroll.

    SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {

    private static final int SWIPE_MIN_DISTANCE = 120;
    private static final int SWIPE_MAX_OFF = 80;
    private static final int SWIPE_THRESHOLD_VELOCITY = 200;

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    if(Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY && Math.abs(e1.getY() – e2.getY()) SWIPE_MIN_DISTANCE) {
    // some work here
    return true;
    } else if (e2.getX() – e1.getX() > SWIPE_MIN_DISTANCE) {
    // some work here
    return true;
    }
    }
    return false;
    }
    };
    GestureDetector gestureDetector;

    @Override
    public void onCreate(Bundle icicle) {
    // …
    wv = (WebView) findViewById(R.id.mainText);
    gestureDetector = new GestureDetector(gestureListener);
    wv.setOnTouchListener(new View.OnTouchListener() {
    public boolean onTouch(View wv, MotionEvent event) {
    if(gestureDetector.onTouchEvent(event)) return true;
    return false;
    }
    });
    // …
    }

  11. Android Shogun
    May 27th, 2010 at 09:52 | #11

    Thanks for sharing! The codes are nice :)

  12. June 1st, 2010 at 00:58 | #12

    To have the gesture detected in a WebView, no need to subclass anything. You just need to add this in your activity:

    @Override
    public boolean dispatchTouchEvent(MotionEvent e){
    super.dispatchTouchEvent(e);
    return mGestureDetector.onTouchEvent(e);
    }

    Where mGestureDetector is initialized as new GestureDetector(this) on your onCreate().

    This will intercept all the gesture events, give opportunity to your listener to do whatever your want with it, and send it back to WebView so behaviour won’t be affected.

  13. vikrant singh
    June 10th, 2010 at 22:40 | #13

    i have to built a newsreader application.
    the pages will be a webview and flinging takes place among pages as in the home screen as mentioned by Maarten.
    is it possible?
    gallery view has that property of gentle swipe, can we have custom gallery view , with webview returned from the setAdapter as its item.

  14. June 19th, 2010 at 02:22 | #14

    Ok i have no idea of what Web View is but this tutorial is nice and i wanna learn just the gestures. To start of i am really new to all this and i have searched many tutorials so please write the very simple code. My app has a table layout with 10 columns and 2 rows. Now rows are not the problem . But my main screen shows only 2 columns and 2 rows and i have 1 image each table entry so the main screen of the activity shows only 4 images now i want the user to scroll horizontally left or right to find out more images. Last but not the least these images are image buttons so i also want to have setonclicklistener for every image…
    Please guys help me

  15. P
    June 25th, 2010 at 13:33 | #15

    I’m sorry am I missing something here..

    when I try and do the lines

    @Override
    public boolean onTouch(View v, MotionEvent event) {
    if (gestureDetector.onTouchEvent(event))
    return true;
    else
    return false;
    }

    I get ‘The method onTouchEvent(MotionEvent) is undefined for the type GestureDetector’ as a compile error?

  16. P
    June 25th, 2010 at 13:40 | #16

    @P
    o0ooo gestureDetector is of the type GestureDetector. Not MyGestureDetector. My bad :-)

  17. Lora
    July 28th, 2010 at 01:22 | #17

    Great tutorial!

    I’m doing this with webView (EbookReader). Hopefully it will work granted that webView is way too special for ViewFlipper.

    Keep up the good work guys. God bless!

  18. Lora
    July 28th, 2010 at 02:44 | #18

    @Eric

    Great tip! It works graciously! Thanks a lot man!
    Have a nice day!

  19. August 11th, 2010 at 23:39 | #19

    here is a working solution that behaves like a homescreen, the source is attached at the end of the artikel

  20. Eric Taix
    August 20th, 2010 at 06:06 | #20

    Here is another way to implement swipe actions (home screen like).
    I started from the android sources of “Workspace.java”, removed unecessary lines of code, fixed issues, et voilà !

    A sample video is available here: http://www.youtube.com/watch?v=w6yi6I6z1rc

    Try and give me your feedback.
    Enjoy !

    ==== WorkspaceTestActivity snippet ======

    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    WorkspaceView work = new WorkspaceView(this, null);
    // Car il y a toujours un petit décalage du doigt même lors d’un scrolling vertical
    work.setTouchSlop(32);
    // Chargement de l’image d fond (peut être enlevée)
    Bitmap backGd = BitmapFactory.decodeResource(getResources(), R.drawable.background_black_1280x1024);
    work.loadWallpaper(backGd);

    ListView lv1 = (ListView) inflater.inflate(R.layout.list, null, false);
    lv1.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, lv_arr));
    ListView lv2 = (ListView) inflater.inflate(R.layout.list, null, false);
    lv2.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, lv_arr2));
    View v1= inflater.inflate(R.layout.relative_layout, null, false);

    ===== WorkspaceView.java =====

    /**
    * Copyright 2010 Eric Taix (eric.taix@gmail.com) Licensed under the Apache License, Version 2.0 (the “License”); you
    * may not use this file except in compliance with the License. You may obtain a copy of the License at
    * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    * either express or implied. See the License for the specific language governing permissions and limitations under the
    * License.
    */
    package org.jared.commons.ui;

    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.os.Parcel;
    import android.os.Parcelable;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewGroup;
    import android.view.ViewParent;
    import android.widget.Scroller;

    /**
    * The workspace is a wide area with a infinite number of screens. Each screen contains a view. A workspace is meant to
    * be used with a fixed width only.
    *
    * This code has been done by using com.android.launcher.Workspace.java
    */
    public class WorkspaceView extends ViewGroup {
    private static final int INVALID_SCREEN = -1;

    // The velocity at which a fling gesture will cause us to snap to the next screen
    private static final int SNAP_VELOCITY = 1000;

    // the default screen index
    private int defaultScreen;
    // The current screen index
    private int currentScreen;
    // The next screen index
    private int nextScreen = INVALID_SCREEN;
    // Wallpaper properties
    private Bitmap wallpaper;
    private Paint paint;
    private int wallpaperWidth;
    private int wallpaperHeight;
    private float wallpaperOffset;
    private boolean wallpaperLoaded;
    private boolean firstWallpaperLayout = true;

    // The scroller which scroll each view
    private Scroller scroller;
    // A tracker which to calculate the velocity of a mouvement
    private VelocityTracker mVelocityTracker;

    // Tha last known values of X and Y
    private float lastMotionX;
    private float lastMotionY;

    private final static int TOUCH_STATE_REST = 0;
    private final static int TOUCH_STATE_SCROLLING = 1;

    // The current touch state
    private int touchState = TOUCH_STATE_REST;
    // The minimal distance of a touch slop
    private int touchSlop;

    // An internal flag to reset long press when user is scrolling
    private boolean allowLongPress;
    // A flag to know if touch event have to be ignored. Used also in internal
    private boolean locked;

    /**
    * Used to inflate the Workspace from XML.
    *
    * @param context The application’s context.
    * @param attrs The attribtues set containing the Workspace’s customization values.
    */
    public WorkspaceView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    /**
    * Used to inflate the Workspace from XML.
    *
    * @param context The application’s context.
    * @param attrs The attribtues set containing the Workspace’s customization values.
    * @param defStyle Unused.
    */
    public WorkspaceView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    defaultScreen = 0;
    initWorkspace();
    }

    /**
    * Initializes various states for this workspace.
    */
    private void initWorkspace() {
    scroller = new Scroller(getContext());
    currentScreen = defaultScreen;

    paint = new Paint();
    paint.setDither(false);
    touchSlop = ViewConfiguration.getTouchSlop();
    }

    /**
    * Set a new distance that a touch can wander before we think the user is scrolling in pixels slop
    *
    * @param touchSlopP
    */
    public void setTouchSlop(int touchSlopP) {
    touchSlop = touchSlopP;
    }

    /**
    * Set the background’s wallpaper.
    */
    public void loadWallpaper(Bitmap bitmap) {
    wallpaper = bitmap;
    wallpaperLoaded = true;
    requestLayout();
    invalidate();
    }

    boolean isDefaultScreenShowing() {
    return currentScreen == defaultScreen;
    }

    /**
    * Returns the index of the currently displayed screen.
    *
    * @return The index of the currently displayed screen.
    */
    int getCurrentScreen() {
    return currentScreen;
    }

    /**
    * Sets the current screen.
    *
    * @param currentScreen
    */
    void setCurrentScreen(int currentScreen) {
    currentScreen = Math.max(0, Math.min(currentScreen, getChildCount()));
    scrollTo(currentScreen * getWidth(), 0);
    invalidate();
    }

    /**
    * Shows the default screen (defined by the firstScreen attribute in XML.)
    */
    void showDefaultScreen() {
    setCurrentScreen(defaultScreen);
    }

    /**
    * Registers the specified listener on each screen contained in this workspace.
    *
    * @param l The listener used to respond to long clicks.
    */
    @Override
    public void setOnLongClickListener(OnLongClickListener l) {
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
    getChildAt(i).setOnLongClickListener(l);
    }
    }

    @Override
    public void computeScroll() {
    if (scroller.computeScrollOffset()) {
    scrollTo(scroller.getCurrX(), scroller.getCurrY());
    }
    else if (nextScreen != INVALID_SCREEN) {
    currentScreen = nextScreen;
    nextScreen = INVALID_SCREEN;
    }
    }

    /**
    * ViewGroup.dispatchDraw() supports many features we don't need: clip to padding, layout animation, animation
    * listener, disappearing children, etc. The following implementation attempts to fast-track the drawing dispatch by
    * drawing only what we know needs to be drawn.
    */
    @Override
    protected void dispatchDraw(Canvas canvas) {
    // First draw the wallpaper if needed
    if (wallpaper != null) {
    float x = getScrollX() * wallpaperOffset;
    if (x + wallpaperWidth = 0 && nextScreen < getChildCount() && Math.abs(currentScreen – nextScreen) == 1) {
    drawChild(canvas, getChildAt(currentScreen), drawingTime);
    drawChild(canvas, getChildAt(nextScreen), drawingTime);
    }
    else {
    // If we are scrolling, draw all of our children
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
    drawChild(canvas, getChildAt(i), drawingTime);
    }
    }
    }
    }

    /**
    * Measure the workspace AND also children
    */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final int width = MeasureSpec.getSize(widthMeasureSpec);
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    if (widthMode != MeasureSpec.EXACTLY) {
    throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
    }

    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    if (heightMode != MeasureSpec.EXACTLY) {
    throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
    }

    // The children are given the same width and height as the workspace
    final int count = getChildCount();
    for (int i = 0; i width ? (count * width – wallpaperWidth) / ((count – 1) * (float) width) : 1.0f;
    if (firstWallpaperLayout) {
    scrollTo(currentScreen * width, 0);
    firstWallpaperLayout = false;
    }
    }

    /**
    * Overrided method to layout child
    */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    int childLeft = 0;
    final int count = getChildCount();
    for (int i = 0; i 0) {
    scrollToScreen(getCurrentScreen() – 1);
    return true;
    }
    }
    else if (direction == View.FOCUS_RIGHT) {
    if (getCurrentScreen() touchSlop;
    boolean yMoved = yDiff > touchSlop;

    if (xMoved || yMoved) {

    if (xMoved && !yMoved) {
    // Scroll if the user moved far enough along the X axis
    touchState = TOUCH_STATE_SCROLLING;
    }
    // Either way, cancel any pending longpress
    if (allowLongPress) {
    allowLongPress = false;
    // Try canceling the long press. It could also have been scheduled
    // by a distant descendant, so use the mAllowLongPress flag to block
    // everything
    final View currentView = getChildAt(currentScreen);
    currentView.cancelLongPress();
    }
    }
    break;

    case MotionEvent.ACTION_DOWN:
    // Remember location of down touch
    lastMotionX = x;
    lastMotionY = y;
    allowLongPress = true;

    /*
    * If being flinged and user touches the screen, initiate drag; otherwise don’t. mScroller.isFinished should be
    * false when being flinged.
    */
    touchState = scroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
    break;

    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
    touchState = TOUCH_STATE_REST;
    break;
    }

    /*
    * The only time we want to intercept motion events is if we are in the drag mode.
    */
    return touchState != TOUCH_STATE_REST;
    }

    /**
    * Track the touch event
    */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
    if (locked) {
    return true;
    }
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    final int action = ev.getAction();
    final float x = ev.getX();

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    /*
    * If being flinged and user touches, stop the fling. isFinished will be false if being flinged.
    */
    if (!scroller.isFinished()) {
    scroller.abortAnimation();
    }

    // Remember where the motion event started
    lastMotionX = x;
    break;
    case MotionEvent.ACTION_MOVE:
    if (touchState == TOUCH_STATE_SCROLLING) {
    // Scroll to follow the motion event
    final int deltaX = (int) (lastMotionX – x);
    lastMotionX = x;

    if (deltaX 0) {
    scrollBy(Math.max(-getScrollX(), deltaX), 0);
    }
    }
    else if (deltaX > 0) {
    final int availableToScroll = getChildAt(getChildCount() – 1).getRight() – getScrollX() – getWidth();
    if (availableToScroll > 0) {
    scrollBy(Math.min(availableToScroll, deltaX), 0);
    }
    }
    }
    break;
    case MotionEvent.ACTION_UP:
    if (touchState == TOUCH_STATE_SCROLLING) {
    final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000);
    int velocityX = (int) velocityTracker.getXVelocity();

    if (velocityX > SNAP_VELOCITY && currentScreen > 0) {
    // Fling hard enough to move left
    scrollToScreen(currentScreen – 1);
    }
    else if (velocityX < -SNAP_VELOCITY && currentScreen 0 && scroller.isFinished()) {
    scrollToScreen(currentScreen – 1);
    }
    }

    /**
    * Scroll to the next right screen
    */
    public void scrollRight() {
    if (nextScreen == INVALID_SCREEN && currentScreen < getChildCount() – 1 && scroller.isFinished()) {
    scrollToScreen(currentScreen + 1);
    }
    }

    /**
    * Return the screen's index where a view has been added to.
    *
    * @param v
    * @return
    */
    public int getScreenForView(View v) {
    int result = -1;
    if (v != null) {
    ViewParent vp = v.getParent();
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
    if (vp == getChildAt(i)) {
    return i;
    }
    }
    }
    return result;
    }

    /**
    * Return a view instance according to the tag parameter or null if the view could not be found
    *
    * @param tag
    * @return
    */
    public View getViewForTag(Object tag) {
    int screenCount = getChildCount();
    for (int screen = 0; screen < screenCount; screen++) {
    View child = getChildAt(screen);
    if (child.getTag() == tag) {
    return child;
    }
    }
    return null;
    }

    /**
    * Unlocks the SlidingDrawer so that touch events are processed.
    *
    * @see #lock()
    */
    public void unlock() {
    locked = false;
    }

    /**
    * Locks the SlidingDrawer so that touch events are ignores.
    *
    * @see #unlock()
    */
    public void lock() {
    locked = true;
    }

    /**
    * @return True is long presses are still allowed for the current touch
    */
    public boolean allowLongPress() {
    return allowLongPress;
    }

    /**
    * Move to the default screen
    */
    public void moveToDefaultScreen() {
    scrollToScreen(defaultScreen);
    getChildAt(defaultScreen).requestFocus();
    }

    // ========================= INNER CLASSES ==============================

    /**
    * A SavedState which save and load the current screen
    */
    public static class SavedState extends BaseSavedState {
    int currentScreen = -1;

    /**
    * Internal constructor
    *
    * @param superState
    */
    SavedState(Parcelable superState) {
    super(superState);
    }

    /**
    * Private constructor
    *
    * @param in
    */
    private SavedState(Parcel in) {
    super(in);
    currentScreen = in.readInt();
    }

    /**
    * Save the current screen
    */
    @Override
    public void writeToParcel(Parcel out, int flags) {
    super.writeToParcel(out, flags);
    out.writeInt(currentScreen);
    }

    /**
    * Return a Parcelable creator
    */
    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
    public SavedState createFromParcel(Parcel in) {
    return new SavedState(in);
    }

    public SavedState[] newArray(int size) {
    return new SavedState[size];
    }
    };
    }

    // ======================== UTILITIES METHODS ==========================

    /**
    * Return a centered Bitmap
    *
    * @param bitmap
    * @param width
    * @param height
    * @param context
    * @return
    */
    static Bitmap centerToFit(Bitmap bitmap, int width, int height, Context context) {
    final int bitmapWidth = bitmap.getWidth();
    final int bitmapHeight = bitmap.getHeight();

    if (bitmapWidth < width || bitmapHeight < height) {
    // Normally should get the window_background color of the context
    int color = Integer.valueOf("FF191919", 16);
    Bitmap centered = Bitmap.createBitmap(bitmapWidth < width ? width : bitmapWidth, bitmapHeight < height ? height
    : bitmapHeight, Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(centered);
    canvas.drawColor(color);
    canvas.drawBitmap(bitmap, (width – bitmapWidth) / 2.0f, (height – bitmapHeight) / 2.0f, null);
    bitmap = centered;
    }
    return bitmap;
    }

    }

  21. Android Shogun
    August 20th, 2010 at 14:55 | #21

    Thanks for sharing :)

  22. Android Shogun
    August 20th, 2010 at 14:56 | #22

    glad it works for u ;)

  23. Alioooop
    September 3rd, 2010 at 08:26 | #23

    can you post your project? cant get it to work….
    @Eric Taix

  24. Eric Taix
    October 6th, 2010 at 23:18 | #24

    Hi everybody,

    Many users asked my to share the project so I created a project at Google code
    http://code.google.com/p/andro-views/

    Still have issues ! This project is opensource under Apache licence. It is provided as it for free. If you fixed an issue, please send me your corrections.

  1. No trackbacks yet.