03 December 2013
Tutorial: Customizing text input
One commonly requested feature is a “widget-based” text input field and today’s tutorial gives you a foundation to build your own. This isn’t a complete implementation, and it only overcomes a few issues related with native.newTextField(), but it should provide a good starting point.
Let’s begin with some basic setup code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
local myTextField = widget.newTextField( { top = 10, left = 20, width = 200, height = 30, cornerRadius = 8, strokeWidth = 3, backgroundColor = { 1, 1, 1 }, strokeColor = { 0, 0, 0 }, font = "Helvetica", fontSize = 24, listener = textFieldHandler } ) |
Naturally, a widget like this could end up with an enormous number of options, but let’s start by considering some basic options:
- top — distance from the top of the screen
- left — distance from the left of the screen
- x — location of the widget horizontally (center)
- y — location of the widget vertically (center)
- width — width of the field
- height — height of the field
- font — Font to use
- fontSize — size of the font
- backgroundColor — the color behind the text
- strokeColor — the color of the stroke around the field
- strokeWidth — the size of the stroke
- cornerRadius — if you want rounded corners
- text — the initial text
- inputType — the type of keyboard to show
- listener — the input handler
Obviously this could be extended to include things like the value of the “return” button, having placeholder text that disappears when you first start editing the field, and even skinning features. However, since this function is going to use a native text field, we’ll need to write — at the core — a function to handle editing of the field:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
local function textFieldHandler( event ) -- "event.text" only exists during the editing phase to show what's being edited; -- it is NOT the field's ".text" attribute (that is "event.target.text"). if ( event.phase == "began" ) then -- user begins editing textField print( "Begin editing", event.target.text ) elseif ( event.phase == "ended" or event.phase == "submitted" ) then -- do something with defaulField's text print( "Final Text: ", event.target.text ) native.setKeyboardFocus( nil ) elseif ( event.phase == "editing" ) then print( event.newCharacters ) print( event.oldText ) print( event.startPosition ) print( event.text ) end end |
If we want this to be part of the widget library, we simply add it to a module and include the widget library:
1 2 3 4 |
local widget = require( "widget" ) function widget.newTextField( options ) ... end |
This will cause a new function, newTextField()
, to be added to the instance of the widget library that has been included in your project. Then, in any scene or module where you require the widget library, the function will be available.
Now let’s define the default option values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function widget.newTextField( options ) local customOptions = options or {} local opt = {} opt.left = customOptions.left or 0 opt.top = customOptions.top or 0 opt.x = customOptions.x or 0 opt.y = customOptions.y or 0 opt.width = customOptions.width or (display.contentWidth * 0.75) opt.height = customOptions.height or 20 opt.id = customOptions.id opt.listener = customOptions.listener or nil opt.text = customOptions.text or "" opt.inputType = customOptions.inputType or "default" opt.font = customOptions.font or native.systemFont opt.fontSize = customOptions.fontSize or opt.height * 0.67 -- Vector options opt.strokeWidth = customOptions.strokeWidth or 2 opt.cornerRadius = customOptions.cornerRadius or opt.height * 0.33 or 10 opt.strokeColor = customOptions.strokeColor or { 0, 0, 0 } opt.backgroundColor = customOptions.backgroundColor or { 1, 1, 1 } ... |
The code above may seem a bit confusing, but we’re simply accepting a parameter to the function called options
. The first line creates a table named customOptions
which makes sure it’s a Lua table. If you don’t pass the options
parameter, an empty table is created. After that, we just set each of the individual options to either the passed value or a default value. For this widget, things like the corner radius and the font size should default to values that adapt to the size of the field.
Creating the visual elements
In this section, we’ll create the visual part of our text field widget, including the native UI element to pair it with:
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 |
local field = display.newGroup() local background = display.newRoundedRect( 0, 0, opt.width, opt.height, opt.cornerRadius ) background:setFillColor( unpack(opt.backgroundColor) ) background.strokeWidth = opt.strokeWidth background.stroke = opt.strokeColor field:insert( background ) if ( opt.x ) then field.x = opt.x elseif ( opt.left ) then field.x = opt.left + opt.width * 0.5 end if ( opt.y ) then field.y = opt.y elseif ( opt.top ) then field.y = opt.top + opt.height * 0.5 end -- Native UI element local tHeight = opt.height - opt.strokeWidth * 2 if "Android" == system.getInfo("platformName") then -- -- Older Android devices have extra "chrome" that needs to be compesnated for. -- tHeight = tHeight + 10 end field.textField = native.newTextField( 0, 0, opt.width - opt.cornerRadius, tHeight ) field.textField.x = field.x field.textField.y = field.y field.textField.hasBackground = false field.textField.inputType = opt.inputType field.textField.text = opt.text print( opt.listener, type(opt.listener) ) if ( opt.listener and type(opt.listener) == "function" ) then field.textField:addEventListener( "userInput", opt.listener ) end local deviceScale = ( display.pixelWidth / display.contentWidth ) * 0.5 field.textField.font = native.newFont( opt.font ) field.textField.size = opt.fontSize * deviceScale |
Here are a few points to consider as you inspect this code:
- If you round the corners, don’t let the actual text field extend into the corners.
- Remember to hide the native text field background so that your custom visuals show up.
- native.newTextField() needs the font string name converted to a native.newFont(). However, the size is a bit tricky because native text fields do not automatically scale. Thus, we must calculate the device’s real scale factor and then multiply the desired font size by that scale factor.
Removing the elements
We need to make sure that when the widget gets removed, the native text field gets removed as well. Previously, we would need to override the removeSelf() function with our own function that would eventually call the original removeSelf(). However, the new finalize event makes this process easier. This allows us to set up a function that’s executed just before the display object is removed from the stage, in case there are related cleanup tasks we need to handle — in this case, removing the native text field just before our custom text field group is removed.
1 2 3 4 5 6 7 |
function field:finalize( event ) event.target.textField:removeSelf() end field:addEventListener( "finalize" ) return field |
Using the custom text field
Using our new “widget” text field is simple. To place it on screen, our code may look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
local myTextField = widget.newTextField( { width = 250, height = 30, text = "Hello World!", fontSize = 18, font = "HelveticaNeue-Light", listener = textFieldHandler } ) myTextField.x = display.contentCenterX myTextField.y = 100 |
Retrieve the value of the text field
1 |
local myText = myTextField.textField.text |
Set the value of the text field
1 |
myTextField.textField.text = "Edited Value" |
In summary
This tutorial should get you started with implementing “styled” text input fields and, hopefully, extending them to more complex use cases.
Dean Hodge
Posted at 15:34h, 03 DecemberRob, great tutorial but…
I got super excited for a moment thinking this would resolve the biggest issue with textFields, ie the font size scaling on various devices, especially Android.
Every time I do a business app, this one issue always bites me, as with many others.
Hopefully we can get the font size scaling issue resolved soon as it’s been plaguing me and many other developers for a very long time.
Please try and find a solution for this issue.
Chris
Posted at 16:01h, 03 December+1 on the font scaling on devices
Rob Miracle
Posted at 16:03h, 03 DecemberDid you try this? I ran the code on my iPhone 5, iPad 4 and Google Nexus 7 (with a 320×480 base content area) and the font size was consistent from device to device. There is code in there to maintain the scaling.
Rob
Kerem
Posted at 11:54h, 05 DecemberJust tried the code on my Android 2.2 Samsung Galaxy S. The font size issue is still there. Not sure if this is due to the real old OS revision.
Rob Miracle
Posted at 17:32h, 05 DecemberCan you reply to this on the forum post and provide a screen shot?
Star Crunch
Posted at 15:03h, 04 DecemberI seem to remember having to explicitly removeSelf() widgets… have those been rewritten with finalize in mind? That is, can I get rid of that handling code?
Rob Miracle
Posted at 15:20h, 04 DecemberAll display objects need to be explicitly removed and nil’ed with the exception of Storyboard where display objects in the scene’s view are removed for you when the scene is destroyed.
Rob
Star Crunch
Posted at 16:57h, 04 DecemberSorry, I wasn’t precise enough; that probably sounded like a bit of a trivial question. I meant is removeSelf() still necessary for widgets versus, say, group:remove()’ing or removing the parent group, on account of the logic being moved into the finalizer. At least, I’d assumed those don’t invoke the (possibly overridden) removeSelf() method…
Of course, maybe I’ll test and see my assumptions were wrong all along. 🙂
Kerem
Posted at 00:14h, 05 DecemberThanks for a great tutorial. This is a great starting point for what I hope will evolve into a formal Corona Labs widget.newTextField() soon.
Question – how can we extend the functionality so that if a new parameter (ie eraseInitialText) is set to true then the initially supplied text is erased when user first taps into this input field? I am able to do this in the began field of my listener function but I would like to push this code into the widget extension code so it can be reused. Is this possible? Thanks
Rob Miracle
Posted at 17:30h, 05 DecemberI responded in the forum thread for this: http://forums.coronalabs.com/topic/41661-widget-for-text-input/
Please direct responses there.
Thanks
Rob
Sean
Posted at 06:54h, 05 DecemberHey Rob, will these text fields scroll in a widget ScrollView now? That has always been a big issue for me; getting the text fields to scroll with the rest of the screen. Is there any sort of time frame on when this will be implemented in the actual widget code base?
Rob Miracle
Posted at 16:31h, 06 DecemberI didn’t try it, but it should. The sync code keeps the native text field wherever the object is. Depending on how fast you scroll the scrollView there may be some lag while the system moves the native.newTextField.
Rob
Kawika
Posted at 10:03h, 08 DecemberAnyone using “Text Candy” by X-PRESSIVE.COM?
Kawika
Posted at 12:35h, 08 DecemberAnyone using “Widget Candy” by X-PRESSIVE.COM?
bobcgausa
Posted at 06:49h, 15 Januaryi use corona to teach students who use the simulator on mac or windows and who do not necessarily have a droid.
“Since this module still utilizes native text fields, it will draw the widget background on the Windows Corona Simulator, but the text field will not function.”
Really, how hard can it be to have a single line text box that is multi-platform?
This is a huge restriction for classroom use.
Rob Miracle
Posted at 17:51h, 15 JanuaryWe’ve explained this on many occasions. Corona SDK is based on OpenGL. On OS-X, iOS and Android, the OS’s permit native objects and the OpenGL canvas to occupy the same window. On Windows, they do not permit native objects and OpenGL objects to co-exist in the same window. Mircosoft lets this happen for DirectX based apps, but we are an OpenGL based app.
sangu
Posted at 01:07h, 17 Septemberwidget.newTextField({}) is showing error in my code, while referencing to widget.
does it support in pro-corona or i m doing something wrong in code?
local widget = require(“widget”)
local myTextField = widget.newTextField(
{
top = 10,
left = 20,
width = 200,
height = 30,
cornerRadius = 8,
strokeWidth = 3,
backgroundColor = { 1, 1, 1 },
strokeColor = { 0, 0, 0 },
font = “Helvetica”,
fontSize = 24
–listener = textFieldHandler
}
Rob Miracle
Posted at 17:28h, 17 SeptemberPlease ask this this in the forums.
Thanks
Rob
Julius Bangert
Posted at 06:21h, 08 SeptemberIs there any module or plugin anyone has come up which that offers typeahead / autocomplete functionality?
So as the user types the first few letters, something in the UI near the text input suggests terms pulled from a lua table?
This would be so good.