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:
- If the circle is tapped, remove it from the screen.
- If the circle is pressed and held, drag it around the screen.
- 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)
Comments
- Anonymous
March 31, 2014
The comment has been removed - Anonymous
April 01, 2014
The comment has been removed