Thursday, 12 April 2012

Detecting Touch with Corona


One of the most distinguishing features of today’s mobile devices, especially the Android and iOS devices that are supported by Corona SDK, is the presence of a touch screen. So naturally, as a developer it makes sense for you to know exactly how to detect user touches in your Corona-made apps, so you can have your app to react accordingly.
Without being able to detect the most basic form of user-input on a touch-screen device, a touch, then your app wouldn’t be much of an app at all. But there’s a lot more to touches than you might think, so this tutorial will not only show you how to detect touches, but will also give you some insight on the various aspects of a user touch so you can design your app exactly as you planned it.
Things like touch phases, focus, and dragging should all be familiar concepts by the time you’re finished with this tutorial. This tutorial will only cover basic, single touches. Multi-touch is a more advanced topic and will be saved for another day—it’s important you understand “regular” touch events first.
If you’re not familiar with Corona events, I recommend you read The Corona Event Model Explained first, so you can get an understanding of how events and listeners work. That article also covers the basics of touch events, so when you’re finished reading about the event model you can come back to this tutorial to get a more complete understanding.

Detecting User Touches

User touches can be detected in two different ways. They can be detected on a per-object basis, or detected on the entire screen (also known as “Runtime” or “global” touches). It’s important to know the differences so I’ll go over both of them.

Per-object touches

First thing’s first. Before any particular event can be detected, you must add an event listener. There are two ways to do this, with a table listener or a function listener. Below are examples of both.
For all intents and purposes, ‘Table Listener A’ and ‘Table Listener B’ are identical, with some minor differences. Take a moment to study the differences—you can gain some valuable Lua/Corona knowledge (that’s best explained through examples and practice).
Table Listener A
local obj = display.newImage( "image.png" )

-- touch listener
function obj:touch( event )
    -- 'self' parameter exists via the ':' in function definition
end

-- begin detecting touches
obj:addEventListener( "touch", obj )
Table Listener B
local obj = display.newImage( "image.png" )

-- touch listener
local function onObjectTouch( self, event )
    -- 'self' parameter must be defined via function argument
end

-- begin detecting touches
obj.touch = onObjectTouch
obj:addEventListener( "touch", obj )
Function Listener
local obj = display.newImage( "image.png" )

-- touch listener
local function onObjectTouch( event )
    -- no 'self' parameter exists in this scenario
end

-- begin detecting touches
obj:addEventListener( "touch", onObjectTouch )
I’ll cover what goes IN the listener functions in a moment, but for now, just pay attention to how the listener is added to the object. What all of the above examples do is simply tell Corona to call the listener function whenever a “touch” event is dispatched. Take the third code snippet for example. Here’s what’s going on:
When the user touches ‘obj’, call the onObjectTouch() function.
NOTE: It is important to know that touch events are dispatched several times for a single touch, in separate phases, which I’ll cover in a moment.

Runtime Touches

You can also add a touch listener to the global Corona “Runtime” object, which will detect touches on the entire screen, and aren’t bound to any one object. Runtime touches only support function listeners. Here’s an example:
local function onScreenTouch( event )
    -- no 'self' parameter
end

-- begin listening for screen touches
Runtime:addEventListener( "touch", onScreenTouch )

How to STOP detecting touches

If you want to stop listening to touch events on a specific object (including the global Runtime object), you simply call removeEventListener() in the same exact manner that you called addEventListener().
-- example 1:
obj:removeEventListener( "touch", obj )

-- example 2:
obj:removeEventListener( "touch", onObjectTouch )

-- example3 :
Runtime:removeEventListener( "touch", onScreenTouch )
VERY IMPORTANT: Runtime event listeners are never automatically removed. If you add a touch listener (or any kind of listener) to the global Runtime object, it is your sole responsibility to remove the event listeners when you’re finished with them to prevent bugs and crashes from occurring.
Event listeners that are added to individual objects are automatically removed when the object is removed; however, it is still recommended you remove them when you’re finished as well (just to ensure you don’t accidentally “double up” event listeners on an object).

Touch Event Phases

Many Corona beginners run into problems when working with touch listeners, because naturally, you’d expect your listener function to get called once per user touch. However, that is not the case. Touch events have several “phases” which are listed below:
  • began – dispatched when the user first touches the object.
  • moved – dispatched whenever user moves their finger, as they are touching.
  • ended & cancelled – dispatched whenever user lifts their finger or touch is cancelled for whatever reason.
Whenever a touch phase is dispatched, the touch event listener function that you specified when adding the listener will be called. This usually results in a minimum of two (but usually three) calls to your function for every user touch!
You will most-likely need to do different things (or do nothing) depending on which phase of the touch is currently in, so you’ll want to test for that via the event.phaseproperty of your touch listener. Here’s an example:
local function onObjectTouch( self, event )
    if event.phase == "began" then

        -- Touch has began

    elseif event.phase == "moved" then

        -- User has moved their finger, while touching

    elseif event.phase == "ended" or event.phase == "cancelled" then

        -- Touch has ended; user has lifted their finger
    end

    return true    -- IMPORTANT
end
The above example shows you how you can test for the different event phases of a single user touch, and do something (or do nothing) during a specific phase. In most cases, the “ended” and “cancelled” phases will be handled together (as shown in the example).

To return true, or not to—THAT is the question.

At the end the the previous example, you probably noticed I put a comment that says “IMPORTANT” next to a return true statement. Whether your function returns true orfalse/nil will affect the outcome of your touch event.
When your touch listener returns true, then that means your touch was successful; the object that was touched is the one that was supposed to be touched during that time. The next event in the sequence will be dispatched properly. If there is a “touchable” object underneath the object that was touched, a touch will not register on the object because the object above successfully captured the touch event.
However, if your listener returns false (or nothing at all), then the touch will be assumed to be faulty and the only event phase you will get is the “began” phase. If there is a “touchable” object underneath the object you touched, then in this case, the touch will “leak through” the object you actually touched and be passed to the next object (assuming you’re touching two objects at once).
This is unfortunately a common cause of confusion for new Corona developers. If you come across the above scenario or something similar, just remember: Return true to keep the touches from going through.

Touch Focus

When you set up your touch listener, in a real-world scenario, it will most-likely resemble something like the following:
local function onObjectTouch( self, event )
    if event.phase == "began" then

        -- specify the global touch focus
        display.getCurrentStage():setFocus( self )
        self.isFocus = true

    elseif self.isFocus then
        if event.phase == "moved" then

            -- do something here; or not

        elseif event.phase == "ended" or event.phase == "cancelled" then

            -- reset global touch focus
            display.getCurrentStage():setFocus( nil )
            self.isFocus = nil
        end
    end

    return true
end
When detecting touch events on a specific object, it is important to set the object as the global “touch focus” to prevent unexpected behavior from occurring, and to ensure your touch event handling goes as smooth as possible. This should be done during the “began” phase (as shown in the example). Touch focus should then be reset during the ended/cancelled phase of the touch so other objects can be touched once the touch is completed, which is also demonstrated in the example.
If it helps, I recommend copying/pasting the above example into a text file to serve as a “touch event” snippet you can re-use in your own apps (it’ll save you a lot of typing).

Other Event Properties

Included with every touch event (to include each of the individual phases) is an eventtable that has a some properties that may be useful to you. They are described below:
  • event.id – a unique identifier for the touch event (mostly used for multi-touch).
  • event.name – this will always be “touch” for touch events.
  • event.phase – the current “phase” of the user’s touch. Possible values include: “began”, “moved”, “ended”, and “cancelled” (hint: I already covered this one)
  • event.target – the object that is being touched; same as ‘self’ (when self is available)
  • event.time – timestamp of when the event took place.
  • event.x, event.y – the x/y coordinates (global/screen) of where the touch is currently taking place.
  • event.xStart, event.yStart – the x/y coordinates of where the touch event began (it can be different if the use dragged their finger during the course of a single touch). Example usage here.
You can get a more detailed account of all of the above touch event properties on thetouch event API reference page.

No comments:

Post a Comment