Android VelocityTracker example VelocityDecelerator in SurfaceView with Canvas

If you are trying to make your own fling/swipe/scroll implementation in a Canvas driven SurfaceView, you will notice that there is a key feature missing – mechanism for smooth velocity deceleration. In this article we will give an android VelocityTracker example using a custom VelocityDecelerator class.

You may find that android includes VelocityTracker, which is a helper class for tracking velocity of touch events. This class is pretty helpful for determining the fling velocity produced by series of touch events. However, android lacks any mechanism for handling the velocity measured by VelocityTracker. The idea behind this is pretty simple, just implement the physics law of inertial movement or equally decelerating movement. The law says that:

S = V0*t - (a*t^2)/2

and also:

Vd = a*t

where:
- “S” is the overall movement distance
- “V0″ is the initial velocity (like the one provided by VelocityTracker)
- “t” is the overall movement time
- “a” is the acceleration, which in our case is less than zero, and that turns it into deceleration (in the law of inertia the deceleration is in fact the friction between the moving object and the supporting surface)
- “Vd” is the delta velocity, which means the velocity in a specified moment.

So basically, we need to start moving with “V0″ speed for “t” seconds until “Vd” becomes 0. Applying this on both X and Y axis we can achieve a smooth fling effect in a custom SurfaceView.

To do so, we are going to use this class implementation:

package eu.iostream.android.util;

public class VelocityDecelerator {
	
	public static final float FRICTIONAL_DECELERATION = 0.01f; //px/ms^2
	
	private int mDirectionX = 1;
	private int mDirectionY = 1;
	
	private float mVelocityX = 0;
	private float mVelocityY = 0;
	
	private float mPreviousVelocityX = 0;
	private float mPreviousVelocityY = 0;
	
	private long mStartTimeX = 0;
	private long mStartTimeY = 0;
	
	private long mPreviousTimeX = 0;
	private long mPreviousTimeY = 0;
	
	private long mCurrentTimeX = 0;
	private long mCurrentTimeY = 0;
	
	private float mTotalTimeX = 0;
	private float mTotalTimeY = 0;
	
	private long mCurrentTime;
	
	private float mDeltaTimeX;
	private float mDeltaTimeY;
	
	private float mTempDistance;
	
	public VelocityDecelerator(float velocityX, float velocityY) {
		start(velocityX, velocityY);
	}
	
	public void stop() {
		mVelocityX = mPreviousVelocityX = 0;
		mVelocityY = mPreviousVelocityY = 0;
		
		mTotalTimeX = 0;
		mTotalTimeY = 0;
		
		mStartTimeX = 0;
		mStartTimeY = 0;
	}
	
	public void start(float velocityX, float velocityY) {
		mDirectionX = (velocityX > 0 ? 1 : -1);
		mDirectionY = (velocityY > 0 ? 1 : -1);
		
		mVelocityX = mPreviousVelocityX = Math.abs(velocityX);
		mVelocityY = mPreviousVelocityY = Math.abs(velocityY);
		
		mCurrentTime = System.currentTimeMillis();
		
		mStartTimeX = mCurrentTime;
		mCurrentTimeX = mCurrentTime;
		mPreviousTimeX = mCurrentTime;
		mStartTimeY = mCurrentTime;
		mCurrentTimeY = mCurrentTime;
		mPreviousTimeY= mCurrentTime;
		
		mTotalTimeX = Math.abs((velocityX/FRICTIONAL_DECELERATION)); //ms
		mTotalTimeY = Math.abs((velocityY/FRICTIONAL_DECELERATION)); //ms
	}
	
	public int getDirectionX() {
		return mDirectionX;
	}
	
	public int getDirectionY() {
		return mDirectionY;
	}
	
	///
	
	public boolean isMoving() {
		return (getSpeedX() > 0 || getSpeedY() > 0);
	}
	
	///
	
	private void updateTimeX() {
		mCurrentTimeX = System.currentTimeMillis();
	}
	
	private void updateTimeY() {
		mCurrentTimeY = System.currentTimeMillis();
	}
	
	///
	
	public void calculateFreezeFrameData() {
		mPreviousTimeX = mCurrentTimeX;
		mPreviousTimeY = mCurrentTimeY;
		
		mPreviousVelocityX = getSpeedX();
		mPreviousVelocityY = getSpeedY();
		
		updateTimeX();
		updateTimeY();
	}
	
	///
	
	private float getSpeedX() {
		
		mDeltaTimeX = (mCurrentTimeX-mStartTimeX);
		
		if (mDeltaTimeX >= mTotalTimeX) return 0;
		
		return mVelocityX - FRICTIONAL_DECELERATION*mDeltaTimeX;
	}
	
	private float getSpeedY() {
		
		mDeltaTimeY = (mCurrentTimeY-mStartTimeY);
		
		if (mDeltaTimeY >= mTotalTimeY) return 0;
		
		return mVelocityY - FRICTIONAL_DECELERATION*mDeltaTimeY;
	}
	
	///
	
	public float getDeltaDistanceX() {
		
		mDeltaTimeX = (mCurrentTimeX - mPreviousTimeX);
		
		mTempDistance = mPreviousVelocityX*mDeltaTimeX - (FRICTIONAL_DECELERATION*mDeltaTimeX*mDeltaTimeX)/2;
		
		return (mTempDistance < 0 ? 0 : mTempDistance);
	}
	
	public float getDeltaDistanceY() {
		mDeltaTimeY = (mCurrentTimeY - mPreviousTimeY);
		
		mTempDistance = mPreviousVelocityY*mDeltaTimeY - (FRICTIONAL_DECELERATION*mDeltaTimeY*mDeltaTimeY)/2;
		
		return (mTempDistance < 0 ? 0 : mTempDistance);
	}
	
	///
	
	public float getCurrentDistanceX() {
		mDeltaTimeX = (mCurrentTimeX - mStartTimeX);
		
		mTempDistance = mVelocityX*mDeltaTimeX - (FRICTIONAL_DECELERATION*mDeltaTimeX*mDeltaTimeX)/2;
		
		return (mTempDistance < 0 ? 0 : mTempDistance);
	}
	
	public float getCurrentDistanceY() {
		mDeltaTimeY = (mCurrentTimeY - mStartTimeY);
		
		mTempDistance = mVelocityY*mDeltaTimeY - (FRICTIONAL_DECELERATION*mDeltaTimeY*mDeltaTimeY)/2;
		
		return (mTempDistance < 0 ? 0 : mTempDistance);
	}
	
	///
	
	public float getTotalDistanceX() {
		return mVelocityX*mTotalTimeX - (FRICTIONAL_DECELERATION*mTotalTimeX*mTotalTimeX)/2;
	}
	
	public float getTotalDistanceY() {
		return mVelocityY*mTotalTimeY - (FRICTIONAL_DECELERATION*mTotalTimeY*mTotalTimeY)/2;
	}
}

Now for example usage, lets assume that we have a class CustomSurface that extends SurfaceView. In our class CustomSurface we need to:
- declare both VelocityTracker and VelocityDecelerator

protected VelocityTracker mVelocity;
	
protected VelocityDecelerator mVelocityDecelerator;

- override the onTouchEvent method of SurfaceView, to capture all touch events, feed them to VelocityTracker and then initialize VelocityDecelerator with the measured velocity

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (event != null)
		{		
			if (!mScaling && event.getPointerCount() == 1) {
				
				if (event.getAction() == MotionEvent.ACTION_DOWN) {
					
					if (mVelocityDecelerator != null) mVelocityDecelerator.stop();
					
					mVelocity = VelocityTracker.obtain();
					
					mVelocity.addMovement(event);
				}
				
				if (event.getAction() == MotionEvent.ACTION_MOVE) {
					
					mVelocity.addMovement(event);
				}
				
				if (event.getAction() == MotionEvent.ACTION_UP) {
					
					mVelocity.addMovement(event);
					
					mVelocity.computeCurrentVelocity(1);
					
					if (mVelocityDecelerator == null) {
						mVelocityDecelerator = new VelocityDecelerator(mVelocity.getXVelocity(), mVelocity.getYVelocity());
					}
					else {
						mVelocityDecelerator.start(mVelocity.getXVelocity(), mVelocity.getYVelocity());
					}
					
					mVelocity.recycle();
				}

			}
			
			return true;
		}
		
		return super.onTouchEvent(event);
	}

- use VelocityDecelerator in your drawing thread

    public class AnimationThread extends Thread {
        protected boolean mRun;       
        protected SurfaceHolder mSurfaceHolder;        

        public AnimationThread(SurfaceHolder surfaceHolder) {
            mSurfaceHolder = surfaceHolder;
        }

        @Override
        public void run() {
            while (mRun) {
            	mCanvas = null;

                try {
                    mCanvas = mSurfaceHolder.lockCanvas();

                    synchronized (mSurfaceHolder) {  
                    	
                    	updatePosition();
                    	
                        doDraw(mCanvas);
                                       
                        sleep(mThreadDelay);
                    }
                } catch (Exception e) {                 
                    e.printStackTrace();
                    
                    setRunning(false);
                } finally {
                    if (mCanvas != null) {
                        mSurfaceHolder.unlockCanvasAndPost(mCanvas);
                    }
                }
            }
        }
        
        protected void doDraw(Canvas canvas) {
            
            if (!mLayers.isEmpty()) {
            	for (Renderable renderer: mLayers) {
            		renderer.render(canvas);
            	}
            }
            
            canvas.restore();
        }
        
        public void setRunning(boolean b) {
            mRun = b;
        }

        public void updatePosition() {
	        if (mVelocityDecelerator != null && mVelocityDecelerator.isMoving()) {
    		
                    mVelocityDecelerator.calculateFreezeFrameData();
    		
                    mDeltaX = Math.round(mVelocityDecelerator.getDeltaDistanceX())*mVelocityDecelerator.getDirectionX();
                    mDeltaY = Math.round(mVelocityDecelerator.getDeltaDistanceY())*mVelocityDecelerator.getDirectionY();
    		
                    //TODO use mDeltaX and mDeltaY to move your drawing objects around
            }
        }
    }

And that’s all. Good luck!

Leave a Reply

Your email address will not be published. Required fields are marked *


8 − four =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>