The table view is the widget to use when creating proper iOS style menus, but what about submenus? I’m talking about menus where tapping one row in the table view causes the whole screen to slide left and the submenu slides in from the right?
The code below has been removed from the WidgetDemo and encapsulated into ‘menu.lua’ - with a lot of changes - to be reusable and provide a potentially endless depth of submenus.
There is also a file called ‘menustructure.lua’ which defines the content for the menus. You will notice that the lower levels need to be defined first, but that’s down to standard lua variable declaration.
The title bars for the menus are handled for you and will include back buttons, where necessary.
Category separators can be defined like this:
[lua]{ id="", text=“Find”, iscategory=true, dataitem=nil, index=“F” },[/lua]
The ‘index’ value should be unique and will match with a character on the alpha strip as this has been included in the menu library listing.
The title, initial row scroll index, alpha strip and fading scrollbar (see my previous posts in this forum) can be enabled by passing true in the following properties:
[lua]title=“Products”,
defaultscroll=1,
hasindex=true,
hasscrollbar=true,[/lua]
You can see how the menu items are defined in the menustructure.lua file, but submenu content does not need to be known at the time of tapping the parent row. Each ‘dataitem’ value can be a fixed table of content, but it can also be a callback function. To allow for asynchronous population of the submenu the dataitem value needs to look like this:
[lua]dataitem=function(d,f) f(GetTestSubMenu()) end[/lua]
Check the “Companies With…” menu item on the root menu to see it in action.
The parameters passed into the function are the parent menu dataitem and the function to call once the data is constructed - this could be after a time-consuming network request has completed, for example.
Basically, the dataitem allows you to either provide a fixed table for the sub menu items or a function which gets called when the parent row is tapped. The function does not return anything to populate the submenu, instead it calls the function passed into its second parameter. That function is the internal ‘callback(dataitem)’ function. If you never call that function it doesn’t matter - it simply means that you’ve used that menu item tap to do something else, like switching to a different tab or taking over the whole screen.
To put the submenu capable tableview in your app use this code:
[lua]require(“menu”)
require(“menustructure”)
newMenu( demoGroup, rootmenu )[/lua]
The demoGroup used above could easily be the demoGroup used in the WidgetDemo code - but you might experience oddities when switching to other tabs because the original WidgetDemo actually clears all the content of the parent group when a tab is tapped. I will be submitting a solution to the Code Exchange soon which solves this problem.
The caveat: Please be aware that some of the values may not be appropriate for all screen sizes - you’ll notice ‘366’ appearing and this is because I’m basically lazy (as all good developers are!)
So, the code:
menustructure.lua:
[lua]-- menu structure
rootmenu, byproduct, byjob, byregion, companieswith = nil, nil, nil, nil, nil, nil
byproduct = {
title=“By Product”,
{ id="", text=“Cutlery”, iscategory=false, dataitem=nil },
{ id="", text=“Homewares”, iscategory=false, dataitem=nil },
{ id="", text=“Frozen”, iscategory=false, dataitem=nil },
{ id="", text=“Refreshments”, iscategory=false, dataitem=nil },
}
byjob = {
title=“By Job”,
{ id="", text=“Director”, iscategory=false, dataitem=nil },
{ id="", text=“Sales”, iscategory=false, dataitem=nil },
{ id="", text=“Purchasing”, iscategory=false, dataitem=nil },
}
byafrica = {
title=“Africa”,
{ id="", text=“Mozambique”, iscategory=false, dataitem=nil },
{ id="", text=“Zaire”, iscategory=false, dataitem=nil },
{ id="", text=“Ethiopia”, iscategory=false, dataitem=nil },
}
byregion = {
title=“By Region”,
{ id="", text=“Africa”, iscategory=false, dataitem=byafrica },
{ id="", text=“Asia”, iscategory=false, dataitem=nil },
{ id="", text=“Oceana”, iscategory=false, dataitem=nil },
}
– example callback function for demonstrating dynamic menu content, this could include network activity
function GetTestSubMenu()
print(‘GetTestSubMenu()’)
local tbl = { title=“Test Items”, defaultscroll=1, hasindex=true, }
for i=65, 90 do
tbl[#tbl+1] = { id="", text=string.char(i), iscategory=false, callback=nil, dataitem=nil, index=string.char(i), }
end
return tbl
end
rootmenu = {
title=“Products”, defaultscroll=1, hasindex=true, hasscrollbar=true,
{ id="", text=“Find”, iscategory=true, dataitem=nil, index=“F” },
{ id="", text=“By Product”, iscategory=false, dataitem=byproduct, candelete=true },
{ id="", text=“By Job”, iscategory=false, dataitem=byjobfunction, candelete=true },
{ id="", text=“By Region”, iscategory=false, dataitem=byregion, candelete=true },
{ id="", text=“Search”, iscategory=true, dataitem=nil, index=“S” },
{ id="", text=“Companies With…”, iscategory=false, dataitem=function(d,f) f(GetTestSubMenu()) end },
{ id="", text=“Info”, iscategory=true, dataitem=nil, index=“I” },
{ id="", text=“About…”, iscategory=false, dataitem=nil },
{ id="", text=“Magazines”, iscategory=true, dataitem=nil, index=“M” },
{ id="", text=“The Times”, iscategory=false, dataitem=nil },
{ id="", text=“The Independent”, iscategory=false, dataitem=nil },
{ id="", text=“The Sun”, iscategory=false, dataitem=nil },
}[/lua]
menu.lua:
[lua]-- menu
function newMenu( parent, rootdata, x, y, width, height )
function newTopBar( parent, title, scrollToBack, scrollToTop )
– local this bar
local group = display.newGroup()
group.widgets = {}
parent:insert( group )
– clean out the widgets
function group:clean()
for i=#group.widgets, 1, -1 do
group.widgets[i]:removeSelf()
group.widgets[i] = nil
end
group:removeSelf()
end
– status bar touch pad
local function touchscroll( event )
if (event.phase == “began” and event.y <= display.statusBarHeight) then
if (scrollTopCallback) then
scrollToTop()
end
end
return true
end
Runtime:addEventListener( “touch”, touchscroll )
– create a gradient for the top-half of the toolbar
local toolbarGradient = graphics.newGradient( {168, 181, 198, 255 }, {139, 157, 180, 255}, “down” )
– create toolbar to go at the top of the screen
local titleBar = widget.newTabBar{
top = display.statusBarHeight,
topGradient = toolbarGradient,
bottomFill = { 117, 139, 168, 255 },
height = 44, width = 320
}
group:insert( titleBar.view )
group.widgets[#group.widgets+1] = titleBar
– create embossed text to go above toolbar
local titleText = display.newEmbossedText( title, 0, 0, native.systemFontBold, 20, { 255 } )
titleText:setReferencePoint( display.CenterReferencePoint )
titleText.x = display.contentWidth * 0.5 – (display.contentWidth * 0.5) – + x
titleText.y = 44 – + (titleBar.y + titleBar.height * 0.5)
group:insert( titleText )
group.widgets[#group.widgets+1] = titleText
– onRelease listener for back button
local function onBackRelease( event )
if (scrollToBack) then
scrollToBack( group.clean )
end
return true
end
– create ‘back’ button to be placed on toolbar
if (parent.parent.numChildren > 1) then
backButton = widget.newButton{
label = “Back”,
left = 5, top = 28,
style = “backSmall”,
onRelease = onBackRelease
}
group:insert( backButton.view )
group.widgets[#group.widgets+1] = backButton
end
end
function newSubMenu( menudata )
– container for all submenu components
local submenugroup = display.newGroup()
parent:insert( submenugroup )
– timers etc
local posCheckTimer = nil
local adjustScrollBar = nil
– position the submenugroup
submenugroup.x, submenugroup.y = (parent.numChildren-1) * display.contentWidth, 0
– create tableView widget
local list = widget.newTableView{
top = display.statusBarHeight + 44,
width = display.contentWidth,
height = 366,
maskFile = “assets/mask-320x366.png”
}
– scroll to top function
local function scrollToTop()
list:scrollToIndex( 1, 400 )
end
– scroll to origin (back button slide)
local function scrollToBack( onComplete )
function clean()
print(‘before’,parent.numChildren)
if (posCheckTimer) then
timer.cancel( posCheckTimer )
posCheckTimer = nil
end
Runtime:removeEventListener( “enterFrame”, adjustScrollBar )
onComplete()
list:removeSelf()
list = nil
submenugroup:removeSelf()
print(‘after’,parent.numChildren)
end
transition.to( parent, {time=300, x=parent.x+display.contentWidth, onComplete=clean} )
end
– create title bar
newTopBar( submenugroup, menudata.title, scrollToBack, scrollToTop )
– insert widget into demoGroup
submenugroup:insert( list.view )
– onEvent listener for the tableView
local function onRowTouch( event )
local row = event.target
local rowGroup = event.view
print(‘touch’,event.phase,menudata[event.index].candelete)
if (not row.isCategory) then
if (event.phase == “swipeLeft” and menudata[event.index].candelete == true) then
transition.to( rowGroup.del, {time=rowGroup.del.transtime,maskX=-rowGroup.del.width/2,onComplete=rowGroup.del.setActive} )
elseif (event.phase == “swipeRight” and menudata[event.index].candelete == true) then
transition.to( rowGroup.del, {time=rowGroup.del.transtime,maskX=rowGroup.del.width/2,onComplete=rowGroup.del.setInactive} )
elseif (event.phase == “release” and not row.isCategory) then
row.reRender = true
print( “You touched row #” … event.index…" - ‘"…menudata[event.index].text…"’" )
– do
local menuitem = menudata[event.index]
print(‘dataitem’,menuitem.dataitem)
if (menuitem.dataitem ~= nil) then
– get the submenu’s data and add the new sub menu
– this will shift the parent group left to show a deeper submenu
local dataitem = menuitem.dataitem
– populate the submenu
function callback(dataitem)
newSubMenu( dataitem )
transition.to( parent, {time=300, x=parent.x-display.contentWidth} )
transition.to( topGroup, {time=300, x=parent.x-display.contentWidth} )
end
– if dataitem is a function, call it to get the data to put in the submenu, otherwise just populate the submenu
– if the dataitem is a function it must callback the passed in function to populate the submenu, this allows for network traffic delays
– the callback function does not need to be called, if the dataitem function wants to switch to another tab, for example
if (type(dataitem) == “function”) then
dataitem = dataitem( menuitem, callback )
else
callback( dataitem )
end
end
end
end
return true
end
– onRender listener for the tableView
local function onRowRender( event )
local row = event.target
local rowGroup = event.view
local textFunction = display.newRetinaText
if row.isCategory then textFunction = display.newEmbossedText; end
if (menudata[event.index].searchcallback ~= nil) then
– is a search bar
row.searchimg = display.newImage( rowGroup, “searchwith.png” )
row.searchimg.xScale, row.searchimg.yScale = .5, .5
row.height = row.searchimg.height/2
row.searchimg.x, row.searchimg.y = display.contentCenterX, row.searchimg.height/4
native.newTextField( 50, 150, 220, 36, nil )
print(‘here’)
else
– is regular bar
row.title = textFunction( menudata[event.index].text, 12, 0, native.systemFontBold, 16 )
row.title:setReferencePoint( display.CenterLeftReferencePoint )
row.title.y = row.height * 0.5
if (not row.isCategory) then
row.title.x = 15
row.title:setTextColor( 0 )
local del = nil – the image sitting over the top of the button widget
local onButtonEvent = function (event )
print(‘del.isactive’,del.isactive)
if (not del.isactive) then
return false
end
if (event.phase == “release”) then
print( “You pressed and released a button!” )
end
return true
end
local myButton = widget.newButton{
id = “btn001”,
left = 225,
top = 9,
label = “”,
width = 90, height = 46,
cornerRadius = 8,
onEvent = onButtonEvent,
default=“deleteback.png”,
over=“deleteback.png”,
}
rowGroup:insert(myButton.view)
del = display.newImage(rowGroup,“deletebtn.png”)
del.x, del.y = 270, 32
del.xScale, del.yScale = .7, .7
del.isactive = false
rowGroup.del = del
local mask = graphics.newMask(“slidemask.png”)
del:setMask( mask )
del.maskScaleX, del.maskScaleY = -1, 1
del.maskX = del.width/2
del.maskY = 100
del.transtime = 300
function del:setActive()
print(‘active’,deletetap)
del.isactive = true
end
function del:setInactive()
print(‘inactive’)
del.isactive = false
end
end
– must insert everything into event.view:
rowGroup:insert( row.title )
end
end
– item: { id="", text=“By Product”, iscategory=false, childcallback=nil, childmenu=byproduct },
– list of scroll index items
local scroll = {}
list.totalheight = 0
– Add rows
for i=1, #menudata do
local rowHeight, rowColor, lineColor, isCategory = 64
local menuitem = menudata[i]
if (menuitem.iscategory) then
print('category: '…menuitem.text)
isCategory = true; rowHeight = 24; rowColor={ 174, 183, 190, 255 }; lineColor={0,0,0,255}
end
– accumulate the total height of the content rows
list.totalheight = list.totalheight + rowHeight
– insert the row into the tableView widget
list:insertRow{
onEvent=onRowTouch,
onRender=onRowRender,
height=rowHeight,
isCategory=isCategory,
rowColor=rowColor,
lineColor=lineColor,
issearch=true
}
– index can be a string or a function to return a string - string can be one char
if (menuitem.index ~= nil) then
local scrollitem = menuitem.index
if (type(scrollitem) == “function”) then
scrollitem = scrollitem( menuitem )
end
scroll[#scroll+1] = { index=i, char=scrollitem }
end
end
function addAlphaStrip(parent, callback)
local strip = display.newGroup()
parent:insert( strip )
strip.x, strip.y = display.contentWidth - 15, display.statusBarHeight+50
local alpha = display.newRoundedRect( strip, -10, 0, 20, display.contentHeight - 128, 10 )
alpha:setFillColor( 224, 224, 224 )
local size = 13.3
for i=65, 90 do
local index = i-64
local function touch(event)
for s=1, #scroll do
if (scroll[s].char == string.char(i)) then
list:scrollToIndex( scroll[s].index, 300 )
break
end
end
return true
end
local rect = display.newRect( strip, 0, 0, 40, size )
rect.x, rect.y = 0, 10+(i-65)*size
rect.alpha = 0
rect.isHitTestable = true
local letter = display.newText(strip, string.char(i), 0, 0, native.systemFont, 10)
letter:setTextColor(106, 115, 125)
letter.x, letter.y = 0.5, 10+(i-65)*size
rect:addEventListener( “touch”, touch )
end
end
if (menudata.hasindex) then addAlphaStrip( submenugroup ) end
if (menudata.defaultscroll) then list:scrollToIndex( menudata.defaultscroll, 0 ) end
local currentpos = list:getScrollPosition()
list.maxscrolltop = 366-list.totalheight+24
–[[scroll bar handling]]–
local scrollbar = display.newRoundedRect( submenugroup, display.contentWidth - 5, display.statusBarHeight+50, 2.5, display.contentHeight - 128, 2.5 )
scrollbar:setFillColor( 255,0,0 )
scrollbar.lastchangetime = system.getTimer()
adjustScrollBar = function( event )
if (list.totalheight < 366) then
scrollbar.alpha = 0
else
local starty = scrollbar.y
– check scroll position and adjust
local scrollpos = list:getScrollPosition()
if (scrollpos <= 0 and scrollpos >= list.maxscrolltop) then
local heightperc = (346 / list.totalheight) * 100
local scrollperc = (math.abs(scrollpos / math.abs(list.maxscrolltop))) * 100
scrollbar.height = 346/100*heightperc
scrollbar.y = 10 + display.statusBarHeight + 44 + (scrollbar.height/2)+((346-scrollbar.height)/100*scrollperc)
elseif (scrollpos > 0) then
local scrollperc = (scrollpos / 366) * 100
local heightperc = (346 / list.totalheight) * 100
heightperc = heightperc - scrollperc
scrollbar.height = 346/100*heightperc
scrollbar.y = 10 + display.statusBarHeight + 44 + (scrollbar.height/2)
elseif (scrollpos < list.maxscrolltop) then
local scrollperc = ((list.maxscrolltop - scrollpos) / 366) * 100
local heightperc = (346 / list.totalheight) * 100
heightperc = heightperc - scrollperc
scrollbar.height = 346/100*heightperc
scrollbar.y = 10 + display.statusBarHeight + 44 + (346 - (scrollbar.height/2))
end
– fade out the scrollbar
if (starty ~= scrollbar.y) then
scrollbar.lastchangetime = system.getTimer()
end
if (system.getTimer() - scrollbar.lastchangetime > 500) then
if (scrollbar.fadeintrans ~= nil) then
transition.cancel( scrollbar.fadeintrans )
scrollbar.fadeintrans = nil
end
if (scrollbar.fadeouttrans == nil) then
scrollbar.fadeouttrans = transition.to( scrollbar, { time=250, alpha=0 } )
– check that scroll position is corrected
if (list:getScrollPosition() > 0 or list.totalheight < 366) then
print(‘scroll to top’,list:getScrollPosition())
list:scrollToY( 0, 200 )
elseif (list:getScrollPosition() < list.maxscrolltop) then
print(‘scroll to bottom’,366-list.height,list:getScrollPosition())
list:scrollToY( list.maxscrolltop, 200 )
end
end
else
if (scrollbar.fadeouttrans ~= nil) then
transition.cancel( scrollbar.fadeouttrans )
scrollbar.fadeouttrans = nil
end
if (scrollbar.fadeintrans == nil) then
scrollbar.fadeintrans = transition.to( scrollbar, { time=150, alpha=1 } )
end
end
end
end
if (menudata.hasscrollbar) then Runtime:addEventListener( “enterFrame”, adjustScrollBar ) end
end
newSubMenu( rootdata )
end[/lua]
Please note: There is an enterFrame event handler for adjusting the tableView when out of proper scroll position. I’m told by the Ansca team that this is fixed in build 721 but as I don’t have access to that I’ll have to wait until the next public drop. [import]uid: 8271 topic_id: 27272 reply_id: 327272[/import]