Reacting to touch events

In order to make our custom view interactive, one of the first things we will implement is to process and react to touch events, or basically, when the user touches or drags on top of our custom view.

Android provides us with the onTouchEvent() method that we can override in our custom view. By overriding this method, we'll get any touch event happening on top of it. To see how it works, let's add it to the custom view we built in the last chapter:

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    Log.d(TAG, "touch: " + event); 
    return super.onTouchEvent(event); 
} 

Lets also add a log call to see the events we receive. If we run this code and touch on top of our view, we'll get the following:

D/com.packt.rrafols.customview.CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=644.3645, y[0]=596.55804, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=30656461, downTime=30656461, deviceId=9, source=0x1002 }

As we can see, there is a lot of information on the event, coordinates, action type, and time, but even if we perform more actions on it, we'll only get ACTION_DOWN events. That's because the default implementation of view is not clickable. By default, if we don't enable the clickable flag on the view, the default implementation of onTouchEvent() will return false and ignore further events.

The onTouchEvent() method has to return true if the event has been processed or false if it hasn't. If we receive an event in our custom view and we don't know what to do or we're not interested in such events, we should return false, so it can be processed by our view's parent or by any other component or the system.

To receive more types of events, we can do two things:

Later on, we'll implement more complex events; we'll go for the second option.

Lets carry out a quick test and change the method to return simply true instead of calling the parent method:

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    Log.d(TAG, "touch: " + event); 
    return true; 
} 

Now, we should receive many other types of events, as follows:

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_UP,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_UP,
...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,

As seen in the preceding example, we can see that in the previous log we not only have both ACTION_DOWN and ACTION_UP but also ACTION_MOVE to indicate that we're performing an action of drag on top of our view.

We'll focus on handling the ACTION_UP and ACTION_DOWN events first. Let's add a boolean variable name that will keep track whether we're currently pressing or touching our view or not:

private boolean pressed; 
 
public CircularActivityIndicator(Context context, AttributeSet attributeSet) { 
    ... 
    ... 
    pressed = false; 
} 

We've added the variable and set its default state to false, as the view will not be pressed when created. Now, lets add the code to handle this on our onTouchEvent() implementation:

 

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    Log.d(TAG, "touch: " + event); 
    switch(event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
            pressed = true; 
            return true; 
 
        case MotionEvent.ACTION_UP: 
            pressed = false; 
            return true; 
 
        default: 
            return false; 
    } 
} 

We're processed the MotionEvent. The ACTION_DOWN and MotionEvent.ACTION_UP events; any other action we receive here, we ignore and return false, since we haven't handled it.

OK, now we've a variable that keeps track if we're pressing our view or not, but we should do something else or otherwise this won't be of that much use. Let's modify the onDraw() method to paint the circle in a different color when the view is pressed:

 

private static final int DEFAULT_FG_COLOR = 0xffff0000; 
private static final int PRESSED_FG_COLOR = 0xff0000ff; 
     
@Override 
protected void onDraw(Canvas canvas) { 
    if (pressed) { 
        foregroundPaint.setColor(PRESSED_FG_COLOR); 
    } else { 
        foregroundPaint.setColor(DEFAULT_FG_COLOR); 
    } 

If we run this example and we touch our view, we'll see that nothing happens! What is the issue? We're not triggering any repaint or redraw event and the view it's not drawn again. We can see this code is working if we manage to keep pressing the view and put the app in the background and return it to the foreground, for example. However, to do it properly, we should trigger a repaint event when we change something that requires our view to be redrawn, as follows:

 

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    Log.d(TAG, "touch: " + event); 
    switch(event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
            pressed = true; 
            invalidate(); 
            return true; 
 
        case MotionEvent.ACTION_UP: 
            pressed = false; 
            invalidate(); 
            return true; 
 
        default: 
            pressed = false; 
            invalidate(); 
            return false; 
    } 
} 

OK, that should do the trick! Calling the invalidate method will trigger an onDraw() method call in the future:
https://developer.android.com/reference/android/view/View.html#invalidate().

We can now refactor this code and move it into a method:

private void changePressedState(boolean pressed) { 
    this.pressed = pressed; 
    invalidate(); 
} 
 
@Override 
public boolean onTouchEvent(MotionEvent event) { 
    Log.d(TAG, "touch: " + event); 
    switch(event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
            changePressedState(true); 
            return true; 
 
        case MotionEvent.ACTION_UP: 
            changePressedState(false); 
            return true; 
 
        default: 
            changePressedState(false); 
            return false; 
    } 
} 

We need to be aware that invalidate has to be called from the UI thread and will throw an exception if called from another thread. If we've to call it from another thread, for example, we've to update a view after receiving some data from a web service, we've to call postInvalidate().

Here is the result: