Crear vistas compuestas en Android
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.



