How to implement Swipe action in Android
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:
The sample source is attached here.

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
Conceivably you can override any method by extending the class.
But webview is a bit special and involves lots of methods, good luck!
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?
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.
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..)
The launcher.java is indeed complicated. sorry developing android UI is still a pain…
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?
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
Thanks
webview is hard as webview has its own ontouch handling.
i won’t suggest you to override webview and implement your own swipe handling.
@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;
}
});
// …
}
Thanks for sharing! The codes are nice
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.
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.
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
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?
@P
o0ooo gestureDetector is of the type GestureDetector. Not MyGestureDetector. My bad
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!
@Eric
Great tip! It works graciously! Thanks a lot man!
Have a nice day!
here is a working solution that behaves like a homescreen, the source is attached at the end of the artikel
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;
}
}
Thanks for sharing
glad it works for u
can you post your project? cant get it to work….
@Eric Taix
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.