31 December 2013
Tutorial: Creating a navigation bar
Using Corona, it’s easy enough to create our own top navigation bar — just draw the background, add some title text, and place the buttons as necessary. But if we want to adhere to the “DRY” programming theory (Don’t Repeat Yourself), including an entire set of navigation bar code in every scene of an app is more work than it should be.
About the navigation bar
In the iOS vernacular, the top navigation bar is known as the UI Navigation Bar and it has fairly consistent behavior:
- a background
- an optional left button
- an optional right button
- an optional title
This tutorial illustrates how to create a standard iOS-style “UI Navigation Bar” widget in which we provide the definitions for two buttons, the functions to handle button interaction, and some basic information to draw the bar.
Setup
Like we did in the Customizing text input tutorial, let’s begin by expanding the core widget library and defining the default parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
local widget = require( "widget" ) function widget.newNavigationBar( options ) local customOptions = options or {} local opt = {} opt.left = customOptions.left or nil opt.top = customOptions.top or nil opt.width = customOptions.width or display.contentWidth opt.height = customOptions.height or 50 if ( customOptions.includeStatusBar == nil ) then opt.includeStatusBar = true -- assume status bars for business apps else opt.includeStatusBar = customOptions.includeStatusBar end -- Determine the amount of space to adjust for the presense of a status bar local statusBarPad = 0 if ( opt.includeStatusBar ) then statusBarPad = display.topStatusBarContentHeight end opt.x = customOptions.x or display.contentCenterX opt.y = customOptions.y or (opt.height + statusBarPad) * 0.5 opt.id = customOptions.id opt.isTransluscent = customOptions.isTransluscent or true opt.background = customOptions.background opt.backgroundColor = customOptions.backgroundColor opt.title = customOptions.title or "" opt.titleColor = customOptions.titleColor or { 0, 0, 0 } opt.font = customOptions.font or native.systemFontBold opt.fontSize = customOptions.fontSize or 18 opt.leftButton = customOptions.leftButton or nil opt.rightButton = customOptions.rightButton or nil -- If "left" and "top" parameters are passed, calculate the X and Y if ( opt.left ) then opt.x = opt.left + opt.width * 0.5 end if ( opt.top ) then opt.y = opt.top + (opt.height + statusBarPad) * 0.5 end |
When creating a top nav bar, there are a few things to consider, and we’ve done so in this code. For example, does the app have a device-specific status bar? If so, the nav bar should be positioned below it, but the area the status bar occupies is still space that we’re responsible for. Because we cannot programmatically detect if the status bar is showing or not, we must pass in a flag that states if we decided to show or hide the status bar. The function will then calculate the status bar height and adjust placement of the nav bar accordingly.
Next, let’s construct the bar itself:
1 2 3 4 5 6 7 8 9 |
local barContainer = display.newGroup() local background = display.newRect( barContainer, opt.x, opt.y, opt.width, opt.height + statusBarPad ) if ( opt.background ) then background.fill = { type="image", filename=opt.background } elseif ( opt.backgroundColor ) then background.fill = opt.backgroundColor else background.fill = { 1, 1, 1 } end |
In this code block, we start by creating a container group to hold all of the visual objects. Then, a rectangle is created for the “background” and it’s added to the container group. Next, the passed-in parameters determine how to fill the rectangle — either with a specified image, a solid color, or (if nothing is provided) a solid white fill.
Now, let’s add the “title” of the page to the nav bar, if provided (note that some iOS apps don’t use titles):
1 2 3 4 5 |
if ( opt.title ) then local title = display.newText( opt.title, background.x, background.y + statusBarPad * 0.5, opt.font, opt.fontSize ) title:setFillColor( unpack(opt.titleColor) ) barContainer:insert( title ) end |
Now, let’s set up the buttons. There will be two buttons in this example: a “left” button and a “right” button. To keep things simple, we’ll use the existing widget.newButton() widget — essentially, we’ll pass the parameters to the button constructor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
local leftButton if ( opt.leftButton ) then if ( opt.leftButton.defaultFile ) then -- construct an image button leftButton = widget.newButton({ id = opt.leftButton.id, width = opt.leftButton.width, height = opt.leftButton.height, baseDir = opt.leftButton.baseDir, defaultFile = opt.leftButton.defaultFile, overFile = opt.leftButton.overFile onEvent = opt.leftButton.onEvent }) else -- else, construct a text button leftButton = widget.newButton({ id = opt.leftButton.id, label = opt.leftButton.label, onEvent = opt.leftButton.onEvent, font = opt.leftButton.font or opt.font, fontSize = opt.fontSize, labelColor = opt.leftButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } }, labelAlign = "left", }) end leftButton.x = 15 + leftButton.width * 0.5 leftButton.y = title.y barContainer:insert( leftButton ) -- insert button into container group end local rightButton if ( opt.rightButton ) then if ( opt.rightButton.defaultFile ) then -- construct an image button rightButton = widget.newButton({ id = opt.rightButton.id, width = opt.rightButton.width, height = opt.rightButton.height, baseDir = opt.rightButton.baseDir, defaultFile = opt.rightButton.defaultFile, overFile = opt.rightButton.overFile, onEvent = opt.rightButton.onEvent }) else -- else, construct a text button rightButton = widget.newButton({ id = opt.rightButton.id, label = opt.rightButton.label or "Default", onEvent = opt.rightButton.onEvent, font = opt.leftButton.font or opt.font, fontSize = opt.fontSize, labelColor = opt.rightButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } }, labelAlign = "right", }) end rightButton.x = display.contentWidth - ( 15 + rightButton.width * 0.5 ) rightButton.y = title.y barContainer:insert( rightButton ) -- insert button into container group end return barContainer end |
If images are provided via the defaultFile
attribute, the code looks for the parameters associated with image-based widget buttons (see the documentation). If simple text buttons are preferred, we can specify a label
, font
, fontSize
, and labelColor
array. Once both buttons are set up, we just return
the container to the caller as a reference to the object.
Using the navigation bar widget
To use this new widget, we simply include the above code in main.lua
. It will be added to the widget library, so in other modules where you require("widget")
, it will be available to you. Then, when we need to display a new navigation bar, we follow three basic steps:
1. Write the button listeners
The buttons on the nav bar will not be very useful unless there are callback listeners associated with tap action upon them. As such, let’s include some basic listener functions in the module where we want to use the nav bar:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
local function handleLeftButton( event ) if ( event.phase == "ended" ) then -- do stuff end return true end local function handleRightButton( event ) if ( event.phase == "ended" ) then -- do stuff end return true end |
2. Declare the navigation bar buttons
Now, let’s configure the buttons specific to this nav bar. In this example, the left button will be a two-image button and the right button will be a simple text button. Notice that we are also including a reference to the callback listeners we just wrote so that our buttons respond to touch.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
local leftButton = { onEvent = handleLeftButton, width = 60, height = 34, defaultFile = "images/backbutton.png", overFile = "images/backbutton_over.png" } local rightButton = { onEvent = handleRightButton, label = "Add", labelColor = { default = {1, 1, 1}, over = { 0.5, 0.5, 0.5} }, font = "HelveticaNeue-Light", isBackButton = false } |
3. Declare the navigation bar
Finally, we declare the actual navigation bar as follows:
1 2 3 4 5 6 7 8 9 10 |
local navBar = widget.newNavigationBar({ title = "SuperDuper App", backgroundColor = { 0.96, 0.62, 0.34 }, --background = "images/topBarBgTest.png", titleColor = {1, 1, 1}, font = "HelveticaNeue", leftButton = leftButton, rightButton = rightButton, includeStatusBar = true }) |
Where from here?
There’s plenty of room for you to expand on this concept. First, this is very iOS-friendly, but it doesn’t build Android-style navigation bars. The Android-style top bar is more like the iOS Toolbar, with a series of buttons on the right side and a “hamburger” icon and graphic title on the left. Secondly, this example could be expanded to support more button styles than 2-image or basic text buttons. Finally, this example doesn’t support the navigation chevrons (less-than and greater-than symbols), but it could be configured easily enough.
Kerem
Posted at 13:20h, 31 DecemberGreat idea and tutorial! Thank you very much Rob. Happy New Year!!!
Mark
Posted at 20:07h, 01 JanuaryJust curious. Why not add this to the core widget library instead of extending it?
Rob Miracle
Posted at 09:54h, 02 JanuaryOur engineering staff is pretty booked with features and bugs that they work on. This particular feature isn’t a high-demand one and most people already doing this in some form or fashion. Part of what we blog about on our Tutorial posts are sometimes to inspire you and show you ways to do things. Hopefully at the end of the day the readers would say “Oh, that’s how that’s done” and have something new to play with.
Rob
Dan R.
Posted at 09:26h, 03 JanuaryRob,
Thanks for a great tutorial post! As a hobbyist these tutorials help tremendously to move me along…
Two quick questions:
1. When I run the NavBar demo in my simulator I get a message that the project uses premium graphics features for Pro (or higher) subscribers. After working over your code several times I’m not sure what these are or what to remove to avoid this. Thoughts? I’m using the starter version 2013.2100 on OSX 10.9
2. What is the best way to adapt the demo to a Storyboard scene to allow device rotation that recreates that Nav Bar at the correct width after the device is rotated? Here’s what I’m trying, and it seems to work, but perhaps I’ve missed a more obvious solution:
– I’ve put the code from the blog demo in the create scene function.
– I’ve added the navBar to the createScene’s group.
– In createScene I’ve added an orientation function (onOrientationChange) that calls storyboard.purgeScene on the current scene and then calls storyboard.gotoScene on the current scene to force recreation of the nav bar elements.
– at the end of the createScene function I include runtime event listener for orientation: Runtime:addEventListener( “orientation”, onOrientationChange )
– In both the exitScene and destroyScene functions I’ve included: Runtime:removeEventListener( “orientation”, onOrientationChange ). I’m not sure this needs to be in both of these places.
So, is there a better way to manage this? What I’ve written seem to work, but is having a scene purge itself and then goto itself ok, or will there be unintended consequences?
Thanks for your thoughts…
{and if this topic needs to be moved to a forum discussion please feel free to relocate it…}
Rob Miracle
Posted at 12:05h, 03 JanuaryThe whole creation of the bar is based around Graphics 2.0 methods. That could be easily re-written to not create a rectangle and fill it with a pattern, but use display.newImageRect() instead of display.newRect(). The iOS 6 gradient is also using G2.0 fills. That’s likely what is happening.
You probably only need the remove listener in destoryScene. I don’t like purging the on screen scene, but this maybe a valid use of that.
tarun
Posted at 05:13h, 06 JanuaryHi Rob,
I am running into this exception:
stack traceback:
[C]: in function ‘setFillColor’
…corona/examples/project/widget_newNavBar.lua:59: in function ‘newNavigationBar’
…top/android/corona/examples/project/main.lua:40: in main chunk
Could you confirm if the example code is bug free or not.
tarun
Posted at 05:14h, 06 JanuaryFile: …corona/examples/project/widget_newNavBar.lua
Line: 59
Attempt to call method ‘setFillColor’ (a nil value)
Rob Miracle
Posted at 14:49h, 06 JanuaryWhat version of Corona SDK are you using?
What level of subscription are you?
If line 59 is the color on the title, you are likely using a pre-Graphics 2.0 version of Corona SDK. You can safely change that to :setTextColor() instead, though setting the background of the navBar is very dependent on being Graphics 2.0.
Dan R,
Posted at 08:41h, 07 JanuaryTaurn – if you aren’t worried about the iOS6 gradient fill and only want an iOS7 style bar, this simplified version seems to work with the Starter edition. It’s not as robust as Rob’s example.
in function widget.newNavigationBar( options ) make these changes:
[lua] opt.backgroundColor = customOptions.backgroundColor or {1,1,1} –defaults to white background for iOS7 bar[/lua]
and
[lua] local barContainer = display.newGroup()
local background = display.newRect(barContainer, opt.x, opt.y, opt.width, opt.height + statusBarPad )
background:setFillColor(unpack(opt.backgroundColor))
[/lua]
Archer
Posted at 07:38h, 15 JanuaryHi there,
At the end of the tutorial you mention that it is very iOS friendly, but not Android. Are you inferring that this shouldn’t be used for Android or with some minor design changes could it be used cross platform?
Secondly, the tabbar UI corona offers doesn’t have scaling capability, does this have scaling capability across all platforms?
Lastly, can the buttons, like the tabBar UI, reflect storyboard scenes in the space below the navigation bar?
Thanks,
Archer
Rob Miracle
Posted at 17:48h, 15 JanuaryOn Android, their top bars have what’s known as a Hamburger icon on the left edge and a left aligned graphics Branding icon, like the GMail logo. Then on the right they have a series of buttons based on the app needs. This is more like the iOS Toolbar UI widget and less like the navBar wigets.
You could easily expand this to take a list of buttons and an icon to work for Android. You can still use this on Android and I think people would be okay with it, but you generally want your UI to behave like the OS wants.
You have to keep in mind that tutorials have limited space. While I could have written a fully functional cross-platform widget, it would have been too long and too complicated for a tutorial. Also part of the tutorials are to inspire you to expand on the ideas in the tutorial and not be the code solution.
For your second question, if you’re using a 320px wide content area it will be 320 on all devices so there is no scaling issues. The height may be variable..
If your tabBar buttons are transluscent the storyboard scenes underneath should show through.
Rob
Archer
Posted at 08:55h, 16 JanuaryOkay, thanks for answering those questions Rob. I really appreciate the tutorial, really great stuff, was just to be sure as a I’m a few days into the platform and still getting a feeling for it. I had an issue with passing info through the onhandle event listener for the buttons in main. The demo code does not pass the information, print(“button pressed”). I submitted this issue below before seeing your reply. I’ve been banging my head over why its not doing what I think it should and can’t come up with a reason.
Archer
Posted at 08:45h, 16 JanuaryThe handLeftButton (event) function in main does not pass information. I tried tp print button pressed to screen and got nothing. Was the same in my project as in the downloaded demo code. Recommendations?
Rob Miracle
Posted at 15:58h, 16 JanuaryI would suggest you open a post in the forums to ask the question about the left button. The blog comments can’t handle posting code very well. When you do, can you post your code you’re using?
Rob
phlo
Posted at 07:52h, 02 Aprili had the same problem with the left button missing an onEvent, just add it to widget_newNavBar.lua after line 71 i think
onEvent = opt.leftButton.onEvent
Sanghyun Lee
Posted at 11:03h, 24 December———————————————————————–
navbar.lua
—————-code start—————————————–
local widget = require( “widget” )
function widget.newNavigationBar( options )
local customOptions = options or {}
local opt = {}
opt.left = customOptions.left or nil
opt.top = customOptions.top or nil
opt.width = customOptions.width or display.contentWidth
opt.height = customOptions.height or 50
if ( customOptions.includeStatusBar == nil ) then
opt.includeStatusBar = true — assume status bars for business apps
else
opt.includeStatusBar = customOptions.includeStatusBar
end
— Determine the amount of space to adjust for the presense of a status bar
local statusBarPad = 0
if ( opt.includeStatusBar ) then
statusBarPad = display.topStatusBarContentHeight
end
opt.x = customOptions.x or display.contentCenterX
opt.y = customOptions.y or (opt.height + statusBarPad) * 0.5
opt.id = customOptions.id
opt.isTransluscent = customOptions.isTransluscent or true
opt.background = customOptions.background
opt.backgroundColor = customOptions.backgroundColor
opt.title = customOptions.title or “”
opt.titleColor = customOptions.titleColor or { 0, 0, 0 }
opt.font = customOptions.font or native.systemFontBold
opt.fontSize = customOptions.fontSize or 18
opt.leftButton = customOptions.leftButton or nil
opt.rightButton = customOptions.rightButton or nil
— If “left” and “top” parameters are passed, calculate the X and Y
if ( opt.left ) then
opt.x = opt.left + opt.width * 0.5
end
if ( opt.top ) then
opt.y = opt.top + (opt.height + statusBarPad) * 0.5
end
local barContainer = display.newGroup()
local background = display.newRect( barContainer, opt.x, opt.y, opt.width, opt.height + statusBarPad )
if ( opt.background ) then
background.fill = { type=”image”, filename=opt.background }
elseif ( opt.backgroundColor ) then
background.fill = opt.backgroundColor
else
background.fill = { 1, 1, 1 }
end
if ( opt.title ) then
local title = display.newText( opt.title, background.x, background.y + statusBarPad * 0.5, opt.font, opt.fontSize )
title:setFillColor( unpack(opt.titleColor) )
barContainer:insert( title )
end
local leftButton
if ( opt.leftButton ) then
if ( opt.leftButton.defaultFile ) then — construct an image button
leftButton = widget.newButton({
id = opt.leftButton.id,
width = opt.leftButton.width,
height = opt.leftButton.height,
baseDir = opt.leftButton.baseDir,
defaultFile = opt.leftButton.defaultFile,
overFile = opt.leftButton.overFile,
onEvent = opt.leftButton.onEvent
})
else — else, construct a text button
leftButton = widget.newButton({
id = opt.leftButton.id,
label = opt.leftButton.label,
onEvent = opt.leftButton.onEvent,
font = opt.leftButton.font or opt.font,
fontSize = opt.fontSize,
labelColor = opt.leftButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } },
labelAlign = “left”,
})
end
leftButton.x = 15 + leftButton.width * 0.5
leftButton.y = title.y
barContainer:insert( leftButton ) — insert button into container group
end
local rightButton
if ( opt.rightButton ) then
if ( opt.rightButton.defaultFile ) then — construct an image button
rightButton = widget.newButton({
id = opt.rightButton.id,
width = opt.rightButton.width,
height = opt.rightButton.height,
baseDir = opt.rightButton.baseDir,
defaultFile = opt.rightButton.defaultFile,
overFile = opt.rightButton.overFile,
onEvent = opt.rightButton.onEvent
})
else — else, construct a text button
rightButton = widget.newButton({
id = opt.rightButton.id,
label = opt.rightButton.label or “Default”,
onEvent = opt.rightButton.onEvent,
font = opt.leftButton.font or opt.font,
fontSize = opt.fontSize,
labelColor = opt.rightButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } },
labelAlign = “right”,
})
end
rightButton.x = display.contentWidth – ( 15 + rightButton.width * 0.5 )
rightButton.y = title.y
barContainer:insert( rightButton ) — insert button into container group
end
return barContainer
end
—————————code end——————————–
In main.lua file, I added
local navbar = require( “navbar” )
then, declared listener and buttons
———————– code start——————-
local function handleLeftButton( event )
if ( event.phase == “ended” ) then
— do stuff
end
return true
end
local function handleRightButton( event )
if ( event.phase == “ended” ) then
— do stuff
end
return true
end
local leftButton = {
onEvent = handleLeftButton,
width = 60,
height = 34,
defaultFile = “icon.png”,
–overFile = “images/backbutton_over.png”
}
local rightButton = {
onEvent = handleRightButton,
label = “Add”,
labelColor = { default = {1, 1, 1}, over = { 0.5, 0.5, 0.5} },
font = “HelveticaNeue-Light”,
isBackButton = false
}
local navBar = widget.newNavigationBar({
title = “Pickle”,
backgroundColor = { 0.96, 0.62, 0.34 },
titleColor = {1, 1, 1},
leftButton = leftButton,
rightButton = rightButton,
includeStatusBar = true
})
——————–code end——————————–
error message is
navbar.lua:79: attempt to index global ‘title'(a nil value) stack tracebak:
so I removed 79 line which is
leftButton.y = title.y
and
rightButton.y = title.y
it works, but y position is not right.
how can I make button’s Y position the same with Title’s Y position?
Rob Miracle
Posted at 11:31h, 25 DecemberPlease ask in the forums. It’s really hard to work with code in the comments.
Yang
Posted at 17:51h, 07 MarchHey Rob,
Great tutorial, I was able to implement this in my app with ease, the only problem(?) I have is I cant manage to change the font size of either buttons or title of the navBar, and the buttons seem to be way bigger than the actual label on the button(if its a label button), so you can tap the title and it will actually read as if you tapped the right button (I tried changing the width of the buttons and it didn’t work, I’m not sure if it was intended to work this way.
Thank you,
-Yang L.
Rob Miracle
Posted at 18:48h, 07 MarchI noticed that recently. I don’t have a solution yet. But then anyone can look at the code in the tutorial and make sure the size is getting passed through.
Yang
Posted at 18:29h, 08 MarchWhat I did to fix it was add
width = opt.rightButton.width
(same goes for left button)to the text button when adding the navBar into the widgets libraby.
I should’ve caught this before posting a comment here, hope this helps anyone who might have had the same problem!
-Yang L.