TextInputLayout En Android: Material Design

El TextInputLayout hace parte de la lista de componentes presentados en el Material Design con el fin de estilizar los campos de texto con etiquetas flotantes.

Este presenta una animación del texto auxiliar que poseen los edit texts hacia la parte superior para crear un comportamiento práctico entre el significado del campo de texto y su contenido.

Además permite asignar mensajes de error al momento de validar el formato en su interior.

Descargar Proyecto En Android Studio De Etiquetas Flotantes

Este tutorial está basado en una app que contendrá todos los conocimientos que vayas obteniendo.

Si quieres desbloquear el link de descarga del código, sigue estas instrucciones:

Etiquetas Flotantes En Material Design

El nuevo esquema de diseño de Google exige que cuando el usuario establezca el foco en un campo de texto, su hint debe flotar hacia la parte superior, proporcionando espacio para el texto. Esto asegura que el usuario nunca pierda el contexto del contenido que está digitando.

Ejemplo TextInputLayout En Android

Para el texto de la entrada y el hint usa tamaños de 16sp. En el caso de la etiqueta se asigna el estilo caption de 12sp. En total el área completa mide 72dp, contando el padding entre los componentes.

Especificaciones para TextInputLayout En Material Design

Gracias a la librería de soporte para diseño, es posible crear etiquetas flotantes a través del componente TextInputLayout. Este actúa como una envoltura del widget EditText (o sus descendientes) con el fin de crear la etiqueta.

A continuación verás un ejemplo donde crearé un formulario para añadir un nuevo cliente hipotético en una app. Este consta de tres campos de texto para el nombre, el teléfono y el correo. La idea es asignar una etiqueta flotante a cada uno y mostrar los posibles comportamientos del TextInputLayout.

1. Uso Del TextInputLayout

Paso 1: Crear Proyecto en Android Studio

Entra a Android Studio y ve a File > New > New Project… para crear un nuevo proyecto llamado “Etiquetas Flotantes”. Asigna las siguientes características:

  • Versión mínima de SDK: 11
  • Blank Activity como actividad principal.
  • Activity Name: ActividadPrincipal
  • Layout Name: actividad_principal
  • Title: Actividad Principal
  • Menu Resource Name: menu_actividad_principal

Luego abre tu layout actividad_principal.xml y elimina el floating action button. De la misma forma en la clase Java, elimina la referencia del fab para la asignación de la escucha.

La clase quedaría así:

ActividadPrincipal.java

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;


public class ActividadPrincipal extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_principal);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

    }

}

El layout de la actividad se vería así:

actividad_principal.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.herprogramacion.etiquetasflotantes.ActividadPrincipal">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/contenido_actividad_principal" />


</android.support.design.widget.CoordinatorLayout>

Y el segmento central contenido_actividad_principal.xml puedes dejarlo con su contenido predeterminado

Paso 2: Añadir Design Support Library

Abre tu archivo build.gradle del módulo principal y añade las librerías para soporte de versiones y la librería de diseño.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
}

Paso 3: Cambia Paleta de Colores

Los colores que se usará para la app se basan en una paleta principal con púrpura 700 para los oscuros y púrpura en tono 500 para el color principal. La paleta de acento será ocupada por el tono 400 de Rosa como se muestra en la herramienta Material Design Colors:

Paletas Púrpura y Rosa en Material Design

El archivo colors.xml quedaría con los siguientes valores:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#7B1FA2</color>
    <color name="colorPrimaryDark">#AB47BC</color>
    <color name="colorAccent">#EC407A</color>
</resources>

Paso 4: Diseñar UI de la Actividad

El formulario de inserción tiene varias componentes a ubicar cómo muestra el siguiente screenshot de la app.

App Android con Etiquetas Flotantes

Se tienen tres edit texts recubiertos por su respectivo TextInputLayout. Al lado izquierdo existe una imagen representativa del contenido de cada campo de texto. Y al final existe una bottom bar con dos botones para cancelar o confirmar la inserción del cliente.

Al campo de texto del nombre asigna un inputType del tipo text. Para el teléfono usa el tipo phone y para el correo el tipo textEmailAdress.

contenido_actividad_principal.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.herprogramacion.etiquetasflotantes.ActividadPrincipal"
    tools:showIn="@layout/actividad_principal">

    <LinearLayout
        android:id="@+id/area_nombre"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/img_cliente"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:src="@drawable/ic_cliente" />

        <android.support.design.widget.TextInputLayout
            android:id="@+id/til_nombre"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="32dp">

            <EditText
                android:id="@+id/campo_nombre"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/hint_nombre"
                android:inputType="text"
                android:singleLine="true" />
        </android.support.design.widget.TextInputLayout>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/area_telefono"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/area_nombre"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/img_correo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:src="@drawable/ic_telefono" />

        <android.support.design.widget.TextInputLayout
            android:id="@+id/til_telefono"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="32dp">

            <EditText
                android:id="@+id/campo_telefono"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/hint_telefono"
                android:inputType="phone" />
        </android.support.design.widget.TextInputLayout>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/area_correo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/area_telefono"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/img_telefono"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/img_correo"
            android:layout_column="0"
            android:layout_gravity="center_vertical"
            android:layout_row="2"
            android:src="@drawable/ic_correo" />

        <android.support.design.widget.TextInputLayout
            android:id="@+id/til_correo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="32dp">

            <EditText
                android:id="@+id/campo_correo"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/hint_correo"
                android:inputType="textEmailAddress"
                android:singleLine="true" />
        </android.support.design.widget.TextInputLayout>
    </LinearLayout>

    <!-- Bottom Bar -->
    <LinearLayout
        android:id="@+id/bottom_bar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_alignParentBottom="true"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <Button
            android:id="@+id/boton_cancelar"
            style="@style/Widget.AppCompat.Button.Borderless.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/accion_cancelar" />

        <Button
            android:id="@+id/boton_aceptar"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:backgroundTint="@color/colorAccent"
            android:text="@string/accion_aceptar"
            android:textColor="@android:color/white" />
    </LinearLayout>


</RelativeLayout>

Cómo ves, el TextInputLayout solo necesita encerrar al edit text al cual le proveerá la etiqueta.

<android.support.design.widget.TextInputLayout
    android:id="@+id/til_nombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="32dp">

    <EditText
        android:id="@+id/campo_nombre"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="@string/hint_nombre"
        android:inputType="text"
        android:singleLine="true" />
</android.support.design.widget.TextInputLayout>

2. Setear Errores en el TextInputLayout

El TextInputLayout tiene la capacidad para mostrar errores asociados al contenido digitado por usuario. Estos pueden ser debido a un formato incorrecto, una cantidad mínima de caracteres no satisfecha, algún carácter indebido, etc.

El ejemplo actual mostrará los errores necesarios luego de que se validen los campos de texto, al presionar el botón GUARDAR de la bottom bar.

Paso 1: Implementar OnClickListener en el Botón

Dentro de ActividadPrincipal.java obtén la instancia del botón con la acción de guardado para tener su referencia. Luego asigna con setOnClickListener() una nueva escucha anónima. Sobrescribe su controlador onClick() y deja expresado que deseas ejecutar un método futuro llamado validarCampos().

Button botonAceptar = (Button)findViewById(R.id.boton_aceptar);
botonAceptar.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // validarDatos()
    }
});

Paso 2: Validación de Datos

Validar los campos de tus edit texts depende directamente de las reglas de negocio de tu aplicación. Normalmente esto va definido en el diccionario de datos si es que tienes una base de datos local o remota.

Supongamos que este ejemplo existen las siguientes reglas:

  • Nombre: Solo caracteres alfabéticos con un tamaño máximo de 30.
  • Teléfono: Solo caracteres numéricos con un tamaño estándar de 7.
  • Email: Secuencia de caracteres que cumplan la sintaxis de correos electrónicos.

Validación del nombre del cliente — Con valor text de inputText no es posible limitar el campo de texto, ya que este filtro permite caracteres alfanuméricos. Una de las formas de hacerlo es a través de la clase Pattern, la cual contiene métodos para el uso de expresiones regulares.

La expresión regular para aceptar caracteres alfabéticos y espacios es:

^[a-zA-Z ]+$

Con ello, crea un nuevo método llamado validarNombre() y haz que retorne en boolean para usarlo como determinante de paso en el flujo de la app.

private boolean validarNombre(String nombre){
    Pattern patron = Pattern.compile("^[a-zA-Z ]+$");
    return patron.matcher(nombre).matches() || nombre.length() > 30;
}

Validación del teléfono — El tipo de entrada para este campo de texto te ayuda a limitar al usuario al ingreso de caracteres relacionados con un número de teléfono.

Por otro lado, puedes usar el patrón predefinido Patterns.PHONE para una validación adicional de la sintaxis del número. Añade esta lógica a un nuevo método llamado validarTelefono().

private boolean validadTelefono(String telefono){
    return Patterns.PHONE.matcher(telefono).matches();
}

Validación de correo electrónico — Al igual que el caso anterior, la clase de utilidades Patterns contiene un patrón para el correo electrónico con el nombre de EMAIL_ADDRESS. Así que añade un nuevo método de validación para email con el siguiente cuerpo.

private boolean esCorreoValido(String correo){
    return Patterns.EMAIL_ADDRESS.matcher(correo).matches();
}

Paso 3: Mostrar Errores en el TextInputLayout

El método setError() se usa para mostrar los errores en la parte inferior del EditText a través del TextInputLayout. También puedes pasar null como parámetro para limpiar los errores.

Teniendo en cuenta estos métodos, dirígete a cada método de validación y despliega un error si el formato no es favorable, de lo contrario límpialo.

ActividadPrincipal.java

import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Patterns;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.regex.Pattern;

public class ActividadPrincipal extends AppCompatActivity {

    private TextInputLayout tilNombre;
    private TextInputLayout tilTelefono;
    private TextInputLayout tilCorreo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_principal);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // Referencias TILs
        tilNombre = (TextInputLayout) findViewById(R.id.til_nombre);
        tilTelefono = (TextInputLayout) findViewById(R.id.til_telefono);
        tilCorreo = (TextInputLayout) findViewById(R.id.til_correo);

        // Referencia Botón
        Button botonAceptar = (Button) findViewById(R.id.boton_aceptar);
        botonAceptar.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                validarDatos();
            }
        });


    }

    private boolean esNombreValido(String nombre) {
        Pattern patron = Pattern.compile("^[a-zA-Z ]+$");
        if (!patron.matcher(nombre).matches() || nombre.length() > 30) {
            tilNombre.setError("Nombre inválido");
            return false;
        } else {
            tilNombre.setError(null);
        }

        return true;
    }

    private boolean esTelefonoValido(String telefono) {
        if (!Patterns.PHONE.matcher(telefono).matches()) {
            tilTelefono.setError("Teléfono inválido");
            return false;
        } else {
            tilTelefono.setError(null);
        }

        return true;
    }

    private boolean esCorreoValido(String correo) {
        if (!Patterns.EMAIL_ADDRESS.matcher(correo).matches()) {
            tilCorreo.setError("Correo electrónico inválido");
            return false;
        } else {
            tilCorreo.setError(null);
        }

        return true;
    }

    private void validarDatos() {
        String nombre = tilNombre.getEditText().getText().toString();
        String telefono = tilTelefono.getEditText().getText().toString();
        String correo = tilCorreo.getEditText().getText().toString();

        boolean a = esNombreValido(nombre);
        boolean b = esTelefonoValido(telefono);
        boolean c = esCorreoValido(correo);

        if (a && b && c) {
            // OK, se pasa a la siguiente acción
            Toast.makeText(this, "Se guarda el registro", Toast.LENGTH_LONG).show();
        }

    }

}

[alert-success]Obtén el EditText del TextInputLayout con el método getEditText().[/alert-success]

Si las tres condiciones se cumplieron, entonces puedes proceder a guardar el registro hipotético.

Paso 4: Añadir Text Watchers a los Edit Texts

Si requieres la comprobación en tiempo real del texto que contiene un EditText, entonces asigna una escucha TextWatcher con el método addTextChangedListener().

Por ejemplo…

Si quieres que los errores se limpien al momento de escribir una caracter en el campo del nombre, usa setError(null) en el controlador onTextChanged().

// Referencias ETs
campoNombre = (EditText) findViewById(R.id.campo_nombre);
campoTelefono = (EditText) findViewById(R.id.campo_telefono);
campoCorreo = (EditText) findViewById(R.id.campo_correo);

campoNombre.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        tilNombre.setError(null);
    }

    @Override
    public void afterTextChanged(Editable s) {
        
    }
});

Si quieres comprobar el campo del correo cada vez que se escriba, entonces llama al método de validación en onTextChanged().

campoCorreo.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        esCorreoValido(String.valueOf(s));
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
});

3. Personalizar Etiquetas Flotantes

El elemento TextInputLayout en su definición XML te permite modificar el comportamiento de las etiquetas flotantes y los errores.

La siguiente tabla muestra algunos de ellos:

Atributo Descripción
app:counterEnabled Determina si se mostrará un contador de caracteres para el contenido del EditText. Acepta los valores true y false.
app:errorEnabled Habilita o deshabilita la visibilidad de los errores en la parte inferior del EditText. Acepta true y false. También proporciona un espacio en blanco adicional en el layout previsualizado
app:hintAnimationEnabled  Habilita o deshabilita la animación de la etiqueta. Si usas false, la etiqueta se moverá bruscamente hacia arriba sin la transición que tiene por defecto.
app:hintTextAppearance  Cambia el estilo del texto de la etiqueta flotante.
 android:hint Texto auxiliar que se muestra dentro del campo de texto. Puedes declararlo en el TextInputLayout o en el EditText, ambos producen los mismos resultados.

Veamos algunos ejemplos para personalizar el TextInputLayout:

Cambiar el color del error — Usa el atributo app:errorTextAppearance para asignar un estilo propio que extienda de otro que esté prefabricado por el sistema como @android:style/TextAppearance.

El siguiente estilo asigna un color naranja al error:

styles.xml

<style name="Error" parent="TextAppearance.AppCompat.Caption">
    <item name="android:textColor">#FF9800</item>
</style>

TIL del nombre

<android.support.design.widget.TextInputLayout
    android:id="@+id/til_nombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_column="1"
    android:layout_marginLeft="32dp"
    app:errorTextAppearance="@style/Error"
    android:layout_row="0">

El resultado:

Cambiar color del error en TextInputLayout

Cambiar color del hint — Puedes cambiar el color la etiqueta con el atributo app:hintTextAppearance. Al igual que la apariencia del error, solo extiéndelo de un estilo asociado al texto como TextAppearance.Design.HintTextAppearance.AppCompat.Caption.

El siguiente ejemplo cambia al color primario la etiqueta:

styles.xml

<style name="Hint" parent="TextAppearance.Design.Hint">
    <item name="android:textColor">?attr/colorPrimary</item>
</style>

Campo del correo

<android.support.design.widget.TextInputLayout
    android:id="@+id/til_correo"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_column="1"
    app:errorEnabled="true"
    android:layout_marginLeft="32dp"
    app:hintTextAppearance="@style/Hint"
    android:layout_row="2">

El resultado será:

Cambiar color hint del textinputlayout

Recuerda que la apariencia del texto también incluye su tamaño (textSize), su formato (textStyle), etc.

Añadir un contador al TextInputLayout — Si deseas contar los caracteres del text input layout usa los atributos app:counterEnabled y app:counterMaxLenght.

El primero habilita la visualización del contador y el segundo establece el límite de caracteres al cual tendrá acceso el usuario. Adicionalmente el input layout cambiará de color el contador y la línea inferior cuando se sobrepase el contador.

Por ejemplo, pongamos un contador con límite de 30 caracteres en el campo del nombre.

<android.support.design.widget.TextInputLayout
    android:id="@+id/til_nombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="32dp"
    android:orientation="horizontal"
    app:counterEnabled="true"
    app:counterMaxLength="30"
    app:errorEnabled="true">

El resultado sería:

Contador en TextInputLayout

¿Cómo establecer el color del contador en el TextInputLayout?

Lo harás con dos atributos que te otorgan personalización: app:counterTextAppearanceapp:counterOverflowTextAppearance.

El primero asigna un estilo para el contador mientras la longitud se encuentre en el rango máximo. Y el segundo es para cuando se traspasa dicho límite.

Por ejemplo…

Crearé dos estilos para el contador donde cambiaré los tonos a naranja profundo para el desborde y azul para el estado normal.

styles.xml

<style name="Counter" parent="TextAppearance.Design.Counter">
    <item name="android:textColor">#42A5F5</item>
</style>

<style name="CounterOverFlow" parent="TextAppearance.Design.Counter.Overflow">
    <item name="android:textColor">#FF7043</item>
</style>

Ahora se los asigno al campo del nombre:

<android.support.design.widget.TextInputLayout
    android:id="@+id/til_nombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="32dp"
    android:orientation="horizontal"
    app:counterEnabled="true"
    app:counterMaxLength="30"
    app:counterOverflowTextAppearance="@style/CounterOverFlow"
    app:counterTextAppearance="@style/Counter"
    app:errorEnabled="true">

Al final tendrías:

Cómo establecer el color del contador de un TextInputLayout

Nota: Si quieres jugar más con el estilo del TextInputLayout, te dejo este link para que pruebes la librería MaterialEditText, donde encontrarás todo tipo de estilos personalizados.

Conclusión

Las etiquetas flotantes son excelentes.

No hay duda que realzan la visualización de los campos de texto y además reducen la complejidad de ubicar un TextView para determinar la función de un EditText.

Incluyendo que te permiten setear errores de forma sencilla para que la integridad de los datos del usuario se mantenga y este sea consciente de ella. Esto representa más fiabilidad en tus apps.

Y qué decir sobre la buena cantidad de atributos que posee el TextInputLayout para personalizar el color de los errores, la etiqueta y la creación de un contador de caracteres.