Detecting gestures

Let's change the code we've been building to use GestureDetector. We'll also use a Scroller implementation to scroll smoothly between values. We can modify the constructor to create the Scroller object and the GestureDetector with an implementation of a GestureDetector.OnGestureListener:

private GestureDetector gestureListener; 
private Scroller angleScroller; 
 
public CircularActivityIndicator(Context context, AttributeSet attributeSet) { 
    super(context, attributeSet); 
 
    ... 
     
    selectedAngle = 280; 
    pressed = false; 
 
    angleScroller = new Scroller(context, null, true); 
    angleScroller.setFinalX(selectedAngle); 
 
    gestureListener = new GestureDetector(context, new
GestureDetector.OnGestureListener() { boolean processed; @Override public boolean onDown(MotionEvent event) { processed = computeAndSetAngle(event.getX(), event.getY()); if (processed) { getParent().requestDisallowInterceptTouchEvent(true); changePressedState(true); postInvalidate(); } return processed; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { endGesture(); return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float
distanceX, float distanceY) { computeAndSetAngle(e2.getX(), e2.getY()); postInvalidate(); return true; } @Override public void onLongPress(MotionEvent e) { endGesture(); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float
velocityX, float velocityY) { return false; } }); }

There are many callbacks in this interface, but first, in order to process the gestures, we need to return true on the onDown() callback; otherwise, we're indicating that we will not process the chain of events further.

We've simplified onTouchEvent() now, as it just simply forwards the event to the gestureListener:

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    return gestureListener.onTouchEvent(event); 
} 

As we may have different gestures, long press, flings, scrolls, we created a method to end the gesture and restore the status:

private void endGesture() { 
    getParent().requestDisallowInterceptTouchEvent(false); 
    changePressedState(false); 
    postInvalidate(); 
} 

We've modified the computeAndSetAngle() method to use Scroller:

private boolean computeAndSetAngle(float x, float y) { 
    x -= getWidth() / 2; 
    y -= getHeight() / 2; 
 
    double radius = Math.sqrt(x * x + y * y); 
    if(radius > circleSize/2) return false; 
 
    int angle = (int) (180.0 * Math.atan2(y, x) / Math.PI) + 90; 
    angle = ((angle > 0) ? angle : 360 + angle); 
 
    if(angleScroller.computeScrollOffset()) { 
        angleScroller.forceFinished(true); 
    } 
 
    angleScroller.startScroll(angleScroller.getCurrX(), 0, angle -
angleScroller.getCurrX(), 0); return true; }

The Scroller instance will be animating the values; we need to keep checking the updated values to perform the animation. One approach to do so will be to check on the onDraw() method if the animation is finished and trigger an invalidate in order to redraw the view if it isn't:

@Override 
protected void onDraw(Canvas canvas) { 
    boolean notFinished = angleScroller.computeScrollOffset(); 
    selectedAngle = angleScroller.getCurrX(); 
 
    ... 
     
    if (notFinished) invalidate(); 
} 

The computeScrollOffset() will return true if the Scroller hasn't reached the end; also after calling it, we can query the value of the scroll using the getCurrX() method. In this example, we're animating the value of the circle angle, but we're using the X coordinate of the Scroller to animate it.

Using this GestureDetector, we can also detect long presses and flings, for example. As flings involve more animations, we'll cover it in the following chapters of this book.

For more information about how to make views interactive refer to:
https://developer.android.com/training/custom-views/making-interactive.html.

The source code of this example can be found in the Example11-Events folder, in the GitHub repository.