Posted on by

NOTE: Widget stylizing has been merged into the respective widget documentation, along with full code examples. All future updates and available options pertaining to widget stylizing will be made to these core API pages. Please link onward to the desired widget documentation:


This week’s tutorial covers how to visually stylize widgets. Although widgets will adopt the OS-like appearance by default, in many cases you’ll want to customize the appearance to suit the style of your app.

This part of the series (Part 1) covers the following widgets:

IMPORTANT: some of the styling exhibited in this tutorial is available only in recent Daily Builds of Corona SDK. If you’re a Pro subscriber, please download the latest build now. If you’re a Starter user, all of these styling options will be included in the next public build of Corona.

NOTE: this tutorial is not about widget functionality — the following examples are focused on visual styling only.

Button

Buttons can be created with the widget.newButton() API. You may construct buttons using one of three methods.

2 Image Files

This is the most simple button to construct. Just create two image files, one for the default state and another for the over state.

widget-button-file

Next, specify the files as defaultFile and overFile respectively. Don’t forget to include the directory path if the files are located inside a subfolder.

local myButton = widget.newButton
{
   width = 240,                        --width of the image file(s)
   height = 120,                       --height of the image file(s)
   defaultFile = "buttonDefault.png",  --the "default" image file
   overFile = "buttonOver.png",        --the "over" image file
   label = "2-file"
}

2 Frame (ImageSheet)

This method uses two frames from an image sheet, one frame each for the default and over states.

widget-button-file

For this method, include a reference to the image sheet in the sheet parameter. Then specify the frame numbers from the image sheet as defaultFrame and overFrame respectively:

--image sheet options and declaration
local options = {
   width = 240,
   height = 120,
   numFrames = 2,
   sheetContentWidth = 480,
   sheetContentHeight = 120
}
local buttonSheet = graphics.newImageSheet( "buttonSheet.png", options )

local myButton = widget.newButton
{
   width = 240,          --width of the button
   height = 120,         --height of the button
   sheet = buttonSheet,  --reference to the image sheet
   defaultFrame = 1,     --number of the "default" frame
   overFrame = 2,        --number of the "over" frame
   label = "2-frame"
}

9-Slice (ImageSheet)

This method uses 9 slices from an image sheet which are assembled internally to create flexible-sized buttons. As indicated in the following image, the 9 slices consist of the 4 corners (red), the 2 horizontal sides (green), the 2 vertical sides (yellow), and the middle fill.

widget-button-9slice

Depending on the size of your button, the corners will remain at the size stated in the image sheet, but sides and middle will stretch to fill the entire width and height.

Remember that you’ll need 18 slices to construct the entire button: 9 slices each for the default and over states. While this requires more initial effort, the benefit is that sliced buttons can be set to virtually any size and still use the same image assets.

--image sheet options and declaration
local options = {
   frames =
   {
      { x=0, y=0, width=21, height=21 },
      { x=21, y=0, width=198, height=21 },
      { x=219, y=0, width=21, height=21 },
      { x=0, y=21, width=21, height=78 },
      { x=21, y=21, width=198, height=78 },
      { x=219, y=21, width=21, height=78 },
      { x=0, y=99, width=21, height=21 },
      { x=21, y=99, width=198, height=21 },
      { x=219, y=99, width=21, height=21 },
      { x=240, y=0, width=21, height=21 },
      { x=261, y=0, width=198, height=21 },
      { x=459, y=0, width=21, height=21 },
      { x=240, y=21, width=21, height=78 },
      { x=261, y=21, width=198, height=78 },
      { x=459, y=21, width=21, height=78 },
      { x=240, y=99, width=21, height=21 },
      { x=261, y=99, width=198, height=21 },
      { x=459, y=99, width=21, height=21 }
   },
   sheetContentWidth = 480,
   sheetContentHeight = 120
}
local buttonSheet = graphics.newImageSheet( "buttonSheet.png", options )

local myButton = widget.newButton
{
   width = 340,          --flexible width of the 9-slice button
   height = 100,         --flexible height of the 9-slice button
   sheet = buttonSheet,  --reference to the image sheet
   topLeftFrame = 1,     --number of the "top left" frame
   topMiddleFrame = 2,   --number of the "top middle" frame
   topRightFrame = 3,    --etc.
   middleLeftFrame = 4,
   middleFrame = 5,
   middleRightFrame = 6,
   bottomLeftFrame = 7,
   bottomMiddleFrame = 8,
   bottomRightFrame = 9,
   topLeftOverFrame = 10,
   topMiddleOverFrame = 11,
   topRightOverFrame = 12,
   middleLeftOverFrame = 13,
   middleOverFrame = 14,
   middleRightOverFrame = 15,
   bottomLeftOverFrame = 16,
   bottomMiddleOverFrame = 17,
   bottomRightOverFrame = 18,
   label = "9-slice"
}

Additional Button Styling

In addition to the three core construction methods, all buttons share the following visual properties:

  • width and height — sets the width and height of the button. If you’re using 2 separate image files for the button, set these values to the width and height of the image. If you’re using the 9-slice method, you may set any width/height and the button will resize accordingly. If you’re using 2 frames from an image sheet, these values are optional since the size is inherited from the sheet options.
  • label — the text label that will appear on top of the button.
  • labelAlign — specifies the alignment of the button label. Valid options are left, right or center. Default is center.
  • labelColor — a table of two RGB+A color settings, one each for the default and over states. Please see the documentation for details.
  • labelXOffset and labelYOffset — optional x/y offsets for the button label. For example, labelYOffset = -8 will shift the label 8 pixels up from default.
  • font — the font used for the button label. Default is native.systemFont.
  • fontSize — the font size (in pixels) for the button label. Default is 14.
  • emboss — if set to true, the button label will appear embossed (inset effect). Default is true.
  • textOnly — if set to true, the button will be constructed via a text object only (no background element). Default is false.

Table View

The table view widget is created with the widget.newTableView() API. Table views that have more content than can be shown in the boundaries can display a scroll bar indicating how far down you’ve scrolled. Customizing this scroll bar requires 3 images in an image sheet. Each frame needs to be a square of equal size. These frames represent the top of the bar (red), the middle of the bar (green), and the bottom of the bar (yellow).

widget-scrollbar-final

Here, the top and bottom frames are the “caps” of the scroll bar. The middle frame will resize to a variable height depending on the overall height of the table view and the number of items to scroll through — this mimics the scroll bar on many Mac OSX apps.

--image sheet options and declaration
local options = {
   width = 10,
   height = 10,
   numFrames = 3,
   sheetContentWidth = 10,
   sheetContentHeight = 30
}
local scrollBarSheet = graphics.newImageSheet( "scrollBar.png", options )

Once the image sheet is declared, simply pass a table named scrollBarOptions to the table view declaration with four parameters: sheet, topFrame, middleFrame, and bottomFrame:

--create the base table view
local tableView = widget.newTableView
{
   height = display.contentHeight,
   width = display.contentWidth,
   onRowRender = createRow,    --function called when a row is inserted
   scrollBarOptions = {
      sheet = scrollBarSheet,  --reference to the image sheet
      topFrame = 1,            --number of the "top" frame
      middleFrame = 2,         --number of the "middle" frame
      bottomFrame = 3          --number of the "bottom" frame
      }
}

--insert 40 rows into the table view
for i = 1,40 do
   tableView:insertRow
   {
      isCategory = false,
      rowHeight = 32,
      rowColor = { default = { 255, 255, 255 } },
      lineColor = { 220, 220, 220 }
   }
end

Additional Table View Styling

In addition to a custom scroll bar, the table view allows for these visual properties:

  • width and height — sets the width and height of the table view. Remember that if you set these to a value smaller than the device screen, you should construct a mask to contain the bounds of the widget. See the documentation for details.
  • backgroundColor — a table of RGB+A color settings for the table view background. Default is white.
  • hideBackground — if set to true, the background of the table view will be hidden but still receive touches.
  • topPadding and bottomPadding — the number of pixels from the top and bottom of the table view in which scrolling will stop when it reaches the top or bottom of scrollable area. The default value for both is 0.
  • noLines — if set to true, lines will not separate individual rows. The default value is false.
  • hideScrollBar — if set to true, no scroll bar will appear in the table view. Default is false.

Row Customization

Table view rows are “rendered” and visual content is inserted using the tableView:insertRow{} function. To accomplish this, specify a listener function in the onRowRender parameter of the table view declaration. Next, write the rendering function similar to this:

local function createRow( event )
   local phase = event.phase
   local row = event.row
   local rowTitle = display.newText( row, "Row " .. row.index, 0, 0, nil, 14 )
   rowTitle.x = row.x - ( row.contentWidth * 0.5 ) + ( rowTitle.contentWidth * 0.5 )
   rowTitle.y = row.contentHeight * 0.5
   rowTitle:setTextColor( 0, 0, 0 )
end

Stepper

The stepper widget is created with the widget.newStepper() API. This consists of a minus and plus button which can be tapped or held down to decrement/increment a value, for example, the music or sound volume setting in a game.

Visually, this widget uses 5 frames from an image sheet as follows:

widget-stepper-final
  1. defaultFrame — this is the default frame with both the minus and plus sides active.
  2. noMinusFrame — this frame is used when the stepper reaches its minimum value, indicating no apparent result from a tap on the minus side.
  3. noPlusFrame — this frame is used when the stepper reaches its maximum value, indicating no apparent result from a tap on the plus side.
  4. minusActiveFrame — this frame indicates that a tap/hold occurred on the minus side.
  5. plusActiveFrame — this frame indicates that a tap/hold occurred on the plus side.
--image sheet options and declaration
local options = {
   width = 196,
   height = 100,
   numFrames = 5,
   sheetContentWidth = 980,
   sheetContentHeight = 100
}
local stepperSheet = graphics.newImageSheet( "stepperSheet.png", options )

local newStepper = widget.newStepper
{
   width = 196,           --width of the stepper
   height = 100,          --height of the stepper
   sheet = stepperSheet,  --reference to the image sheet
   defaultFrame = 1,      --number of the "default" frame
   noMinusFrame = 2,      --number of the "noMinus" frame
   noPlusFrame = 3,       --etc.
   minusActiveFrame = 4,
   plusActiveFrame = 5
}

Spinner

The spinner widget is created with the widget.newSpinner() API. You may construct a spinner using one of two methods, both of which utilize an image sheet:

  1. A single frame that will be rotated.
  2. A multi-frame animation that will be cycled.

Single Frame

This method utilizes one frame from an image sheet and rotates it to a delta angle defined by deltaAngle on the defined time increment of incrementEvery.

widget-spinner-single
--image sheet options and declaration
local options = {
   width = 128,
   height = 128,
   numFrames = 1,
   sheetContentWidth = 128,
   sheetContentHeight = 128
}
local spinnerSingleSheet = graphics.newImageSheet( "spinnerSingleSheet.png", options )

local spinner = widget.newSpinner
{
   width = 128,
   height = 128,
   sheet = spinnerSingleSheet,  --reference to the image sheet
   deltaAngle = 10,             --rotate 10 degrees each increment
   incrementEvery = 20          --rotate every 20 milliseconds
}
spinner:start()

Multi-Frame Animation

Animated (multi-frame) spinners can be done using more complex image sheets. By definition, a spinner should spin, but you can use this method to create other kinds of “process underway” animations.

widget-spinner-multi
local options = {
   width = 128,
   height = 128,
   numFrames = 8,
   sheetContentWidth = 1024,
   sheetContentHeight = 128
}
local spinnerMultiSheet = graphics.newImageSheet( "spinner-multi.png", options )

Once the image sheet is declared, create the spinner with three control parameters: startFrame, count, and time.

local spinner = widget.newSpinner
{
   width = 128,
   height = 128,
   sheet = spinnerMultiSheet,  --reference to the image sheet
   startFrame = 1,             --starting animation frame
   count = 8,                  --total frames in animation series
   time = 1600                 --time (milliseconds) to complete the full animation
}
spinner:start()

This concludes Part 1 of the “Stylizing Widgets” series. Keep an eye out for Part 2, coming soon!


Posted by . Thanks for reading...

14 Responses to “Tutorial: Stylizing Widgets, Part 1”

  1. Kenny

    Is there a way to change the defaultFrame on the widget button? So for example, if I have a sprite sheet of 4 buttons (frame 1 being the default). When they release the button I want to switch to ether frame 3 or 4 but I can’t seem to get it working.

    (I’m probably missing something very basic).

    I did try setting it after I initialized the button by doing button.defaultFrame = 3 but that doesn’t seem to change it.

    Any ideas or feedback would be awesome!

    Reply
    • Brent Sorrentino

      Hi Kenny,
      At this time, it’s not possible to change the default image to something other than what you created the button with (its starting default). This feature may be added in the future, but at this point you would need to recreate the button.

      Brent

      Reply
      • Kenny

        I remembered to come back and post the solution I’m using incase anyone else is looking for a solution. I created the button widget with a defaultFrame and overFrame. To then get the button to either show red or green (right or wrong button picked) I create a temp button right on top which looks like this:

        local function buttonPress(event)
        local btn = event.target

        –Get the clicked button label, and set it on the tempOptions (used to define original buttons)
        btnWidgetOptions.label = btn:getLabel()
        btnWidgetOptions.defaultFrame = 3 –or whatever frame you need

        –Define a new buttonWidget
        –Use current btn.y to set tempBtn.y
        tempBtn = widget.newButton(btnWidgetOptions)
        tempBtn:setReferencePoint(display.TopCenterReferencePoint)
        tempBtn.x = halfW
        tempBtn.y = btn.y

        –Insert back into original display group
        card:insert(tempBtn)
        end

        It’s one of the solutions that I tried and it appears to be working perfectly. Hopefully it will be a good starting point for anybody else.

        Reply
  2. Olivier

    Hi :)
    Thanks for this tutorial Brent!
    2 question :

    - If I have 3 imagesSheet, one for each resolutions ( normal, @2x, @4x)
    which “width” and “height” should I put in the buttons parameters? The smallest sizes from normal image sheet or the biggest sizes from @4x image sheet

    - In your example you use one imageSheet for one button. Could I use the same imageSheet with all interface stuffs for all my buttons? What is the best way to do?

    Thanks
    Olivier

    Reply
    • Brent Sorrentino

      Hi Olivier,
      The width and height parameters should be the 1x size, in accordance to how you’ve set up your config.lua.

      Yes, you can put many button graphic images onto one sheet. Just reference the same sheet for different buttons, but specify the frames that are used for that specific button.

      Hope this helps,
      Brent

      Reply
  3. Olivier

    Hi,

    Thanks for this tutorial Brent!
    It would be very useful to have an example with a real image sheet and all the slices on it for the 9-Slice (ImageSheet) button’s example.
    As it is, it’s too theorical for me, and I don’t understand for example how should look like the middle fill image…
    Thanks for your help :)
    Olivier

    Reply
    • Brent Sorrentino

      Hi Olivier,
      I’m glad you’re finding this useful. Although it may not appear so, the 9-slice sample is, in fact, an image sheet (it just looks like the actual button, but it’s a sheet). Just remember that the middle fill should be a texture/image which can “stretch” to whatever size you make your buttons… same with the side edges. The corners will not stretch/deform if you change the button size, but the other elements will.

      Hope this helps,
      Brent

      Reply

Leave a Reply

  • (Will Not Be Published)