Introduction
In my previous blogpost about touch handling in Android, I only handled touches in simple View
derived controls. But recently, I was working on a custom ListView
and was introduced to touches in ViewGroup
derived controls which is handled somewhat differently.
Also, in the provided source code, I have made the execution of the code configurable so you can experiment with touch and multi-touch on your Android phone, or the emulator if you don’t have a phone.
So, without any further ado:
Background
So, What Is Different
A ViewGroup
derived is a control with, this won’t be a surprise, a group of child controls. As such, when touching a ViewGroup
, it must be decided if the event will be handled by the control itself or if it will be handled by the child controls.
For this, an overridable method onInterceptTouchEvent must be implemented:
@Override
public boolean onInterceptTouchEvent (MotionEvent ev){
}
By choosing the return value, you can make Android send any future events to the child views or to the onTouchEvent
method of your ViewGroup
:
- return
true
: Yes, you want to intercept the touchevent
, meaning any following events will be delivered on your ViewGroup
‘s onTouchEvent
method - return
false
: No, you do not want to intercept the touchevent
, meaning the child views will receive touchevent
s (of course, only if they happen upon their surface)
In the last case, if you return false
, Android will continue to call your method for each new touchevent
somewhere on the surface of any child of your ViewGoup
derived control. So, depending on certain conditions, you can always decide to stop forwarding them by return true
after all.
Please read the first part of the previous sentence again: “continue to call your method for each new touchevent somewhere on the surface of any child of your ViewGoup
derived control”. Thus, if you do NOT touch down on a child, your onInterceptTouchEvent
will only be called once !
On the other hand, once you have returned true
from the method, your onTouchEvent
method will be called for any future events. But for the gesture at hand, you are not able to send any touches to the child views anymore (unless you program this yourself of course). And if any child was receiving touch events, it will receive an event with an action of MotionEvent.ACTION_CANCEL
.
Do Try This At home: The Code
The attached code and the github project are the code from the original blogpost Touch handling in Android extended with new code to demonstrate the above use cases:
TouchVisualizerViewGroupActivity
: The activity to show the new view TouchVisualizerViewGroupView
: A custom ViewGroup
to demonstrate the above concepts TouchVisualizerViewGroupChildView
: A custom View
which serves as a child for TouchVisualizerViewGroupView
to demonstrate the above concepts TouchVisualizerViewGroupConfigActivity
: An Activity
to configure various properties for demonstrating various use cases
ViewGroup Child: TouchVisualizerViewGroupChildView
This class is based upon the original TouchVisualizerSingleTouchGraphicView
class to demonstrate single touches. Below, I will only show what was changed to support demonstrating the new concepts:
@Override
public void onDraw(Canvas canvas) {
paint.setStyle(Paint.Style.STROKE);
if(isCancelled) {
paint.setStyle(Paint.Style.FILL);
}
canvas.drawRect(0, 0, this.getWidth()-1, this.getHeight()-1, paint);
if(downX > 0)
{
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(downX, downY, getScreenSize(touchCircleRadius), paint);
}
if(beginChild1CaptureTime != -1) {
canvas.drawText(String.valueOf(remainderChild1CaptureTime)
+ "?"
+ String.valueOf(stopChild1CaptureTimeOut), 0, 10, paint);
}
}
In the onDraw
method, code was added to draw a border so we can differentiate the various children in their parent ViewGroup
container. At the end, some code was added to show some timer values. This last point will become clear when you read on.
@Override
public boolean onTouchEvent(MotionEvent event) {
if(this.touchListener != null)
{
this.touchListener.onTouchHappened(childId, action, event.getX(), event.getY());
}
remainderChild1CaptureTime =
Math.abs(beginChild1CaptureTime - System.currentTimeMillis());
if((stopChild1CaptureTimeOut != -1)
&& (remainderChild1CaptureTime > stopChild1CaptureTimeOut)) {
stopChild1CaptureTimeOut = -1;
return false;
}
boolean result = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_CANCEL:
isCancelled = true;
downX = event.getX();
downY = event.getY();
result = false;
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
invalidate();
return result;
}
Nothing spectacular here:
- We call a callback with which we can monitor the
MotionEvent
s send to the child control. We will use it in the parent ViewGroup
control to see how the onTouchEvent
gets called once we move off the childs surface. - We calculate the
timeout
value enabling us to specify when to always return false
from the method. - We monitor the extra action
MotionEvent.ACTION_CANCEL
for which, if it happens, we set the background of the control.
Setting the timeout
value is done in the configuration of the parent TouchVisualizerViewGroupView
view: the TouchVisualizerViewGroupConfigActivity
activity.
ViewGroup: TouchVisualizerViewGroupView
This view allows to experiment with the concepts of this article:
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
interceptPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(padding / 2, lastInterceptY, padding / 2, interceptPaint);
canvas.drawCircle(lastInterceptX, padding/2, padding/2, interceptPaint);
if(downX > 0)
{
markerPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(downX, downY, getScreenSize(touchCircleRadius), markerPaint);
}
if(childDownX > 0)
{
Point ulCorner = getChildULCorner(childId, left, top, right, bottom);
interceptPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(ulCorner.x + childDownX,
ulCorner.y + childDownY,
getScreenSize(touchCircleRadius + pressureRingOffset),
interceptPaint);
}
if(beginReturnTrueTimeOut != -1) {
canvas.drawText(String.valueOf(remainderReturnTrueTimeOut)
+ "?" + String.valueOf(startReturnTrueTimeOut), 0, 10, markerPaint);
}
if(beginReturnFalseInOnToucheventTimeOut != -1) {
canvas.drawText(String.valueOf(remainderReturnFalseInOnToucheventTimeOut)
+ "?"
+ String.valueOf(startReturnFalseInOnToucheventTimeOut), 0, 20, markerPaint);
}
}
The dispatchDraw
(yes, a ViewGroup
doesn’t handle drawing the same way as a simple View
neither) draws two red dots in the margin with an X and Y coordinate of the last onInterceptTouchEvent
call (see below). Next, a white circle is drawn at a position set in the onTouchEvent
method. Next, we draw the configurable timeout
values.
@Override
public boolean onInterceptTouchEvent (MotionEvent ev)
{
lastInterceptX = ev.getX();
lastInterceptY = ev.getY();
this.invalidate();
remainderReturnTrueTimeOut =
Math.abs(beginReturnTrueTimeOut - System.currentTimeMillis());
if((startReturnTrueTimeOut != -1)
&& (remainderReturnTrueTimeOut > startReturnTrueTimeOut)) {
startReturnTrueTimeOut = -1;
return true;
}
return interceptTouchEvent;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(callBaseClass)
{
super.onTouchEvent(event);
}
if(!handleOnTouchEvent)
{
return false;
}
remainderReturnFalseInOnToucheventTimeOut =
Math.abs(beginReturnFalseInOnToucheventTimeOut - System.currentTimeMillis());
if((startReturnFalseInOnToucheventTimeOut != -1)
&& (remainderReturnFalseInOnToucheventTimeOut
> startReturnFalseInOnToucheventTimeOut)) {
startReturnFalseInOnToucheventTimeOut = -1;
return false;
}
int action = event.getAction();
boolean result = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
if (returnValueOnActionDown)
{
result = returnValueOnActionDown;
}
break;
case MotionEvent.ACTION_MOVE:
downX = event.getX();
downY = event.getY();
if (returnValueOnActionMove)
{
result = returnValueOnActionMove;
}
break;
case MotionEvent.ACTION_UP:
downX = -1;
downY = -1;
if (returnValueOnActionUp)
{
result = returnValueOnActionUp;
}
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
invalidate();
return result;
}
@Override
public void onTouchHappened(int child, int action, float x, float y) {
childId = child;
childAction = action;
if(action != MotionEvent.ACTION_UP
&& action != MotionEvent.ACTION_CANCEL) {
childDownX = x;
childDownY = y;
}
else
{
childDownX = -1;
childDownY = -1;
}
}
Again, as in the previous blogpost, you have several configurable values allowing you to experiment with the use cases:
The following table maps these variables to the config setting:
Configuration | Variable | What it does |
Return TRUE from onInterceptTouchEvent | interceptTouchEvent | If set, the returnvalue from the onInterceptTouchEvent will always be true . If not set, it will always be false . It allows testing the most basic behaviour of this method. |
Start returning TRUE after seconds. | startReturnTrueTimeOut | If not -1 , the onInterceptTouchEvent will start returning true after the set value. It allows testing what will happen if you switch from returning false to returning true and the coloring of the childs background shows you the MotionEvent.ACTION_CANCEL the child receives (There is a catch here: the timeout is calculated and evaluated inside the onInterceptTouchEvent method, so you must keep moving to have a correct timeout) |
Start returning FALSE in onTouchEvent after milliseconds. | startReturnFalseInOnToucheventTimeOut | If not -1 , the onTouchEvent will start returning false after the set value. I wanted to see if I could start receiving calls of the onInterceptTouch calls again by returning false from the onTouchEvent method, but no such luck. (There is a catch here: the timeout is calculated and evaluated inside the onTouchEvent method, so you must keep moving to have a correct timeout .) |
Stop Child1 capture after milliseconds. | stopChild1CaptureTimeOut | If not -1 , the onTouchEvent of the child will start returning false after the set value. I wanted to see if I could start receiving calls of the onInterceptTouch calls again by returning false from the onTouchEvent method of the child, but no such luck. (There is a catch here: the timeout is calculated and evaluated inside the onTouchEvent method, so you must keep moving to have a correct timeout .) |
There are 2 more indicators in the view which are not configurable:
The red dots are drawn at the last coordinates received in the onInterceptTouch
method. Thus, if they move, you are receiving more such calls. Once they stay fixed, the method is stopped being called.
If you touch inside a child, you will also see a white ring. This ring is drawn by the parent at coordinates received from the child. If it moves, it means the child's onTouchEvent
method is being called. It allows to show that once you touchdown in a child but move outside its bounds, the child still receives touchevents.
Finally, if you touch the TouchVisualizerViewGroupView
, a white circle is drawn at the position you touched the view.
Conclusion
Most of what is written on touch handling on the Android platform is for a regular View
. With this article, I wanted to provide some information on handling touches in a ViewGroup
derived class and give you an app to experiment with the behaviour.
External References
Filed under: Android, CodeProject Tagged: android, codeproject