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:
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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, | |
} |
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 :)))
ReplyDelete-John
Hi John, glad to hear that you liked my blog post. :-)
DeleteIf 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
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
DeleteThanks! And you're welcome, I'm just happy to be able help out a fellow Corona dev in need! :-)
Delete