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:
- Button
- Table View
- Stepper
- Spinner
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.
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.
1 2 3 4 5 6 7 |
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
--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.
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.
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 |
--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).
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.
1 2 3 4 5 6 7 8 9 |
--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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
--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:
1 2 3 4 5 6 7 8 |
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:
- defaultFrame — this is the default frame with both the minus and plus sides active.
- noMinusFrame — this frame is used when the stepper reaches its minimum value, indicating no apparent result from a tap on the minus side.
- noPlusFrame — this frame is used when the stepper reaches its maximum value, indicating no apparent result from a tap on the plus side.
- minusActiveFrame — this frame indicates that a tap/hold occurred on the minus side.
- plusActiveFrame — this frame indicates that a tap/hold occurred on the plus side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
--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:
- A single frame that will be rotated.
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
--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.
1 2 3 4 5 6 7 8 |
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.
1 2 3 4 5 6 7 8 9 10 |
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!
Original article:
Tutorial: Stylizing Widgets, Part 1
Comments by Brent Sorrentino