How to implement press and hold in Corona SDK

I've been playing with Corona SDK for the past two weeks, and really am having a lot of fun. My favorite part is that you don't have to use any fancy IDEs or tools. It's just you, your text editor, and the Corona libraries. For this reason, I think it's a great way to introduce someone to programming and it's not something you'll quickly outgrow. I have a bunch of blog posts in mind, but for starters I thought I'd share how I implemented press and hold behavior, something not supported by Corona out of the box as of version 2014.2189.

The tasks are as follows. Draw a circle, and then:

  1. If the circle is tapped, remove it from the screen.
  2. If the circle is pressed and held, drag it around the screen.
  3. If the circle is touched but not held, draw a line extending away from the circle.

If you've been playing with Corona for a bit, you'll know it handles both tap and touch events. However, there's nothing special in Corona as of today to handle press and hold events. Here's how I accomplished all three tasks. I'm using Graphics 2.0, so you'll have to modify things slightly if you're running an old version of Corona. It definitely works in version 2014.2189.

Let's start with something easy. First, draw a circle in the center of the screen, with a radius 1/10 the screen width. Make the circle green:

 local radius = display.contentWidth * 0.1
local circle = display.newCircle(display.contentWidth / 2, 
     display.contentHeight / 2, radius)
circle:setFillColor(0,1,0)

Then, implement an event handler which removes the circle from the stage when tapped:

 local function removeCircle(event)
    -- remove circle when tapped
    display.remove(event.target)
    return true -- stops processing of further events
end

circle:addEventListener("tap", removeCircle)

Now you're done with task 1. For task 2, we need to implement a handler for the touch event. Start with the following code, and we'll add to it shortly:

 local holdTimer
local myLine
local function touchCircle(event)
    if event.phase == "began" then
        -- Determine if circle is being held
    elseif event.phase == "moved" and not held then
        -- Draw line extending from circle
    elseif event.phase == "moved" and held then
        -- Drag circle
    elseif event.phase == "ended" or event.phase == "cancelled" then
        -- Clean up
    end
end

circle:addEventListener("touch", touchCircle)

As you can see, we'll soon define a boolean called held. The above function should look familiar to anyone who's played with Corona event handlers. If it doesn't look familiar, read up on the touch event in Corona's documentation.

In the began phase, we determine if the circle is being held. We accomplish this with a timer. Add this code to the began phase to call the holdListener function after 1500 milliseconds:

 display.getCurrentStage():setFocus(event.target) -- sets focus to circle
holdTimer = timer.performWithDelay(1500, holdListener)

Next create the holdListener function, which is called by the timer:

 local held = false
local function holdListener()
    held = true
    circle:setFillColor(1,0,0)
end

The holdListener function sets held = true and turns the circle red. This gives feedback to the user that the circle has been selected.

If you run the code now, you'll see that the timer is started when the circle is touched, and holdListener is called even if you lift your finger. The solution is to cancel the timer in the ended phase:

 display.getCurrentStage():setFocus(nil)
timer.cancel(holdTimer)
held = false
circle:setFillColor(0,1,0)

This resets the focus, cancels the timer, sets the held boolean to false, and colors the circle green again.

The final step in task 2 is to drag the circle. This code in the elseif event.phase == "moved" and held block accomplishes that:

 circle.x = event.x
circle.y = event.y

If you run the code now, you'll see that task 2 is complete. Note that the circle jumps to your finger's position when it starts to move. This isn't ideal in production, but I wanted to keep the code simple. For a better implementation, read How to Drag Objects on the Corona Labs blog.

For task 3, we want to draw a line extending from the center of the circle to your finger's position. We first need to take care of the situation when your finger moves outside of the circle's boundary. If this happens and if your finger is still touching the screen, the ended phase won't be reached and the timer won't be cancelled. The solution is to detect if your finger is outside the circle boundary and then cancel the timer. To do this, calculate your finger's distance from the circle center. If that distance is greater than the circle's radius, then you've moved outside the circle. Here's the code:

 local distanceFromCenter = math.sqrt(math.pow((circle.x - event.x),2) + 
    math.pow((circle.y - event.y),2)) -- Not optimized
if distanceFromCenter > radius then
    timer.cancel(holdTimer)
end

Note that the calculation of distanceFromCenter should be optimized in production code. For example, math.sqrt is inefficient. A more efficient approach would be compare the square of distanceFromCenter with the square of radius. You could also avoid math.pow by simply using multiplication.

Lastly, draw a line from the circle's center to your finger's position:

 display.remove(myLine)
myLine = display.newLine(circle.x, circle.y, event.x, event.y)
myLine.strokeWidth = 5

The above code first removes a previously drawn line, if one exists, and then draws a new line. Note that I don't address a few edge cases involving removing the line. For example, if you touch and move within the boundary of the circle, a short line will be drawn on top of it. You probably won't want that in production. Also, when you press and hold the circle to move it, any previously drawn line does not follow it. Addressing these edge cases is left as an exercise to the reader, or perhaps a future blog post.

So that's how I implemented press and hold behavior in Corona. If you can think of a better solution, please feel free to leave a comment.

The last thing I'll mention is that this is my first blog post created with MarkdownPad. It's a great tool, and I highly recommend you check it out.

Here's the complete code:

 local radius = display.contentWidth * 0.1
local circle = display.newCircle(display.contentWidth / 2, 
    display.contentHeight / 2, radius)
circle:setFillColor(0,1,0)

local function removeCircle(event)
    -- remove circle when tapped
    display.remove(event.target)
    return true
end

local held = false
local function holdListener(event)
    held = true
    circle:setFillColor(1,0,0)
end

local holdTimer
local myLine
local function touchCircle(event)
    if event.phase == "began" then
        -- Determine if circle is being held
        display.getCurrentStage():setFocus(event.target)
        holdTimer = timer.performWithDelay(1500, holdListener)
    elseif event.phase == "moved" and not held then
        -- Draw line from circle
        local distanceFromCenter = math.sqrt(math.pow((circle.x - event.x),2) + 
            math.pow((circle.y - event.y),2)) -- Not optimized
        if distanceFromCenter > radius then
            timer.cancel(holdTimer)
        end
        display.remove(myLine)
        myLine = display.newLine(circle.x, circle.y, event.x, event.y)
        myLine.strokeWidth = 5
    elseif event.phase == "moved" and held then
        -- Drag circle. See
        -- https://coronalabs.com/blog/2011/09/24/tutorial-how-to-drag-objects/
        -- to learn how to do this better and prevent circle from jumping
        -- to finger position.
        circle.x = event.x
        circle.y = event.y
    elseif event.phase == "ended" or event.phase == "cancelled" then
        -- Clean up
        display.getCurrentStage():setFocus(nil)
        timer.cancel(holdTimer)
        held = false
        circle:setFillColor(0,1,0)
    end
end

circle:addEventListener("touch", touchCircle)
circle:addEventListener("tap", removeCircle)