Tap/Touch Anatomy

Most Corona developers understand the concepts behind tap and touch events, but the lines get a little blurry when dealing with more complex scenarios, for example overlapping objects with different types of event listeners. This tutorial explains exactly how Corona handles these events and which events are broadcast to which objects.

Tap vs. Touch

Let's quickly discuss the basics for users who may be new to Corona or mobile development in general. When the user touches the screen of a touch-sensitive device, this action is regarded in one of two ways:

This tutorial won't go into depth about the properties returned from these events — if you need to explore this topic further, please refer to the Tap/Touch/Multitouch guide.

Event Distinction

Looking beyond the basic concept of tap vs. touch, let's explore how Corona handles these events. At the core level, it's important to understand that each type is regarded as distinct. You may think that "a screen touch is just a screen touch," but the difference will become more apparent as we examine more complex examples.

Test Project Setup

For purposes of this tutorial, let's quickly set up a test project consisting of two squares that overlap in the center. We'll use these squares to test different types of listeners and explore how/when the events are processed.

  1. Open the Corona Simulator.

  2. Click New Project from the welcome window or select New Project... from the File menu.

  3. For the project/application name, type TapTouch and ensure that the Blank template option is selected. Leave the other settings at default and click OK (Windows) or Next (Mac). This will create the basic files for the test project in the location (folder) that you specified.

  4. Locate the project folder and open the main.lua file in your chosen text editor. Inside, replace the existing lines with this code:

local backObject = display.newRect( 130, 130, 150, 150 )
backObject:setFillColor( 0.2, 0.4, 0.8 )
backObject.alpha = 0.75
backObject.name = "Back Object"

local frontObject = display.newRect( 190, 190, 150, 150 )
frontObject:setFillColor( 1, 0.2, 0.3 )
frontObject.alpha = 0.75
frontObject.name = "Front Object"

-- Tap listener function
local function tapListener( event )
    local object = event.target
    print( object.name .. " TAP" )
end

-- Touch listener function
local function touchListener( event )
    local object = event.target
    print( object.name .. " TOUCH (" .. event.phase .. ")" )
end

-- Add event listener to back object
backObject:addEventListener( "tap", tapListener )

-- Add event listener to front object
frontObject:addEventListener( "tap", tapListener )

Tap Over Tap

Working with this example, let's explore the most simple case of overlapping objects: one tap object overlapping another tap object. When you click/tap in the center section where the squares overlap and observe the output in the console, you'll notice that (by default) tap events transmit through — or "propagate" through — to underlying objects. In other words, the front (red) square doesn't block the tap event from reaching the back (blue) square, and the console reflects this:

Front Object TAP
Back Object TAP

This is by design, but how do you prevent this from occurring when such behavior is not desired? The solution is to simply return true at the end of the listener function for associated objects, in this case tapListener(). Doing so will prevent tap events on an object from propagating through to tap-able objects behind it.

To test this concept, add the following line to your code:

-- Tap listener function
local function tapListener( event )
    local object = event.target
    print( object.name .. " TAP" )
    return true  -- Prevent propagation to underlying tap objects
end

Now refresh/reload the project and, once again, click/tap in the center section where the squares overlap. By inspecting the console, you'll notice that the tap now only reaches the front square:

Front Object TAP

Touch Over Touch

As expected, the same principle applies to touch events. Let's return true at the end of our touch event listener function (touchListener()) exactly as we did for the tap listener function:

-- Touch listener function
local function touchListener( event )
    local object = event.target
    print( object.name .. " TOUCH (" .. event.phase .. ")" )
    return true  -- Prevent propagation to underlying touch objects
end

In addition, let's change both squares to touch objects instead of tap objects and associate them with the touchListener() function. This can be done on lines 26 and 29 by changing "tap" to "touch" and changing tapListener to touchListener.

-- Add event listener to back object
backObject:addEventListener( "touch", touchListener )

-- Add event listener to front object
frontObject:addEventListener( "touch", touchListener )

Refresh/reload the project and then click/drag on the front (red) square. While doing so, inspect the console and you'll notice output messages that look like this:

Front Object TOUCH (began)
Front Object TOUCH (moved)
Front Object TOUCH (moved)
Front Object TOUCH (moved)
Front Object TOUCH (moved)
...

Tap/Touch Over Touch/Tap

Things get a little more complicated when you have objects with different listener types overlapping each other, but you still need to control the propagation of tap and touch events. For testing purposes, let's adjust the sample project so it becomes a tap object (red square) over a touch object (blue square). Do this by editing line 29, changing "touch" to "tap" and touchListener to tapListener:

-- Add event listener to back object
backObject:addEventListener( "touch", touchListener )

-- Add event listener to front object
frontObject:addEventListener( "tap", tapListener )

Now refresh/reload the project and click/tap in the center section where the squares overlap. In the console, the output messages may look similar to this:

Back Object TOUCH (began)
Back Object TOUCH (ended)
Front Object TAP

Notice that the back square still receives touch events despite the fact that we've added return true to both the tapListener() and touchListener() functions. This is why it's important to understand, as noted earlier, that tap and touch events are actually distinct from Corona's standpoint, despite some similarities from the user's standpoint.

The behavior is similar if we change the sample project to a touch object over a tap object. To test this, edit the code as follows:

  1. On line 26, change "touch" to "tap" and touchListener to tapListener.
  2. On line 29, change "tap" to "touch" and tapListener to touchListener.
-- Add event listener to back object
backObject:addEventListener( "tap", tapListener )

-- Add event listener to front object
frontObject:addEventListener( "touch", touchListener )

Refresh/reload the project and, once again, click/tap in the center section where the squares overlap. In the console, the output messages may look similar to this:

Front Object TOUCH (began)
Front Object TOUCH (ended)
Back Object TAP

Working with Overlaps

The above examples of overlapping objects with different listener types is fairly common in app development, so you'll need an approach to handle these situations. Some examples include:

For both of these cases, and numerous others, there is a tactic to prevent the "wrong type" of event from propagating through to an underlying object with a different listener type, as follows:

  1. In the instance where a front object should behave as a tap object, and objects behind are touch objects, we can add a listener of both types (tap and touch) to the front object, with the "touch" event listener being simply an anonymous function which returns true:
-- Add event listener to back object
backObject:addEventListener( "touch", touchListener )

-- Add two event listeners to front object
frontObject:addEventListener( "tap", tapListener )
frontObject:addEventListener( "touch", function() return true; end )

In addition, we can use the :setFocus() method in the touchListener() function to give focus to the background object. This works nicely because, since we effectively blocked touch propagation from passing through the front object, any touch object behind it will only receive focus if the touch point is outside of the front object's bounds.

-- Touch listener function
local function touchListener( event )
    local object = event.target

    if ( event.phase == "began" ) then
        display.getCurrentStage():setFocus( object )
    elseif ( event.phase == "ended" or event.phase == "cancelled" ) then
        display.getCurrentStage():setFocus( nil )
    end

    print( object.name .. " TOUCH (" .. event.phase .. ")" )
    return true  -- Prevent propagation to underlying touch objects
end
  1. If the front object should behave as a touch object, and objects behind are tap objects, the same concept applies. Just add a listener of both types to the front object and define its "tap" event listener as an anonymous function which returns true:
-- Add event listener to back object
backObject:addEventListener( "tap", tapListener )

-- Add two event listeners to front object
frontObject:addEventListener( "touch", touchListener )
frontObject:addEventListener( "tap", function() return true; end )

Conclusion

Every project will vary slightly in the exact way that it should behave in regards to tap and touch, but understanding these core concepts is essential to the ultimate user experience. Experiment with the different types and phases, and always remember to test, test, and test again!