Skip to main content

Swiping objects sideways in Corona SDK

Just the other day I needed to create a "swipeable menu" for my next Corona SDK game called Ice Trap, where I want the player to be able to swipe left/right to select among a number of chapters in the level selection scene.

Basically, what I wanted to achieve was this:

1. Have a number of display groups, each one representing one of my game's chapters
2. Only one chapter (display group) visible at the screen at a time
3. Allow for the user to swipe left/right to select chapter
4. Prevent swiping too far left or right
5. Use transition effects to slide the chapters into place nicely
6. Display a clickable thumbnail for each chapter, allowing the user to quick jump to any  one chapter. Also highlight the currently selected chapter

Here's a simple sketch of the idea:



After scanning the Internet for half an hour or so I still hadn't found a complete solution for this, so I decided to roll my own instead. What I did find was this small tutorial from Corona Labs describing how to recognize swipe gestures to move objects to fixed places. Not exactly what I was looking for, but it provided me with some nice input so I could get a jump start on my little project.

I won't go through the implementation process in detail, but the main steps I did was this:

1. Wrap up the handling of the swipeable objects in a reusable module which I decided to call slideshow. Maybe not a totally accurate name, but good enough.
2. Make it possible to inject the swipeable objects into the slideshow module
3. Make the slideshow module customizable with some parameters, for example to handle swipe sensitivity, transition effects and vertical position
4. Create the thumbnails outside of the slideshow module, and instead use callbacks from the slideshow module to update them

This is what is looks like when used in the game. Each chapter is its own display group, created separately and injected into the slideshow module.




To test and demonstrate all this I created a simple example, and this is what the example looks like when run in the simulator. Note that colors are randomized.


And here's the full source code for both the slideshow module as well as the example:

--[[
Example of using the slideshow module
Creates a number of large circles as the main display groups used by the slideshow example
Also creates thumbnails that will be updated by callbacks from the slidehow module
Markus Ranner 2016
--]]
local slideshow = require("slideshow")
display.setStatusBar(display.HiddenStatusBar)
--[[
Create circles of random colors to use as slideshow objects
--]]
local function createSlideshowObjects(numberOfObjects)
local objects = {}
for i = 1, numberOfObjects do
--[[
Each slideshow object is added to its own display group.
This is not really necessary in this example, but shows that the slideshow works with display groups which will be the most likely use case
--]]
local group = display.newGroup()
local circle = display.newCircle(0, 0, 200)
-- Save the fill color as a property to be able to retrieve it later for the thumbnails
circle.fillColor = { math.random(), math.random(), math.random(), 1 }
circle:setFillColor(unpack(circle.fillColor))
group:insert(circle)
objects[#objects + 1] = group
end
return objects
end
--[[
Create and return a display group containing a thumbnail for each of the slideshow objects
--]]
function createThumbnails(slideshowObjects)
local group = display.newGroup()
local thumbSize = 30
local thumbMargin = 10
for i = 1, #slideshowObjects do
-- Since we added the slideshow objects to a display group, we get the first (only) object of the group here to find out its color
local obj = slideshowObjects[i][1]
local thumb = display.newCircle((i - 1) * (thumbSize + thumbMargin), 0, thumbSize / 2, thumbSize / 2)
thumb:setFillColor(unpack(obj.fillColor))
thumb:setStrokeColor(1, 1, 1, 1.0)
thumb.strokeWidth = 0
thumb.anchorX = 0
-- Setup a tap handler for each thumb that will quick jump to selected object index
thumb:addEventListener("tap", function()
local disableTransition = false
slideshow.showObjectAtIndex(i, disableTransition)
end)
group:insert(thumb)
end
group.x = display.contentCenterX - (#slideshowObjects / 2 * (thumbSize + thumbMargin))
group.y = display.contentHeight - 100
return group
end
math.randomseed( os.time() )
-- Setup the objects and thumbnails to use for the slideshow
local slideshowObjects = createSlideshowObjects(9)
local thumbnailsGroup = createThumbnails(slideshowObjects)
local function updateThumbnails(selectedObjectIndex)
-- Highlight the stroke of the selected object's corresponding thumbnail
for i = 1, thumbnailsGroup.numChildren do
local thumb = thumbnailsGroup[i]
if (i == selectedObjectIndex) then
thumb.strokeWidth = 3
else
thumb.strokeWidth = 0
end
end
end
--[[
Start the slideshow
This example shows all the customizable parameters, but all the parameters are optional
--]]
local slideshowParams = {
startIndex = 5,
transitionEffect = easing.outCubic,
transitionEffectTimeMs = 250,
y = display.contentCenterY - 100,
swipeSensitivityPixels = 50,
onChange = updateThumbnails,
}
slideshow.init(slideshowObjects, slideshowParams)
view raw main.lua hosted with ❤ by GitHub
--[[
Corona SDK slideshow module
Handles a number of display objects/groups, displaying only one at a time.
Allows user to swipe left/right to change displayed object.
Uses callbacks to update other parts of the program when a new object is displayed.
Markus Ranner 2016
--]]
-- State
local _slideshowObjects
local _currentObjectIndex
local _transitionEffectTimeMs
local _transitionEffect
local _swipeSensitivityPixels
local _onChange
local function showObject(objectIndex, disableTransition)
local objectToShow = _slideshowObjects[objectIndex]
-- Update current object index and make a callback if selected object has changed
if(_currentObjectIndex ~= objectIndex) then
_currentObjectIndex = objectIndex
if (_onChange) then
_onChange(_currentObjectIndex)
end
end
-- Transition all slideshow objects into place
for i = 1, #_slideshowObjects do
local object = _slideshowObjects[i]
local x = _slideshowObjects[i].originalPosition.x - ((objectIndex - 1) * display.contentWidth)
-- If transition has been disabled, update position immediately
if (disableTransition) then
object.x = x
else
transition.to(object, {
x = x,
time = _transitionEffectTimeMs,
transition = _transitionEffect,
})
end
end
end
local function handleSwipe( event )
local swipedObject = event.target
local swipeDistanceX = event.x - event.xStart
if (event.phase == "began") then
-- Set a focus flag on the object, so that we don't handle touch events that weren't started on the same object
swipedObject.hasFocus = true
-- This redirects all futre touch events to the swiped object, even when touch moves outside of its bounds
display.getCurrentStage():setFocus( swipedObject )
elseif ( event.phase == "moved" ) then
if (swipedObject.hasFocus) then
-- Move all objects according to swipe gesture
for i = 1, #_slideshowObjects do
local object = _slideshowObjects[i]
local offsetX = -((_currentObjectIndex - 1) * display.contentWidth)
local x = object.originalPosition.x + offsetX + swipeDistanceX
object.x = x
end
end
elseif( event.phase == "ended" ) then
-- Reset touch event focus
swipedObject.hasFocus = false
display.getCurrentStage():setFocus( nil )
-- Calculate which object to show next, preventing swiping too far left or right
local nextObjectIndex = _currentObjectIndex
if((swipeDistanceX >= _swipeSensitivityPixels) and (_currentObjectIndex > 1)) then
nextObjectIndex = _currentObjectIndex - 1
elseif((swipeDistanceX <= -_swipeSensitivityPixels) and (_currentObjectIndex < #_slideshowObjects)) then
nextObjectIndex = _currentObjectIndex + 1
end
-- Finally, show the selected object in the slideshow
showObject(nextObjectIndex)
elseif( event.phase == "cancelled" ) then
-- Reset touch event focus
swipedObject.hasFocus = false
display.getCurrentStage():setFocus( nil )
end
return true
end
local function showObjectAtIndex(objectIndex, disableTransition)
showObject(objectIndex, disableTransition)
end
local function init( slideshowObjects, params )
if (not params) then
params = {}
end
-- Set initial state for slideshow component
_transitionEffect = params.transitionEffect or easing.outCirc
_transitionEffectTimeMs = params.transitionEffectTimeMs or 200
_slideshowObjects = slideshowObjects
_swipeSensitivityPixels = params.swipeSensitivityPixels or 100
_onChange = params.onChange or nil
local y = params.y or display.contentCenterY
-- Position each slideshow object and setup a touch handler for each one
for i = 1, #slideshowObjects do
local obj = slideshowObjects[i]
-- Set initial position for every slideshow object
obj.x = display.contentCenterX + ((i - 1) * display.contentWidth)
obj.y = y
-- For display groups, we need to set the anchorChildren property to true to correctly position the child objects
obj.anchorChildren = true
-- The originalPosition will be used to position all slideshow objects correctly while swiping
obj.originalPosition = { x = obj.x , y = obj.y }
obj:addEventListener( "touch", handleSwipe )
end
-- Show selected start object
local startIndex = params.startIndex or 1
local disableTransition = true
showObject(startIndex, disableTransition)
end
local function cleanUp()
_slideshowObjects = nil
_currentObjectIndex = nil
end
return {
init = init,
cleanUp = cleanUp,
showObjectAtIndex = showObjectAtIndex,
}
view raw slideshow.lua hosted with ❤ by GitHub


Comments

  1. This is just what I was looking for! Thank you so much! I have just one question. I noticed that the circles only change when you press and drag the circle itself. I wondered if it would be possible to make the circles change when you swipe anywhere on the screen. I tried to do this using a rectangle and an eventListener but no luck. I'm really struggling on this since I'm not really familiar with Corona SDK yet. If this is possible could you tell me how? Any advice would be deeply appreciated. Thanks :)))
    -John

    ReplyDelete
    Replies
    1. Hi John, glad to hear that you liked my blog post. :-)

      If I understand you correctly you can do this by adding an invisible rectangle that spans the entire screen to each display group passed into slideshow.init(). Just make sure to set isHitTestable=true to be able to catch touch events for invisible objects.

      In my example code, try adding the following four lines of code to createSlideshowObjects(). Note that it's important to insert the rectangle into the group _after_ the circle because of how the thumbnails are created in my example.

      group:insert(circle)

      -- BEGIN NEW CODE
      local bg = display.newRect(0, 0, display.contentWidth, display.contentHeight) bg.isVisible = false
      bg.isHitTestable = true
      group:insert(bg)
      -- END NEW CODE

      objects[#objects + 1] = group

      Delete
    2. Markus, I cannot thank you enough for this !!! My app now looks a million times better! :)) By the way, I love the way your blog/website looks! Extremely aesthetic

      Delete
    3. Thanks! And you're welcome, I'm just happy to be able help out a fellow Corona dev in need! :-)

      Delete

Post a Comment