We have been using a fixed time-step mechanism to manage the animations, but let's see what advantages it gives us to use a scroller class provided by Android to handle the animations, instead of handling all the animations by ourselves.
First, let's create a GestureDetector instance to handle the touch events:
private GestureDetectorCompat gestureDetector = new GestureDetectorCompat(context, new MenuGestureListener());
We are using the GestureDetectorCompat from the support library to guarantee the same behavior on older versions of Android.
As we covered in Chapter 3, Handling Events, by introducing a GestureDetector we can greatly simplify our onTouchEvent(), as all the logic will be handled by the MenuGestureListener callback instead of being on the onTouchEvent():
@Override public boolean onTouchEvent(MotionEvent event) { return gestureDetector.onTouchEvent(event); }
The gestureDetector requires an implementation of an OnGestureListener, but if we only want to implement some methods and not have to worry about the other methods exposed by the interface, we could just extend from GestureDetector.SimpleOnGestureListener and only override the methods we need. The GestureDetector.SimpleOnGestureListener class comes with a dummy empty implementation for all the methods exposed in the OnGestureListener interface.
SimpleOnGestureListener also implements other interfaces to make our lives as software engineers easier, but please refer to the Android documentation for more information.
Let's then create our own internal class, MenuGestureListener, extending from GestureDetector.SimpleOnGestureListener:
class MenuGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { scroller.forceFinished(true); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float
distanceX,
float distanceY) { scroller.computeScrollOffset(); int lastX = scroller.getCurrX(); scroller.forceFinished(true); scroller.startScroll(lastX, 0, -(int) (distanceX + 0.5f), 0); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float
velocityX, float velocityY) { scroller.computeScrollOffset(); int lastX = scroller.getCurrX(); scroller.forceFinished(true); scroller.fling(lastX,
0,
(int) (velocityX/4.f),
0,
-360*100,
360*100,
0,
0); return true; } }
As we have mentioned before, even if it's an OnGestureListener implementation, we have got to return true on the onDown() method. Otherwise, the onScroll() or onFling() methods from our OnGestureListener implementation won't be called whenever there is a scroll or fling event.
Anyway, we still have some work to do on the onDown() method: We have got to stop any running animation so the custom view will feel more reactive to the user.
We have implemented two other methods: onScroll() and onFling(). They're both managing different gestures that directly map to different ways of scrolling. Whenever we are dragging on the screen, the onScroll() method will be called as we will be actually scrolling. On the other hand, when we do a fling gesture; that is, when the user drags and lifts the finger from the screen very quickly, we need to take into consideration other parameters, such as the velocity and friction of the animation. When the gesture finishes, the animation will still run for some time, slowing down until stopping depending on the defined friction. In that case, the onFling() method from our listener will be called with the horizontal and vertical velocity of the fling event, leaving the friction to be handled by us.
In both events, we will be using a scroller class to simplify the calculations. We could do it ourselves but, although implementing the onScroll() logic would be quite straightforward, implementing the onFling() animation properly would require some calculations and complexities that we can take for granted by using a scroller class.
On the onScroll() implementation, we are simply calling the startScroll method of the scroller from the current position and the dragged distance. To get the current position, we have got to call scroller.computeScrollOffset first. If we don't call it, the current value will always be zero. Once we have called this method, we can retrieve the current value of the scroller by using the getCurrX method.
As in our listener we are getting the distance as a floating point and startScroll only accepts integer values, we will round the distanceX value by just adding 0.5 and then converting it in to an integer value.
Similarly, on the onFling() implementation we will be calling the fling method of the scroller. We'll get the current position, as we have described in the onScroll() implementation, and we will adjust the velocity as it was too high from the point of view of animating a rotating cube. We have set the maximum and minimum values to 100 full turns of the cube as, in normal circumstances, we don't want to limit the rotation.
Now, by using a scroller, we can get rid of the animateLogic() method and all associated variables, as we will no longer need them. On both gestures, scroll and fling, the animations will be performed on the background and we can directly query the current animated value directly from the scroller instance.
The only changes we have got to do on the onDraw() method is to call the scroller.computeScrollOffset method to have an updated value and, instead of using the angleFr variable, get the value from the scroller:
Matrix.rotateM(mMVPMatrix, 0, scroller.getCurrX(), 0.f, 1.f, 0.f);