Android SDK: Using the Scroller object to create a page flip interaction
23Oct'11
This weekend I decided to play with the Android SDK. I decided to implement an app that uses the basic sweep gesture: you can touch the buttons, but you can drag to change the current page smoothly, or use a gesture to flip between views. The one from the Home screen of most Androids phones and iPhones. There are several solutions on the net, but I thought that it will be great if I developed one on my own. And it was ツ
So, this is my solution, the PageFlipper class. I extended the ViewGroup, so each child you add to my class will be a page on the final view. I use the onInterceptTouchEvent method to check if the user wants to drag/flip the screen or just want to click on a button on it. If I understand the user’s intent to change the page, the method returns true, and the motion of the screen is divided between the onTouchEvent and the computeScroll methods.
My solution uses a very simple State Machine. Take a look at it. I will explain the code based on it.
1) Firstly, the user touches the screen. In the onInterceptTouchEvent, I capture the initial coordinates of the touch, and changes to the state ST_WAITING. The VelocityTracker is initialized here to compute the velocity that the user moves his finger on the screen. Note that I return false from here because I want to watch the MotionEvent, but I still want to let the target child receive the event.
if (action == MotionEvent.ACTION_DOWN && getState() == ST_IDLE) { if (mVTracker != null) { mVTracker.recycle(); } mVTracker = VelocityTracker.obtain(); mVTracker.addMovement(event); setState(ST_WATCHING); mLastX = mFirstX = (int) event.getX(); mFirstY = (int) event.getY(); return false; }
2) The user leaves his finger from the screen. Just go back to the ST_IDLE state, and releases the VelocityTracker object.
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { setState(ST_IDLE); mVTracker.recycle(); mVTracker = null; } return false;
3) Now the things are getting interesting. The user’s finger moved slowly across the screen. What we do here is check if he moved long enough on the horizontal axis to assume that the screen should be moved. If it is true, changes to the ST_DRAGGING state. By returning true here, the current and the next MotionEvents will be delivered direct to the onTouchEvent of my class. The ST_IGNORING is just to avoid interaction with the PageFlipper while the user is interacting with, let’s say, a list in one of the pages.
if (action == MotionEvent.ACTION_MOVE && getState() == ST_WATCHING) { int deltaX = Math.abs(mFirstX - (int) event.getX()); int deltaY = Math.abs(mFirstY - (int) event.getY()); if (deltaX > mTouchSlop && deltaY < mTouchSlop) { setState(ST_DRAGGING); return true; } if (deltaX < mTouchSlop && deltaY > mTouchSlop) { setState(ST_IGNORING); return false; } }
4) While the user is moving his finger on the screen, I scroll the view. Doing this way the user can actually drag the views on the screen.
if (getState() == ST_DRAGGING && action == MotionEvent.ACTION_MOVE) { int deltaX = mLastX - (int) event.getX(); scrollBy(deltaX, 0); mLastX = (int) event.getX(); }
5 and 6) When the user stops touching the screen, the state changes to ST_ANIMATING. Here are several things to do. First, I use the VelocityTracker to compute the speed that the user moved his finger. If it is greater than a minimum speed, the animation will scroll the view to the next child on the left or right. If the user moved his finger slowly, but for more than 50% of the screen, it is moved to the left or right too. If not, then move the view to the correct position back again.
if (getState() == ST_DRAGGING && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { setState(ST_ANIMATING); mVTracker.computeCurrentVelocity(1000); float velocity = mVTracker.getXVelocity(); final int width = getWidth(); final int delta = mLastX - mFirstX; final boolean fling = Math.abs(velocity) > mFlingSlop; final boolean moveToNextScreen = Math.abs(delta) > (width / 2); if (fling || moveToNextScreen) { int motion = (delta > 0 ? -1 : 1) * (width - Math.abs(delta)); mScroller.startScroll(getScrollX(), getScrollY(), motion, 0); } else { mScroller.startScroll(getScrollX(), getScrollY(), delta, 0); } invalidate(); mLastX = mFirstX = mFirstY = -1; mVTracker.recycle(); mVTracker = null; }
Note that I call the invalidate() method on the end. This will force the view to redraw itself. But before that, the SDK will call the computeScroll() method, which is the key to animate the scroll of our view. Note that the methods scrollTo() and scrollBy() just move the view at once to the specified position. So, the Scroller object will help us to move just a little bit at a time, to give the feeling of an animation.
7 and 8) Finishing the main code, our computeScroll() will be called after the call to invalidate() that we did before. The computeScrollOffset() method will return true until the scroll has been completed. So we move a little bit again the view, and call invalidate() again. When the scroller is finished, go back to the state ST_IDLE.
public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } else { if (getState() == ST_ANIMATING && mScroller.isFinished()) setState(ST_IDLE); super.computeScroll(); } }
There are some points that I didn’t solved in this code. One is that the view gets lost if you try to sweep to the left of the first view or the right of the last view. Also, if you add a child view that doesn’t receive touchEvents (such as a TextView), the MotionEvents are not working as I would expect. You need to call setClickable(true) in these cases.
Please, download the full source code of the PageFlipper class and an running example. I hope that you can use it for your projects. And, if you use it or find a bug, please leave a comment bellow ツ
Tags: Android SDKJava
What kind of code is it? [closed] : Android Community - For Application Development (December 24th, 2012)
Shlomo (February 21st, 2013)
Leonardo Fischer (February 21st, 2013)