Snap Scrolling

Hi Everyone-

I’ve been exploring how to make a unique menu system, yet have been having a very tough time trying to figure out how to properly code it. I am trying to create a unique menu system where there is a series of blocks (100% screen width, about 1/3 screen height) that will act as buttons to various game modes. There will be 5 blocks to start, which will increase as I add more content.

I would like it so that as the player swipes vertically, the blocks will snap to a grid so that at any time three blocks will be neatly displayed. The thing about the swipe is that I would like for the blocks to snap to grid as the player swipes, rather than a fluid swipe that only snaps to grid after the user removes their finger.

Attatched is a basic layout of this system, where the boxes inside the red box are what will be seen in the screen, and below the red box are what can be scrolled to (and snapped to grid so that only three are in the red area, which is the visible screen area). Lastly, these blocks will serve as buttons that when tapped will call a function.

I’ve seen a number of threads similar to this, but none have come to a divinitive solution. Maybe this can help it others who are looking into a similar style.

Any feedback is appreciated.

First thanks for posting this.  Interesting question.

I considered this for a bit after you asked and thought the best first pass would use a widget scroller to do part of the work.

https://www.youtube.com/watch?v=zEf9rmdHgFg&feature=youtu.be

Code:

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2017/02/swipemenu.zip

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) local centerX = display.contentCenterX local centerY = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = centerX - fullw/2 local right = left + fullw local top = centerY - fullh/2 local bottom = top + fullh local scrollThreshold = 10 local resetColorOnScrolled = false local snapRate = 600 -- pixels per second local scrollW = fullw local scrollH = fullh local buttonW = fullw local buttonH = math.floor(fullh/3) local function isInBounds( obj, obj2 ) if(not obj2) then return false end local bounds = obj2.contentBounds if( obj.x \> bounds.xMax ) then return false end if( obj.x \< bounds.xMin ) then return false end if( obj.y \> bounds.yMax ) then return false end if( obj.y \< bounds.yMin ) then return false end return true end local buttons = {} local function listener( event ) --[[for k,v in pairs( event ) do print(k,v) end print("-------------------------") --]] if( event.phase == "began" ) then event.target.scrolled = false for i = 1, #buttons do if( isInBounds( event, buttons[i] ) ) then buttons[i]:setFillColor(0.5, 0.8, 0.5) else buttons[i]:setFillColor(0.8, 0.5, 0.5) end end elseif(event.phase == "ended" ) then event.target:realign() for i = 1, #buttons do buttons[i]:setFillColor(0.8, 0.5, 0.5) end -- Is it a click? if( not event.target.scrolled ) then local button for i = 1, #buttons do if( isInBounds( event, buttons[i] ) ) then button = buttons[i] end end if( button ) then button:clickAction() button:setFillColor(0.5,1,1) timer.performWithDelay( 333, function() button:setFillColor(0.8,0.5,0.5) end ) end end elseif(event.phase == "moved" ) then -- Update to 'scrolled' if drag passes threshold local lastState = event.target.scrolled event.target.scrolled = event.target.scrolled or (math.abs(event.y-event.yStart) \>= scrollThreshold ) -- If state changed, recolor all buttons to 'off' if( resetColorOnScrolled and lastState ~= event.target.scrolled ) then for i = 1, #buttons do buttons[i]:setFillColor(0.8, 0.5, 0.5) end end end end local widget = require "widget" local options = { x = centerX, y = centerY, width = fullw, height = fullh, backgroundColor = { 0.8, 0.8, 0.8 }, hideScrollBar = true, listener = listener, } local menu = widget.newScrollView( options ) -- This function make sure the buttons align nicely to the top edge function menu.realign(self) local minY = -self:getView().contentHeight + 3 \* buttonH local x,y = self:getContentPosition() if( y \<= minY ) then print( "Skipping") elseif( y \>= 0 ) then print("Skipping") else local dy = y local count = 0 local toY while(dy \< 0) do count = count + 1 dy = dy + buttonH end if( dy \> buttonH/2 ) then toY = y + (buttonH-dy) else toY = y + (buttonH-dy) - buttonH end local distance = math.abs(y-toY) print("Realign it" , buttonH, dy, y, count, toY, distance ) self:scrollToPosition( { y = toY, time = 1000 \* distance/snapRate }) end end for i = 1, 6 do local button = display.newRect( buttonW/2, (i-0.5) \* buttonH, buttonW-2, buttonH-2 ) menu:insert(button) button:setFillColor(0.8, 0.5, 0.5) button:setStrokeColor(0) button.strokeWidth = 2 buttons[#buttons+1] = button button.label = display.newText( i, button.x, button.y, native.systemFont, 20 ) button.label:setFillColor(0) menu:insert(button.label) button.id = i function button.clickAction( self ) print("Clicked button ", self.id ) -- Do click work here end end

Thank you so much, this is fantastic! For reference, on the following line what purpose does the + 3 serve? 

local minY = -self:getView().contentHeight + 3 \* buttonH

For the algorithm to work minY is assumed to be -( totalContentHeight - threeButtonHeights)

Just as an add-on tip for newbies…  

If you replace the line

local button = display.newImageRect( buttonW/2, (i-0.5) \* buttonH, buttonW-2, buttonH-2 )

with

local button = display.newImageRect( "p"..i..".jpg", buttonW-2, buttonH-2 ) button.x = buttonW/2 button.y = (i-0.5) \* buttonH

then the buttons are nice images. (Just create images like p1.jpg, p2.jpg etc)

(You will need to play with the fill colours to stop the backfilling, but you should be able to sort it)

First thanks for posting this.  Interesting question.

I considered this for a bit after you asked and thought the best first pass would use a widget scroller to do part of the work.

https://www.youtube.com/watch?v=zEf9rmdHgFg&feature=youtu.be

Code:

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2017/02/swipemenu.zip

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) local centerX = display.contentCenterX local centerY = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = centerX - fullw/2 local right = left + fullw local top = centerY - fullh/2 local bottom = top + fullh local scrollThreshold = 10 local resetColorOnScrolled = false local snapRate = 600 -- pixels per second local scrollW = fullw local scrollH = fullh local buttonW = fullw local buttonH = math.floor(fullh/3) local function isInBounds( obj, obj2 ) if(not obj2) then return false end local bounds = obj2.contentBounds if( obj.x \> bounds.xMax ) then return false end if( obj.x \< bounds.xMin ) then return false end if( obj.y \> bounds.yMax ) then return false end if( obj.y \< bounds.yMin ) then return false end return true end local buttons = {} local function listener( event ) --[[for k,v in pairs( event ) do print(k,v) end print("-------------------------") --]] if( event.phase == "began" ) then event.target.scrolled = false for i = 1, #buttons do if( isInBounds( event, buttons[i] ) ) then buttons[i]:setFillColor(0.5, 0.8, 0.5) else buttons[i]:setFillColor(0.8, 0.5, 0.5) end end elseif(event.phase == "ended" ) then event.target:realign() for i = 1, #buttons do buttons[i]:setFillColor(0.8, 0.5, 0.5) end -- Is it a click? if( not event.target.scrolled ) then local button for i = 1, #buttons do if( isInBounds( event, buttons[i] ) ) then button = buttons[i] end end if( button ) then button:clickAction() button:setFillColor(0.5,1,1) timer.performWithDelay( 333, function() button:setFillColor(0.8,0.5,0.5) end ) end end elseif(event.phase == "moved" ) then -- Update to 'scrolled' if drag passes threshold local lastState = event.target.scrolled event.target.scrolled = event.target.scrolled or (math.abs(event.y-event.yStart) \>= scrollThreshold ) -- If state changed, recolor all buttons to 'off' if( resetColorOnScrolled and lastState ~= event.target.scrolled ) then for i = 1, #buttons do buttons[i]:setFillColor(0.8, 0.5, 0.5) end end end end local widget = require "widget" local options = { x = centerX, y = centerY, width = fullw, height = fullh, backgroundColor = { 0.8, 0.8, 0.8 }, hideScrollBar = true, listener = listener, } local menu = widget.newScrollView( options ) -- This function make sure the buttons align nicely to the top edge function menu.realign(self) local minY = -self:getView().contentHeight + 3 \* buttonH local x,y = self:getContentPosition() if( y \<= minY ) then print( "Skipping") elseif( y \>= 0 ) then print("Skipping") else local dy = y local count = 0 local toY while(dy \< 0) do count = count + 1 dy = dy + buttonH end if( dy \> buttonH/2 ) then toY = y + (buttonH-dy) else toY = y + (buttonH-dy) - buttonH end local distance = math.abs(y-toY) print("Realign it" , buttonH, dy, y, count, toY, distance ) self:scrollToPosition( { y = toY, time = 1000 \* distance/snapRate }) end end for i = 1, 6 do local button = display.newRect( buttonW/2, (i-0.5) \* buttonH, buttonW-2, buttonH-2 ) menu:insert(button) button:setFillColor(0.8, 0.5, 0.5) button:setStrokeColor(0) button.strokeWidth = 2 buttons[#buttons+1] = button button.label = display.newText( i, button.x, button.y, native.systemFont, 20 ) button.label:setFillColor(0) menu:insert(button.label) button.id = i function button.clickAction( self ) print("Clicked button ", self.id ) -- Do click work here end end

Thank you so much, this is fantastic! For reference, on the following line what purpose does the + 3 serve? 

local minY = -self:getView().contentHeight + 3 \* buttonH

For the algorithm to work minY is assumed to be -( totalContentHeight - threeButtonHeights)

Just as an add-on tip for newbies…  

If you replace the line

local button = display.newImageRect( buttonW/2, (i-0.5) \* buttonH, buttonW-2, buttonH-2 )

with

local button = display.newImageRect( "p"..i..".jpg", buttonW-2, buttonH-2 ) button.x = buttonW/2 button.y = (i-0.5) \* buttonH

then the buttons are nice images. (Just create images like p1.jpg, p2.jpg etc)

(You will need to play with the fill colours to stop the backfilling, but you should be able to sort it)