Posted on by

text-inputOne feature that is commonly requested on our feedback tool is a “widget-based” text input field. Until we can add this feature, 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. The goal is to provide a function that configures a non-native text input field.

Let’s begin with some basic setup code:

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:

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:

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:

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:

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.

Syncing the Elements

If we ever need to move/transition the text input field — a common practice if the field(s) should be shifted upward as the virtual keyboard moves onto the screen — we need to make sure that our text field and UI move together in visual synchronization. The following code synchronizes the native text field with our custom display objects via an enterFrame listener. Note that this will look better in an app running at 60 frames per second versus 30.

local function syncFields( event )
   field.textField.x = field.x
   field.textField.y = field.y
end
Runtime:addEventListener( "enterFrame", syncFields )

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.

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:

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:
local myText = myTextField.textField.text
Set the value of the text field:
myTextField.textField.text = "Edited Value"

Gotchas

  • If you use this in Storyboard scenes, the native.newTextField() doesn’t transition with the rest of the screen. Thus, you must either remove this in the exitScene() or hide the field.textField object.
  • In the OS X Corona Simulator, you’ll see a blue rectangle around the field, but on the device, it behaves as expected.
  • 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.

In Summary

This tutorial should get you started with implementing “styled” text input fields and, hopefully, extending them to more complex use cases. To begin implementing the above code in your projects, please download the Lua module and, as always, contribute feedback and comments below.


Posted by . Thanks for reading...

18 Responses to “Tutorial: Customizing Text Input”

  1. Dean Hodge

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

    Reply
    • Rob Miracle

      Did 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

      Reply
  2. Star Crunch

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

    Reply
    • Rob Miracle

      All 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

      Reply
      • Star Crunch

        Sorry, 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. :)

        Reply
  3. Kerem

    Thanks 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

    Reply
  4. Sean

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

    Reply
    • Rob Miracle

      I 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

      Reply
  5. bobcgausa

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

    Reply
    • Rob Miracle

      We’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.

      Reply
  6. sangu

    widget.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
    }

    Reply

Leave a Reply

  • (Will Not Be Published)