Creación dinámica de botones

En esta ocasión vamos a ver cómo añadir dinámicamente una lista de botones a un Scroll View. Dentro del Scroll View tendremos un Linear Layout llamado llBotonera donde colocaremos los botones.

Tendremos que:

  • Obtener la referencia al Linear Layout llBotonera donde colocar los botones.
  • Iterar en un bucle creando tantos objetos Button como queramos
  • Asignar propiedades de LinearLayout al botón o bien usar sus propiedades de ancho y alto.
  • Añadir el botón a la botonera.

Vamos a ver el código necesario.

El activity_main.xml que usamos es el siguiente.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.amm12.dinamicbuttons.MainActivity" >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:id="@+id/llBotonera" >
        </LinearLayout>
    </ScrollView>
</LinearLayout>

Suponemos que en la MainActivity tenemos una variable de la clase indicando el número de botones, numBotones=20

public class MainActivity extends Activity {

    static int numBotones = 20;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Establecemos el layout main
        setContentView(R.layout.activity_main);

        //Obtenemos el linear layout donde colocar los botones
        LinearLayout llBotonera = (LinearLayout) findViewById(R.id.llBotonera);

        //Creamos las propiedades de layout que tendrán los botones.
        //Son LinearLayout.LayoutParams porque los botones van a estar en un LinearLayout.
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 
                                           LinearLayout.LayoutParams.WRAP_CONTENT );

        //Creamos los botones en bucle
        for (int i=0; i<numBotones; i++){
            Button button = new Button(this);
            //Asignamos propiedades de layout al boton
            button.setLayoutParams(lp);
            //Asignamos Texto al botón
            button.setText("Boton "+String.format("%02d", i ));
            //Añadimos el botón a la botonera
            llBotonera.addView(button);
        }
    }
}

Como vemos iteramos hasta numBotones, y en cada iteración creamos el botón, le asignamos las propiedades de layout que se han creado antes, le ponemos el texto y lo añadimos a la botonera.

Esto creará 20 botones en el Scroll View, podremos desplazarnos  para ver los botones que no caben dentro de él.

Pero este código no asigna ningúna acción a realizar cuando el usuario hace click en el botón. No podemos hacerlo mediante las propiedades en tiempo de diseño puesto que no tenemos los botones creados.

Para poder hacer esto creamos una nueva clase que implementa el interfaz View.OnClickListener. De forma que este será el código que tendremos que llamar para que cada botón tenga su propio OnClickListener

button.setOnClickListener(new ButtonsOnClickListener());

Lo que hacemos habitualmente es crear un objeto OnClickListener sin nombre, sin instancia, pero en este caso crearemos la clase  ButtonsOnClickListener para trabajar más cómodos.

Este es el código completo con la  nueva clase:

public class MainActivity extends Activity {
    static int numBotones = 20;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Establecemos el layout main
        setContentView(R.layout.activity_main);

        //Obtenemos el linear layout donde colocar los botones
        LinearLayout llBotonera = (LinearLayout) findViewById(R.id.llBotonera);

        //Creamos las propiedades de layout que tendrán los botones.
        //Son LinearLayout.LayoutParams porque los botones van a estar en un LinearLayout.
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT );

        //Creamos los botones en bucle
        for (int i=0; i<numBotones; i++){
            Button button = new Button(this);
            //Asignamos propiedades de layout al boton
            button.setLayoutParams(lp);
            //Asignamos Texto al botón
            button.setText("Boton "+String.format("%02d", i ));

            //Asignamose el Listener
            button.setOnClickListener(new ButtonsOnClickListener(this));
            //Añadimos el botón a la botonera
            llBotonera.addView(button);
        }
    }

    class ButtonsOnClickListener implements View.OnClickListener
    {
        @Override
        public void onClick(View v)
        {
            Toast.makeText(getApplicationContext(),"Pulsado",Toast.LENGTH_SHORT).show();
        }

    };
}

Vemos que el método onClick únicamente muestra un Toast con la palabra “Pulsado“. Para poder usar el Toast hemos tenido que obtener el contexto de la aplicación con getApplicationContext().

Si queremos mostrar en el Toast el nombre del botón utilizaremos la vista que recibe el onClick para obtener a traves de esta el texto del mismo, asi:

    class ButtonsOnClickListener implements View.OnClickListener
    {
        @Override
        public void onClick(View v)
        {
            Button b = (Button) v;
            Toast.makeText(getApplicationContext(),b.getText(),Toast.LENGTH_SHORT).show();
        }

    };

¿Qué pasa si esta clase la declaramos fuera de la clase MainActivity?

Por ejemplo como en el siguiente esquema, o por ejemplo si lo definimos en cualquier otro fichero o clase …

public class MainActivity extends Activity {
    static int numBotones = 20;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }
}

//Fuera de la MainActivity
class ButtonsOnClickListener implements View.OnClickListener
{
   @Override
   public void onClick(View v)
   {
       Button b = (Button) v;
       Toast.makeText(getApplicationContext(),b.getText(),Toast.LENGTH_SHORT).show();
   }
};

Pues que entonces el método getApplicationContext() que utilizamos en el Toast no podemos usarlo, sin embargo necesitamos un context ahí.

La solución sería pasar el contexto en el constructor de la clase ButtonsOnCliclListener, así independientemente de donde se ubique la clase recibirá el contexto de la MainActivity y podrá mostar el Toast y lo que es más importante interactuar con el UIThread.

La clase definida fuera sería la siguiente:

class ButtonsOnClickListener implements View.OnClickListener
{
    Context context;

    public ButtonsOnClickListener(Context context) {
        this.context = context;
    }

    @Override
    public void onClick(View v)
    {
        Button b = (Button) v;
        Toast.makeText(this.context,b.getText(),Toast.LENGTH_SHORT).show();
    }

};

Como vemos hemos tenido que definir una variable miembro de la clase para almacenar el contexto que nos pasan en el constructor, así en el Toast podemos usar this.context que se refiere ahora a la variable miembro de ButtonsOnClickListener.

Como hemos cambiado el constructor debemos cambiar la asignación del onClickListener que hacemos dentro del bucle para cada botón por esta:

button.setOnClickListener(new ButtonsOnClickListener(this));

Esta misma técnica de añadir elementos en el constructor la vamos a usar para pasar cualquier cosa que necesitemos, en este caso en el onClick, por ejemplo el número del botón que han pulsado. Esto nos puede servir para mostrar un mensaje u otro en función de si es un botón par o impar, por ejemplo, o cualquier otra cosa que necesitemos hacer diferente para unos botones u otros.

Incluyendo iterador del bucle de los botones, i, en la llamada, el código sería:

    ...
    //Asignamos el Listener
    button.setOnClickListener(new ButtonsOnClickListener(this,i));
    ...
}

class ButtonsOnClickListener implements View.OnClickListener
{
    Context context;
    int numButton;

    public ButtonsOnClickListener(Context context, int numButton) {
        this.context = context;
        this.numButton = numButton;
    }

    @Override
    public void onClick(View v)
    {
        String mensaje="";
        if (numButton%2==0)
            mensaje="Boton PAR "+String.format("%02d", numButton );
        else
            mensaje="Boton IMPAR "+String.format("%02d", numButton );

        Toast.makeText(this.context,mensaje,Toast.LENGTH_SHORT).show();
    }

};

Ahora que ya vemos que en el constructor del ButtonsOnClickListener puedo pasar lo que necesite para ser usado en el onClick() u en otro método que añada …..

¿Sabes lo que es un Runnable?  …  ¿Si paso un Runnable en el constructor? ¿podré decirle en el onClick que lo ejecute?  Así podré en cualquier momento asignar un código a ejecutar diferente a los botones.

Apicación de ejemplo

Os dejo en el código de la aplicación de ejemplo que implementa esta técnica.

Como se ve en la imagen se trata de una actividad con dos botones para añadir y quitar los botones que se alojan en un ScrollView, mostrando el contador de botones que hay creados.

Cuando no quedan botones el botón de eliminar no funciona. Cuando se llena el Scrollview se puede desplazar para ver los botones que están fuera del tamaño del scroll view. Al pulsar en los botones del scrollview se muestra un Toast.

Se ha utilizado findViewByTag del LinearLayout que incorpora el Scrollview para encontrar una referencia al último botón, y éste es eliminado del LinearLayout.