Click here to Skip to main content
Click here to Skip to main content

Touch handling in Android

, 18 Oct 2012
Rate this:
Please Sign up or sign in to vote.
Touch handling in Android.

Source code

You can find the latest source code over at github: the source at github.

Introduction

There is already a lot of information available about touch and multi-touch on Android. Consequently, I don’t have the illusion to be providing anything completely new here.

So then why did I write this article?

What I want to try is bundle some information dispersed on the net into a single article and also learn myself by explaining the concepts of touch and multi-touch to you. 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

Single touch

Receiving touch events is done in the view by implementing the overridable method onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent event) {
    // Do your stuff here
}

Whenever one or a series of touch events are produced, your method will be called with the parameter event providing details on what exactly has happened. Mind that I have written “or a series of touch events” and not “a single touch event”. Android can buffer some touch events and then call your method providing you with the details of the touch events which have happened. I will give more information about this in the section Historic Events.

So, you created the above method, now how do you know what happened?

The type MotionEvent of the argument to your method has the method getAction giving you the kind of touch-action which happened. The main values explained in this article and concerning touch actions are:

  • ACTION_DOWN: You’ve touched the screen
  • ACTION_MOVE: You moved your finger on the screen
  • ACTION_UP: You removed your finger from the screen
  • ACTION_OUTSIDE: You’ve touched the screen outside the active view (see Touch outside the view)

Thus, in your code you use a case statement to differentiate between the various actions

@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
switch (action) {
    case MotionEvent.ACTION_DOWN:
    // Do your stuff here
        break;
    case MotionEvent.ACTION_MOVE:
    // Do your stuff here
        break;
    case MotionEvent.ACTION_UP:
    // Do your stuff here
        break;
    }

    return true;
}

Mind that the ACTION_OUTSIDE has nothing to do with moving your finger of the screen. In that case you simply get an ACTION_UP event.

The normal sequence of events is of course ACTION_DOWN when you put your finger down, optionally ACTION_MOVE if you move your finger while touching the screen and finally ACTION_UP when you remove your finger from the screen.

However, if you return false from the onTouchEvent override in response to an ACTION_XXX event, you will not be notified (your method will not be called) about any subsequent events. Android asserts that by returning false you did not process the event and thus are not interested in any further events. Thus you get following table:

Return false on notification of Receive notification of
  ACTION_DOWN ACTION_MOVE ACTION_UP
ACTION_DOWN N.A. NO NO
ACTION_MOVE N.A. N.A. NO
ACTION_UP N.A. N.A. N.A.

As an example: say you returned false from the ACTION_DOWN event, then you will not be notified the ACTION_MOVE and ACTION_UP events.

Other data of MotionEvent

The MotionEvent class has some more methods which provide you with additional information about the event. Those currently supported by the sample application are the screen coordinates of the touch event and an indication of the pressure with which you pressed on the screen.

Touch events and click and longclick

The basics of click and long-click are of course also touch events and they are implemented in de View implementation of onTouchEvent. This means that if you don’t call the base class implementation, your implementations of onClick and onLongClick will not get called.

@Override
public boolean onTouchEvent(MotionEvent event) {
    // call the base class implementation so that other touch dependent methods get called
    super.onTouchEvent(event);
    // Do your stuff here
}

Alternatively, if you want to do everything yourself, then don’t call the base class implementation.

Multiple Touch

Multitouch is a little more complex than single touch because with single touch the sequence of events is always the same: down, optionally move and eventually up. With multitouch however, you can get multiple consecutive down or up events and the order of the down events, meaning which finger they represent, is not necessary the same as the order of the move and up events.

You can for example put your forefinger, middle finger and ring finger down, but lift them in the order middle finger, ring finger and forefinger. In order to keep track of “your finger” Android assigns a pointer ID to each event which is constant for the sequence down, move and up.

If your implementation of onTouchEvent is called, Android provides you for each pointer/finger what happened. For multi-touch Android does not use the ACTION_DOWN and ACTION_UP codes but instead the ACTION_POINTER_DOWN and ACTION_POINTER_UP. You also must use the method getActionMasked to get the action. You can get the pointer ID with the following code:

int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
// do your stuff

As such, you will not receive any events for these actions containing the data for multiple pointers. Thus when you touch with two fingers at what you think is the same time, Android will produce two calls and therefore there will always be one pointer which is first.

For the ACTION_MOVE however, you can have a single move event for multiple pointers. To get the correct pointer ID you must iterate through the provided pointers using the following code:

for(int i = 0; i < event.getPointerCount(); i++)
{
    int curPointerId = event.getPointerId(i);
    
    // do your stuff
}

Historic events

For ACTION_MOVE, there is not only a list of the events for each pointer, but also a list of ACTION_MOVE events since the last call of your method. Android caches the events which occurred during subsequent calls of your onTouchEvent method for ACTION_MOVE events. To get at these events you must use the following code:

for(int j = 0; j < event.getHistorySize(); j++)
{
    for(int i = 0; i < event.getPointerCount(); i++)
    {
        int curPointerId = event.getPointerId(i);
        
        // in order to get the historical data of the event
        //    you must use the getHistorical... methods
        int x = event.getHistoricalX(i, j);
        
    }
}

Touch outside the view

To receive the ACTION_OUTSIDE event, you must set two flags for your window:

  • WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL: Indicates that any touches outside of your view will be send to the views behind it.
  • WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH: Indicates that you want to receive the ACTION_OUTSIDE event when touched outside your view.

When these two flags are set on your window, then you will receive the ACTION_OUTSIDE event when a touch happens outside your view.

Do try this at home: the code

The code has four views which allow you to experiment with the various use cases. When you start the application you will see the following screen:

Each entry corresponds with a view allowing you to experiment with that feature. Following is an explanation of what entry corresponds with what view/java file and the configurations possible in that view

Graphics Single: TouchVisualizerSingleTouchGraphicView

@Override
public void onDraw(Canvas canvas) {
    if(downX > 0)
    {
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(downX, downY, touchCircleRadius, paint);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(downX, downY, touchCircleRadius + pressureRingOffset + 
                         (pressureRingOffset * pressure), paint);
    }
}

The onDraw method simply draws two concentric circles at the position where the last event happened. This position is set on the onTouchEvent method shown beneath. The radius of the outer circle is dependent on the pressure with which you touched the screen.

@Override
public boolean onTouchEvent(MotionEvent event) {
    if(callBaseClass)
    {
        super.onTouchEvent(event);
    }
    
    if(!handleOnTouchEvent)
    {
        return false;
    }

    int action = event.getAction();
    pressure = event.getPressure() * pressureAmplification;       

    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 onClick(View v) {
    Toast msg = Toast.makeText(TouchVisualizerSingleTouchGraphicView.this.getContext(), 
                               "onClick", Toast.LENGTH_SHORT);
    msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
    msg.show();        
}

@Override
public boolean onLongClick(View v) {
    Toast msg = Toast.makeText(TouchVisualizerSingleTouchGraphicView.this.getContext(), 
                               "onLongClick", Toast.LENGTH_SHORT);
    msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
    msg.show();        
    return returnValueOnLongClick;
}

As you can see there are a whole bunch of variables which enable you to configure what the behaviour of the activity. The config menu option of the view allow you to configure these variables

The following table maps these variables to the config setting

Configuration Variable What it does
Call base class callBaseClass If set the base class will be called first. It allows to test OnClick and OnLongClick behaviour.
Handle touch events handleOnTouchEvent If set the rest of the method will be executed. It allows to test the behaviour as if you didn’t override, for this set the variable callBaseClass to true.
True on ACTION_DOWN returnValueOnActionDown The returnvalue of the onTouchEvent method when ACTION_DOWN is received. This allows you to see what other actions you receive when setting this to true or false.
True on ACTION_MOVE returnValueOnActionMove The returnvalue of the onTouchEvent method when ACTION_MOVE is received. This allows you to see what other actions you receive when setting this to true or false.
True on ACTION_UP returnValueOnActionUp The returnvalue of the onTouchEvent method when ACTION_UP is received. This allows you to see what other actions you receive when setting this to true or false.
True on onLongClick returnValueOnLongClick The value returned from the onLongClick method
Pressure amplification pressureAmplification The diameter of the circles shown when putting your finger on the screen is influenced by the pressure with which you press on the screen. This variable allows to amplify this influence.

Graphics Multi: TouchVisualizerMultiTouchGraphicView

@Override
public void onDraw(Canvas canvas) {
    for(EventData event : eventDataMap.values())
    {
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(event.x, event.y, touchCircleRadius, paint);
        paint.setStyle(Paint.Style.STROKE);
        if(event.pressure <= 0.001)
        {
            paint.setColor(Color.RED);
        }
        canvas.drawCircle(event.x, event.y, touchCircleRadius + pressureRingOffset + 
                         (pressureRingOffset * event.pressure), paint);
    }
}

The onDraw method iterates through a list which maintains for each pointer (thus finger) what happened last and draws two concentric circles at the position of each event. This list is maintained in the onTouchEvent method shown beneath. Again, the radius of the outer circle is dependent on the pressure with which you touched the screen.

@Override
public boolean onTouchEvent(MotionEvent event) {
    if(callBaseClass)
    {
        super.onTouchEvent(event);
    }
    
    if(!handleOnTouchEvent)
    {
        return false;
    }

    int action = event.getActionMasked();

    int pointerIndex = event.getActionIndex();
    int pointerId = event.getPointerId(pointerIndex);

    boolean result = false;
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_POINTER_DOWN:
        EventData eventData = new EventData();
        eventData.x = event.getX(pointerIndex);
        eventData.y = event.getY(pointerIndex);
        eventData.pressure = event.getPressure(pointerIndex) * pressureAmplification;
        eventDataMap.put(new Integer(pointerId), eventData);
        if (returnValueOnActionDown)
        {
            result = returnValueOnActionDown;
        }
        break;
    case MotionEvent.ACTION_MOVE:
        for(int i = 0; i < event.getPointerCount(); i++)
        {
            int curPointerId = event.getPointerId(i);
            if(eventDataMap.containsKey(new Integer(curPointerId)))
            {
                EventData moveEventData = eventDataMap.get(new Integer(curPointerId));
                moveEventData.x = event.getX(i);
                moveEventData.y = event.getY(i);
                moveEventData.pressure = event.getPressure(i) * pressureAmplification;
            }
        }
        if (returnValueOnActionMove)
        {
            result = returnValueOnActionMove;
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
        eventDataMap.remove(new Integer(pointerId));
        if (returnValueOnActionUp)
        {
            result = returnValueOnActionUp;
        }
        break;
    case MotionEvent.ACTION_OUTSIDE:
        break;
    }
    invalidate();
    return result;
}

@Override
public void onClick(View v) {
    Toast msg = Toast.makeText(TouchVisualizerMultiTouchGraphicView.this.getContext(), 
                               "onClick", Toast.LENGTH_SHORT);
    msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
    msg.show();        
}

@Override
public boolean onLongClick(View v) {
    Toast msg = Toast.makeText(TouchVisualizerMultiTouchGraphicView.this.getContext(), 
                               "onLongClick", Toast.LENGTH_SHORT);
    msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
    msg.show();        
    return handleOnLongClick;
}

The same configuration variables reappear as in the TouchVisualizerSingleTouchGraphicView. You can look in de table there for what they mean.

History Multi: TouchVisualizeMultiTouchHistoricView

@Override
public void onDraw(Canvas canvas) {
    for(List<EventData> path : eventDataMap.values())
    {
        boolean isFirst = true;
        EventData previousEvent = null;
        for(EventData event : path)
        {
            if (isFirst)
            {
                previousEvent = event;
                isFirst = false;
                continue;
            }
            paint.setColor(Color.WHITE);
            if(event.historical)
            {
                paint.setColor(Color.RED);
            }

            canvas.drawLine(previousEvent.x, previousEvent.y, event.x, event.y, paint);
            
            previousEvent = event;
        }
    }
}

The onDraw method again iterates a list of events captured in the onTouchEvent method. However, all events originate from a single touch down, move and up sequence. If it is a historical event, the line is drawn in red, otherwise the line is white.

@Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);

    boolean result = handleOnTouchEvent;
    int action = event.getActionMasked();

    int pointerIndex = event.getActionIndex();
    int pointerId = event.getPointerId(pointerIndex);

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_POINTER_DOWN:
        EventData eventData = new EventData();
        eventData.x = event.getX(pointerIndex);
        eventData.y = event.getY(pointerIndex);
        eventData.pressure = event.getPressure(pointerIndex);
        List<EventData> path = new Vector();
        path.add(eventData);
        eventDataMap.put(new Integer(pointerId), path);
        break;
    case MotionEvent.ACTION_MOVE:
        if(handleHistoricEvent)
        {
            for(int j = 0; j < event.getHistorySize(); j++)
            {
                for(int i = 0; i < event.getPointerCount(); i++)
                {
                    int curPointerId = event.getPointerId(i);
                    if(eventDataMap.containsKey(new Integer(curPointerId)))
                    {
                        List<EventData> curPath = 
                           eventDataMap.get(new Integer(curPointerId));
                        EventData moveEventData = new EventData();
                        moveEventData.x = event.getHistoricalX(i, j);
                        moveEventData.y = event.getHistoricalY(i, j);
                        moveEventData.pressure = event.getHistoricalPressure(i, j);
                        moveEventData.historical = true;

                        curPath.add(moveEventData);
                    }
                }
            }
        }
        for(int i = 0; i < event.getPointerCount(); i++)
        {
            int curPointerId = event.getPointerId(i);
            if(eventDataMap.containsKey(new Integer(curPointerId)))
            {
                List<EventData> curPath = eventDataMap.get(new Integer(curPointerId));
                EventData moveEventData = new EventData();
                moveEventData.x = event.getX(i);
                moveEventData.y = event.getY(i);
                moveEventData.pressure = event.getPressure(i);
                moveEventData.historical = false;
                
                curPath.add(moveEventData);
            }
        }
        
        if(pauseUIThread != 0)
        {
            try {
                Thread.sleep(pauseUIThread);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
        eventDataMap.remove(new Integer(pointerId));
        break;
    case MotionEvent.ACTION_OUTSIDE:
        break;
    }
    invalidate();
    return result;
}

To simplify things a bit here, I removed the configuration variables from the previous views and just left in two variables which allow you to experiment with the historical events.

Configuration Variable What it does
Handle historic events handleHistoricEvent If set, historical events will also be added to the list of events
Pause UI Thread pauseUIThread A value indicating, in milliseconds, how long the UIThread will be paused during processing of onTouchEvent. If you set this longer, more events should be cached as historical events by Android.

Dialog: TouchVisualizerSingleTouchDialog

public TouchVisualizerSingleTouchDialog(Context context) {
    super(context);
    
    registerForOutsideTouch = 
      ((TouchVisualizerSingleTouchDialogActivity)context).getRegisterForOutsideTouch();
    handleActionOutside = 
      ((TouchVisualizerSingleTouchDialogActivity)context).getHandleActionOutside();
            
    if(registerForOutsideTouch) {
        Window window = this.getWindow(); 
        window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, 
                        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
        window.setFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, 
                        LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
    }
    
    this.setContentView(R.layout.custom_dialog);
    this.setTitle("Custom Dialog");          
}

public boolean onTouchEvent(MotionEvent event)   { 
    if (handleActionOutside) {
        if(event.getAction() == MotionEvent.ACTION_OUTSIDE){
            this.dismiss();
        }          
    }
    
    return false;
}

Here also there are configuration variables which allow you to play with this use case.

Configuration Variable What it does
Register outsidetouch registerForOutsideTouch To receive ACTION_OUTSIDE events, you must register for them in the constructor of your view. This variable enables you to do this.
Handle ACTION_OUTSIDE handleActionOutside Of course, you must also handle the ACTION_OUTSIDE event.

Conclusion

A lot has been written already about multi-touch in Android. Although the article does not aim at providing any new information, the application in the accompanying source code gives the user the possibility to experiment with different scenario’s and see how Android response.

External references

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Serge Desmedt
Software Developer (Senior)
Belgium Belgium
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
Questionhmmmmm Pinmemberfreakyit17-Oct-12 6:16 
AnswerRe: hmmmmm PinmemberSerge Desmedt18-Oct-12 21:06 
SuggestionExternal link issue PinmemberSandip.Nascar12-Sep-12 7:46 
GeneralRe: External link issue PinmemberSerge Desmedt12-Sep-12 8:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140814.1 | Last Updated 19 Oct 2012
Article Copyright 2012 by Serge Desmedt
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid