1. Code
  2. Mobile Development
  3. Android Development

Crear vistas compuestas en Android

Al crear aplicaciones complejas, a menudo desearás reutilizar el mismo grupo de vistas en varios lugares de la aplicación. Una forma de resolver este problema es creando una vista que encapsule la lógica y el diseño de un grupo de vistas para que puedas volver a utilizarlas sin duplicar el código en varios lugares del proyecto. En este tutorial, aprenderás cómo usar vistas compuestas para crear vistas personalizadas que sean fácilmente reutilizables.
Scroll to top

Spanish (Español) translation by CYC (you can also view the original English article)

Al crear aplicaciones complejas, a menudo desearás reutilizar el mismo grupo de vistas en varios lugares de la aplicación. Una forma de resolver este problema es creando una vista que encapsule la lógica y el diseño de un grupo de vistas para que puedas volver a utilizarlas sin duplicar el código en varios lugares del proyecto. En este tutorial, aprenderás cómo usar vistas compuestas para crear vistas personalizadas que sean fácilmente reutilizables.

1. Introducción

En Android, una vista compuesta por un grupo de vistas se denomina vista compuesta o componente compuesto. En este tutorial, crearás un control para seleccionar un valor de una lista que se desplaza de un lado a otro. Denominaremos al compuesto como una rueda lateral ya que la vista predeterminada del SDK de Android para elegir un valor de una lista se llama spinner. La siguiente captura de pantalla muestra lo que crearemos en este tutorial.

2. Configuración del proyecto

Para comenzar, debes crear un nuevo proyecto de Android con Android 4.0 como el nivel de SDK mínimo requerido. Este proyecto solo debe contener una actividad en blanco llamada MainActivity. La Activity no hace más que inicializar el diseño como puedes ver en el siguiente fragmento de código.

1
public class MainActivity extends Activity {
2
   @Override
3
   protected void onCreate(Bundle savedInstanceState) {
4
      super.onCreate(savedInstanceState);
5
      setContentView(R.layout.activity_main);
6
   }
7
}

El diseño de MainActivity se encuentra en el archivo /res/layout/activity_main.xml y solo debe contener un RelativeLayout vacío en el que la vista compuesta se mostrará más adelante.

1
<RelativeLayout 
2
    xmlns:android="https://schemas.android.com/apk/res/android"
3
    xmlns:tools="http://schemas.android.com/tools"
4
    android:layout_width="match_parent"
5
    android:layout_height="match_parent"
6
    tools:context=".MainActivity">
7
</RelativeLayout>

3. Crear una vista compuesta

Para crear una vista compuesta, debes crear una nueva clase que administre las vistas en la vista compuesta. Para la rueda lateral, necesitas dos Button vistas para las flechas y una vista TextView para mostrar el valor seleccionado.

Para comenzar, crea el archivo /res/layout/sidespinner_view.xml de diseño que usaremos para la clase secundaria, asegurándote de ajustar las tres vistas en una etiqueta <merge>.

1
<merge xmlns:android="http://schemas.android.com/apk/res/android">
2
    <Button
3
        android:id="@+id/sidespinner_view_previous"
4
        android:layout_width="wrap_content"
5
        android:layout_height="wrap_content"
6
        android:layout_toLeftOf="@+id/sidespinner_view_value"/> 
7
   <TextView
8
        android:id="@+id/sidespinner_view_current_value"
9
        android:layout_width="wrap_content"
10
        android:layout_height="wrap_content"
11
        android:textSize="24sp" />
12
   <Button
13
        android:id="@+id/sidespinner_view_next"
14
        android:layout_width="wrap_content"
15
        android:layout_height="wrap_content" />
16
</merge>

A continuación, necesitamos crear la clase SideSpinner que infla este diseño y establece las flechas como imágenes de fondo para los botones. En este punto, la vista compuesta no hace nada ya que no hay nada que mostrar todavía.

1
public class SideSpinner extends LinearLayout {
2
3
   private Button mPreviousButton;
4
   private Button mNextButton;
5
6
   public SideSpinner(Context context) {
7
      super(context);
8
      initializeViews(context);
9
   }
10
11
   public SideSpinner(Context context, AttributeSet attrs) {
12
      super(context, attrs);
13
      initializeViews(context);
14
   }
15
16
   public SideSpinner(Context context, 
17
                      AttributeSet attrs, 
18
                      int defStyle) {
19
      super(context, attrs, defStyle);
20
      initializeViews(context);
21
   }
22
23
   /**

24
    * Inflates the views in the layout.

25
    * 

26
    * @param context

27
    *           the current context for the view.

28
    */
29
   private void initializeViews(Context context) {
30
      LayoutInflater inflater = (LayoutInflater) context
31
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
32
      inflater.inflate(R.layout.sidespinner_view, this);
33
   }
34
35
   @Override
36
   protected void onFinishInflate() {
37
      super.onFinishInflate();
38
39
      // Sets the images for the previous and next buttons. Uses 

40
      // built-in images so you don't need to add images, but in 

41
      // a real application your images should be in the 

42
      // application package so they are always available.

43
      mPreviousButton = (Button) this
44
        .findViewById(R.id.sidespinner_view_previous);
45
      mPreviousButton
46
        .setBackgroundResource(android.R.drawable.ic_media_previous);
47
48
      mNextButton = (Button)this
49
                       .findViewById(R.id.sidespinner_view_next);
50
      mNextButton
51
        .setBackgroundResource(android.R.drawable.ic_media_next);
52
   }
53
}

Notarás que la vista compuesta extiende el grupo de vistas LinearLayout. Esto significa que cualquier diseño que use la vista compuesta tiene acceso a los atributos del diseño lineal. Como resultado, el diseño de la vista compuesta es un poco diferente de lo habitual, la etiqueta raíz es una etiqueta <merge> en lugar de la etiqueta para un grupo de vista como <LinearLayout> o <RelativeLayout>.

Cuando agregas la vista compuesta al diseño de MainActivity, la etiqueta de la vista compuesta actuará como una etiqueta <LinearLayout>. Una clase de vista compuesta puede derivar de cualquier clase derivada de ViewGroup, pero en este caso el diseño lineal es el más apropiado ya que las vistas se disponen horizontalmente.

4. Agrega la vista compuesta a un diseño

En este punto, el proyecto se compila pero no se ve nada, ya que la vista compuesta no está en el diseño de MainActivity. La vista del panel lateral debe agregarse al diseño de la actividad como cualquier otra vista. El nombre de la etiqueta es el nombre completo de la clase SideSpinner, incluido el espacio de nombres.

Para agregar la rueda lateral a MainActivity, agrega lo siguiente a la disposición relativa en el archivo /res/layout/activity_main.xml.

1
<com.cindypotvin.sidespinnerexample.SideSpinner
2
   android:id="@+id/sidespinner_fruits"
3
   android:layout_width="match_parent"
4
   android:layout_height="wrap_content"
5
   android:orientation="horizontal"
6
   android:gravity="center"/>

Los atributos disponibles en la etiqueta <SideSpinner> son atributos del diseño lineal, ya que la clase SideSpinner que creamos amplía la clase LinearLayout. Si inicias el proyecto, la rueda lateral debería estar visible, pero todavía no contiene ningún valor.

5. Agrega métodos a la vista compuesta

Todavía faltan algunas cosas si realmente queremos usar el spinner lateral. Deberíamos poder agregar nuevos valores a la rueda, seleccionar un valor y obtener el valor seleccionado.

La forma más fácil de agregar nuevos comportamientos a una vista compuesta es agregar nuevos métodos públicos a la clase SideSpinner. Estos métodos pueden ser utilizados por cualquier Activity que tenga una referencia a la vista.

1
private CharSequence[] mSpinnerValues = null;
2
private int mSelectedIndex = -1;
3
4
/**

5
 * Sets the list of value in the spinner, selecting the first value 

6
 * by default.

7
 * 

8
 * @param values

9
 *           the values to set in the spinner.

10
 */
11
public void setValues(CharSequence[] values) {
12
   mSpinnerValues = values;
13
14
   // Select the first item of the string array by default since 

15
   // the list of value has changed.

16
   setSelectedIndex(0);
17
}
18
19
/**

20
 * Sets the selected index of the spinner.

21
 * 

22
 * @param index

23
 *           the index of the value to select.

24
 */
25
public void setSelectedIndex(int index) {
26
   // If no values are set for the spinner, do nothing.

27
   if (mSpinnerValues == null || mSpinnerValues.length == 0)
28
      return;
29
30
   // If the index value is invalid, do nothing.

31
   if (index < 0 || index >= mSpinnerValues.length)
32
      return;
33
34
   // Set the current index and display the value.

35
   mSelectedIndex = index;
36
   TextView currentValue;
37
   currentValue = (TextView)this
38
                  .findViewById(R.id.sidespinner_view_current_value);
39
   currentValue.setText(mSpinnerValues[index]);
40
41
   // If the first value is shown, hide the previous button.

42
   if (mSelectedIndex == 0)
43
      mPreviousButton.setVisibility(INVISIBLE);
44
   else
45
      mPreviousButton.setVisibility(VISIBLE);
46
47
   // If the last value is shown, hide the next button.

48
   if (mSelectedIndex == mSpinnerValues.length - 1)
49
      mNextButton.setVisibility(INVISIBLE);
50
   else
51
      mNextButton.setVisibility(VISIBLE);
52
}
53
54
/**

55
 * Gets the selected value of the spinner, or null if no valid 

56
 * selected index is set yet.

57
 * 

58
 * @return the selected value of the spinner.

59
 */
60
public CharSequence getSelectedValue() {
61
   // If no values are set for the spinner, return an empty string.

62
   if (mSpinnerValues == null || mSpinnerValues.length == 0)
63
      return "";
64
65
   // If the current index is invalid, return an empty string.

66
   if (mSelectedIndex < 0 || mSelectedIndex >= mSpinnerValues.length)
67
      return "";
68
69
   return mSpinnerValues[mSelectedIndex];
70
}
71
72
/**

73
 * Gets the selected index of the spinner.

74
 * 

75
 * @return the selected index of the spinner.

76
 */
77
public int getSelectedIndex() {
78
   return mSelectedIndex;
79
}

El método onFinishInflate de la vista compuesta se invoca cuando todas las vistas en el diseño están infladas y listas para usar. Este es el lugar para agregar tu código si necesitas modificar las vistas en la vista compuesta.

Con los métodos que acabas de agregar a la clase SideSpinner, ahora se puede agregar el comportamiento de los botones que seleccionan el valor anterior y siguiente. Reemplaza el código existente en el método onFinishInflate con lo siguiente:

1
@Override
2
protected void onFinishInflate() {
3
4
   // When the controls in the layout are doing being inflated, set 

5
   // the callbacks for the side arrows.

6
   super.onFinishInflate();
7
8
   // When the previous button is pressed, select the previous value 

9
   // in the list.

10
   mPreviousButton = (Button) this
11
      .findViewById(R.id.sidespinner_view_previous);
12
   mPreviousButton
13
      .setBackgroundResource(android.R.drawable.ic_media_previous);
14
15
   mPreviousButton.setOnClickListener(new OnClickListener() {
16
      public void onClick(View view) {
17
         if (mSelectedIndex > 0) {
18
            int newSelectedIndex = mSelectedIndex - 1;
19
            setSelectedIndex(newSelectedIndex);
20
         }
21
      }
22
   });
23
24
   // When the next button is pressed, select the next item in the 

25
   // list.

26
   mNextButton = (Button)this
27
                    .findViewById(R.id.sidespinner_view_next);
28
   mNextButton
29
      .setBackgroundResource(android.R.drawable.ic_media_next);
30
   mNextButton.setOnClickListener(new OnClickListener() {
31
      public void onClick(View view) {
32
         if (mSpinnerValues != null
33
               && mSelectedIndex < mSpinnerValues.length - 1) {
34
            int newSelectedIndex = mSelectedIndex + 1;
35
            setSelectedIndex(newSelectedIndex);
36
         }
37
      }
38
   });
39
40
   // Select the first value by default.

41
   setSelectedIndex(0);
42
}

Con los nuevos métodos creados setValues y setSelectedIndex, ahora podemos inicializar el spinner lateral desde nuestro código. Al igual que con cualquier otra vista, debes encontrar la vista del panel lateral en el diseño con el método findViewById. A continuación, podemos llamar a cualquier método público en la vista desde el objeto devuelto, incluidos los que acabamos de crear.

El siguiente fragmento de código muestra cómo actualizar el método onCreate de la clase MainActivity para mostrar una lista de valores en el spinner lateral, utilizando el método setValues. También podemos seleccionar el segundo valor en la lista por defecto invocando el método setSelectedIndex.

1
public class MainActivity extends Activity {
2
3
   @Override
4
   protected void onCreate(Bundle savedInstanceState) {
5
      super.onCreate(savedInstanceState);
6
      setContentView(R.layout.activity_main);
7
8
      // Initializes the side spinner from code.

9
      SideSpinner fruitsSpinner;
10
      fruitsSpinner = (SideSpinner)this
11
                         .findViewById(R.id.sidespinner_fruits);
12
13
      CharSequence fruitList[] = { "Apple", 
14
                                   "Orange", 
15
                                   "Pear", 
16
                                   "Grapes" };
17
      fruitsSpinner.setValues(fruitList);
18
      fruitsSpinner.setSelectedIndex(1);
19
   }
20
}

Si inicias la aplicación, la rueda lateral debería funcionar como se esperaba. La lista de valores se muestra y el valor Orange se selecciona de manera predeterminada.

6. Agrega atributos de diseño a la vista compuesta

Las vistas disponibles en Android SDK se pueden modificar mediante código, pero algunos atributos también se pueden configurar directamente en el diseño correspondiente. Agreguemos un atributo a la rueda giratoria lateral que establece los valores que el spinner lateral necesita para mostrar.

Para crear un atributo personalizado para la vista compuesta, primero debemos definir el atributo en el archivo /res/values/attr.xml. Cada atributo de la vista compuesta debe agruparse en un estilo con una etiqueta <declare-styleable>. Para el spinner lateral, el nombre de la clase se usa como se muestra a continuación.

1
<resources>
2
  <declare-styleable name="SideSpinner">
3
    <attr name="values" format="reference" />
4
  </declare-styleable>
5
</resources>

En la etiqueta <attr>, el atributo name contiene el identificador utilizado para referirse al nuevo atributo en el diseño y el atributo format contiene el tipo del nuevo atributo.

Para la lista de valores, se utiliza el tipo reference ya que el atributo se referirá a una lista de cadenas definidas como un recurso. Los tipos de valores que normalmente se usan en los diseños se pueden usar para sus atributos personalizados, incluidos boolean, color, dimension, enum, integer, float y string.

Aquí se explica cómo definir el recurso para una lista de cadenas a las que se referirá el atributo values del spinner lateral. Se debe agregar al archivo /res/values/strings.xml como se muestra a continuación.

1
<resources>
2
    <string-array name="vegetable_array">
3
        <item>Cucumber</item>
4
        <item>Potato</item>
5
        <item>Tomato</item>
6
        <item>Onion</item>
7
        <item>Squash</item>
8
    </string-array>  
9
</resources>

Para probar el nuevo atributo values, crea una vista para el spinner lateral en el diseño MainActivity debajo del spinner lateral existente. El atributo debe ir precedido de un espacio de nombre agregado a RelativeLayout, como xmlns:sidespinner="http://schemas.android.com/apk/res-auto". Esto es lo que debería ser el diseño final en /res/layout/activity_main.xml.

1
<RelativeLayout 
2
    xmlns:android="http://schemas.android.com/apk/res/android"
3
    xmlns:tools="http://schemas.android.com/tools"
4
    xmlns:sidespinner="http://schemas.android.com/apk/res-auto"
5
    android:layout_width="match_parent"
6
    android:layout_height="match_parent"
7
    tools:context=".MainActivity">
8
    <com.cindypotvin.sidespinnerexample.SideSpinner
9
      android:id="@+id/sidespinner_fruits"
10
      android:layout_width="match_parent"
11
      android:layout_height="wrap_content"
12
      android:orientation="horizontal"
13
      android:gravity="center"/>
14
    
15
     <com.cindypotvin.sidespinnerexample.SideSpinner
16
      android:id="@+id/sidespinner_vegetables"
17
      android:layout_width="match_parent"
18
      android:layout_height="wrap_content"
19
      android:orientation="horizontal"
20
      android:gravity="center"
21
      android:layout_below="@id/sidespinner_fruits" 
22
      sidespinner:values="@array/vegetable_array" />
23
</RelativeLayout>

Finalmente, la clase SideSpinner necesita ser modificada para leer el atributo de valores. El valor de cada atributo de la vista está disponible en el objeto AttributeSet que se pasa como un parámetro del constructor de la vista.

Para obtener el valor de tus atributos values personalizados, primero llamamos al método getStyledAttributes del objeto AttributeSet con el nombre del estilo que contiene el atributo. Esto devuelve la lista de atributos para ese estilo como un objeto TypedArray.

Luego llamamos al método getter del objeto TypedArray que tiene el tipo correcto para el atributo que deseas, pasando el identificador del atributo como parámetro. El siguiente bloque de código muestra cómo modificar el constructor de la rueda lateral para obtener la lista de valores y configurarlos en la rueda lateral.

1
public SideSpinner(Context context) {
2
   super(context);
3
4
   initializeViews(context);
5
}
6
7
public SideSpinner(Context context, AttributeSet attrs) {
8
   super(context, attrs);
9
10
   TypedArray typedArray;
11
   typedArray = context
12
             .obtainStyledAttributes(attrs, R.styleable.SideSpinner);
13
   mSpinnerValues = typedArray
14
                       .getTextArray(R.styleable.SideSpinner_values);
15
   typedArray.recycle();
16
17
   initializeViews(context);
18
}
19
20
public SideSpinner(Context context, 
21
                   AttributeSet attrs,
22
                   int defStyle) {
23
   super(context, attrs, defStyle);
24
25
   TypedArray typedArray;
26
   typedArray = context
27
             .obtainStyledAttributes(attrs, R.styleable.SideSpinner);
28
   mSpinnerValues = typedArray
29
                       .getTextArray(R.styleable.SideSpinner_values);
30
   typedArray.recycle();
31
32
   initializeViews(context);
33
}

Si inicias la aplicación, deberías ver dos marcadores laterales que funcionan independientemente el uno del otro.

7. Guardar y restaurar el estado

El último paso que debemos completar es guardar y restaurar el estado de la vista compuesta. Cuando se destruye y recrea una actividad, por ejemplo, cuando se gira el dispositivo, los valores de las vistas nativas con un identificador único se guardan y restauran automáticamente. Esto actualmente no es cierto para el spinner lateral.

El estado de las vistas no se guarda. Los identificadores de las vistas en la clase SideSpinner no son únicos, ya que se pueden reutilizar muchas veces. Esto significa que somos responsables de guardar y restaurar los valores de las vistas en la vista compuesta. Hacemos esto implementando los métodos onSaveInstanceState, onRestoreInstanceState y dispatchSaveInstanceState. El siguiente bloque de código muestra cómo hacer esto para el spinner lateral.

1
/**

2
 * Identifier for the state to save the selected index of 

3
 * the side spinner.

4
 */
5
private static String STATE_SELECTED_INDEX = "SelectedIndex";
6
7
/**

8
 * Identifier for the state of the super class.

9
 */
10
private static String STATE_SUPER_CLASS = "SuperClass";
11
12
@Override
13
protected Parcelable onSaveInstanceState() { 
14
   Bundle bundle = new Bundle();
15
16
    bundle.putParcelable(STATE_SUPER_CLASS,
17
                         super.onSaveInstanceState());
18
    bundle.putInt(STATE_SELECTED_INDEX, mSelectedIndex);
19
20
    return bundle;
21
}
22
23
@Override
24
protected void onRestoreInstanceState(Parcelable state) {
25
    if (state instanceof Bundle) {
26
        Bundle bundle = (Bundle)state;
27
28
        super.onRestoreInstanceState(bundle
29
                                 .getParcelable(STATE_SUPER_CLASS));
30
        setSelectedIndex(bundle.getInt(STATE_SELECTED_INDEX));
31
    } 
32
    else
33
       super.onRestoreInstanceState(state);   
34
}
35
36
@Override
37
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
38
    // Makes sure that the state of the child views in the side 

39
    // spinner are not saved since we handle the state in the

40
    // onSaveInstanceState.

41
    super.dispatchFreezeSelfOnly(container);
42
}
43
44
@Override
45
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
46
    // Makes sure that the state of the child views in the side 

47
    // spinner are not restored since we handle the state in the

48
    // onSaveInstanceState.

49
    super.dispatchThawSelfOnly(container);
50
}

Conclusión

El spinner lateral ahora está completo. Ambos spinners laterales funcionan como se espera y sus valores se restauran si la actividad se destruye y se recrea. Ahora puedes aplicar lo que has aprendido para reutilizar cualquier grupo de vistas en una aplicación de Android mediante el uso de vistas compuestas.