Posted on by

In the Widget 1.0 library, developers could use widget.newTabBar() to create a top-level navigation bar, but only in the sense that it would draw a graphical gradient bar. It had no other features that were useful. In the Widget 2.0 library, a “no-buttons” tab bar is no longer possible, because a tab bar, by basic definition, needs some tabs to interact with.

Of course, 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 “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

With iOS6 and earlier, the nav bar for most apps was a gradient image. In iOS7, it’s either a solid color or a translucent white rectangle. 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.

Setting Up

Like we did in the newTextField tutorial, let’s begin by expanding the core widget library and defining the default parameters:

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. With iOS6, this area is hidden, but with iOS7, we have to consider the area behind it. 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:

   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
      if ( widget.isSeven() ) then
         background.fill = { 1, 1, 1 } 
      else
         background.fill = { type="gradient", color1={ 0.5, 0.5, 0.5 }, color2={ 0, 0, 0 } }
      end
   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 (iOS7) or a gray-to-black gradient fill (iOS6).

Now, let’s add the “title” of the page to the nav bar, if provided (note that some iOS apps don’t use titles):

   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:

   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:

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.

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:

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 — for example, support for image sheets or 9-slice buttons (see documentation). Finally, this example doesn’t support the navigation chevrons (less-than and greater-than symbols), but it could be configured easily enough.

To begin implementing the above code in your projects, please download the demo project and, as always, contribute feedback and comments below.


Posted by . Thanks for reading...

15 Responses to “Tutorial: Extending Widgets with a Navigation Bar”

    • Rob Miracle

      Our 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

      Reply
  1. Dan R.

    Rob,
    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…}

    Reply
    • Rob Miracle

      The 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.

      Reply
  2. tarun

    Hi 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.

    Reply
    • tarun

      File: …corona/examples/project/widget_newNavBar.lua
      Line: 59

      Attempt to call method ‘setFillColor’ (a nil value)

      Reply
    • Rob Miracle

      What 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.

      Reply
      • Dan R,

        Taurn – 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]

        Reply
  3. Archer

    Hi 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

    Reply
    • Rob Miracle

      On 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

      Reply
      • Archer

        Okay, 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.

        Reply
    • Archer

      The 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?

      Reply
      • Rob Miracle

        I 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

        Reply
      • phlo

        i 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

        Reply

Leave a Reply

  • (Will Not Be Published)