El patrón de diseño Dashboard, que podríamos traducir como Panel de control, es un patrón de diseño de interfaces gráficas que llegó a ser muy popular entre las aplicaciones Android. Su objetivo es ubicar en una misma pantalla principal las acciones/secciones más importantes de la aplicación. Por ejemplo, en versiones antigüas Yelp utilizaba este patrón.
Actualmente (2015) su utilización es escasa y se prefiere utilizar elementos como NavigationDrawner. De hecho, Yelp optó por esta solución y ya no utiliza el dashboard mostrado anteriormente.
No hay un widget o layout Dashboard en la SDK de Android pero podemos recurrir a varias alternativas:
- Implementar un layout adecuado.
- Utilizar un GridView.
- Utilizar un layout ya existente que permita crear una visualización de «celdas» de tamaño ajustable
Optaremos por el último enfoque que a su vez se puede aplicar de dos formas distintas:
- Utilizar un LinearLayout y repartir el espacio disponible entre sus hijos de forma proporcional gracias al atributo layout_weight
- Utilizar PercentRelativeLayout, publicado por Google en agosto de 2015. Nota: Percent Layout ha sido abandonado en la versión 26 y en su lugar se debe utilizar Constraint Layout
La segunda opción es más fléxible y eficiente pero el presente tutorial utiliza la primera ya que RelativePercentLayout no estaba disponible en el momento de su primera redacción (2012). Por tanto, recomiendo echar un vistazo antes al Tip Android #38: PercentRelativeLayout para la construcción de un dashboard.
EL ATRIBUTO layout_weight
El uso del atributo layout_weight será la base de nuestro dashboard. Este atributo permite que dentro de un linear layout se distribuya de forma proporcional todo el espacio no ocupado entre sus hijos según la «importancia» que cada uno de ellos tenga asignado dado el valor de su weight. Vamos a verlo con un ejemplo para un LinearLayout horizontal, aunque es aplicable también para uno vertical:
- Si tenemos en el linear layout horizontal tres widgets, por ejemplo Checkbox, se apilarían de la siguiente forma :
Pero si a cada uno se le define el atributo android:layout_weight=»1″, el espacio sobrante se repartirá por igual entre los tres widgets, y como consecuencia todos estarán separados por la misma distancia:
- Del mismo modo, se puede comprobar que si uno tuviera como weight por ejemplo 3 y los otros 1, el que tiene tres consumirá el triple del espacio sobrante que los elementos que tienen uno. Y, al igual que en el caso anterior, se repartirá todo el espacio horizontal disponible para el layout y tendremos la siguiente disposición:
Por defecto, el valor de weight es 0 lo que implica que cada widget ocupa sólo el espacio necesario para mostrarse según su width. Es el comportamiento “normal” al que estamos acostumbrados y que simplemente apila los elementos, a no ser que juguemos un poco con los márgenes y el padding. Es el comportamiento que observamos en la primera captura de los checkboxes.
Gracias a esta propiedad, vamos a poder construir un dashboard con botones que ocupen todo el espacio disponible en la pantalla, o bien los posicionaremos de manera que el espaciado sea proporcional.
Expandir el ancho de los botones
Nuestro dashboard tendrá seis botones(Button con texto e imagen) organizados en tres filas en formato vertical y dos filas si el dispositivo está en posición horizontal. La estructura que seguirá la interfaz es la siguiente:
Lo más destacable es que cada botón tiene asignado el valor android:layout_weigh a uno.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:fillViewport="true" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@color/background" android:orientation="vertical" android:padding="@dimen/top_padding" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:orientation="horizontal" > <Button android:id="@+id/button1" style="@style/dashboard_button" android:drawableTop="@drawable/icon1" android:onClick="clicked" android:text="@string/button1" /> <Button android:id="@+id/button2" style="@style/dashboard_button" android:drawableTop="@drawable/icon2" android:onClick="clicked" android:text="@string/button2" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/button3" style="@style/dashboard_button" android:drawableTop="@drawable/icon3" android:onClick="clicked" android:text="@string/button3" /> <Button android:id="@+id/button4" style="@style/dashboard_button" android:drawableTop="@drawable/icon4" android:onClick="clicked" android:text="@string/button4" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/button5" style="@style/dashboard_button" android:drawableTop="@drawable/icon5" android:onClick="clicked" android:text="@string/button5" /> <Button android:id="@+id/button6" style="@style/dashboard_button" android:drawableTop="@drawable/icon6" android:onClick="clicked" android:text="@string/button6" /> </LinearLayout> </LinearLayout> </ScrollView>
Los botones, puesto que se van a repetir muchas veces, están configurados mediante estilos:
<resources> <style name="dashboard_button"> <item name="android:layout_width">fill_parent</item> <item name="android:layout_height">wrap_content</item> <item name="android:layout_weight">1</item> <item name="android:background">@drawable/buttonselector</item> <item name="android:padding">5dp</item> <item name="android:drawablePadding">8dp</item> </style> </resources>
Y este es el drawable que se le aplicará a cada uno:
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- PRESS --> <item android:state_pressed="true"> <shape android:shape="rectangle"> <solid android:color="@color/buttonpressed"/> <corners android:bottomLeftRadius="9dp" android:bottomRightRadius="9dp" android:topLeftRadius="9dp" android:topRightRadius="9dp" /> </shape> </item> <!-- FOCUS --> <item android:state_focused="true"> <shape android:shape="rectangle"> <solid android:color="@color/buttonfocus"/> <corners android:bottomLeftRadius="9dp" android:bottomRightRadius="9dp" android:topLeftRadius="9dp" android:topRightRadius="9dp" /> </shape> </item> <!-- SELECTED --> <item android:state_selected="true"> <shape android:shape="rectangle"> <solid android:color="@color/buttonfocus"/> <corners android:bottomLeftRadius="9dp" android:bottomRightRadius="9dp" android:topLeftRadius="9dp" android:topRightRadius="9dp" /> </shape> </item> <!-- DEFAULT --> <item android:drawable="@android:color/transparent"/> </selector>
El resultado en un AVD hdpi y Android 2.1 es el siguiente:
Para el formato landscape, basta con reubicar los botones del layout.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:fillViewport="true" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@color/background" android:orientation="vertical" android:padding="@dimen/top_padding" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:orientation="horizontal" > <Button android:id="@+id/button1" style="@style/dashboard_button" android:drawableTop="@drawable/icon1" android:onClick="clicked" android:text="@string/button1" /> <Button android:id="@+id/button2" style="@style/dashboard_button" android:drawableTop="@drawable/icon2" android:onClick="clicked" android:text="@string/button2" /> <Button android:id="@+id/button3" style="@style/dashboard_button" android:drawableTop="@drawable/icon3" android:onClick="clicked" android:text="@string/button3" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/button4" style="@style/dashboard_button" android:drawableTop="@drawable/icon4" android:onClick="clicked" android:text="@string/button4" /> <Button android:id="@+id/button5" style="@style/dashboard_button" android:drawableTop="@drawable/icon5" android:onClick="clicked" android:text="@string/button5" /> <Button android:id="@+id/button6" style="@style/dashboard_button" android:drawableTop="@drawable/icon6" android:onClick="clicked" android:text="@string/button6" /> </LinearLayout> </LinearLayout> </ScrollView>
Expandir el dashboard a toda la pantalla
El resultado no es del todo satisfactorio en formato vertical ya que los botones ocupan todo el ancho pero queda un tercio de la altura desocupada. Para hacer que el dashboard ocupe toda la altura disponible, basta con repetir la jugada que hicimos con los botones y asignar el atributo android:layout_weigh a uno también a los LinearLayout que representan cada fila.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:fillViewport="true" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@color/background" android:orientation="vertical" android:padding="@dimen/top_padding" > <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_marginTop="15dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" > <Button android:id="@+id/button1" style="@style/dashboard_button" android:drawableTop="@drawable/icon1" android:onClick="clicked" android:text="@string/button1" /> <Button android:id="@+id/button2" style="@style/dashboard_button" android:drawableTop="@drawable/icon2" android:onClick="clicked" android:text="@string/button2" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" > <Button android:id="@+id/button3" style="@style/dashboard_button" android:drawableTop="@drawable/icon3" android:onClick="clicked" android:text="@string/button3" /> <Button android:id="@+id/button4" style="@style/dashboard_button" android:drawableTop="@drawable/icon4" android:onClick="clicked" android:text="@string/button4" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" > <Button android:id="@+id/button5" style="@style/dashboard_button" android:drawableTop="@drawable/icon5" android:onClick="clicked" android:text="@string/button5" /> <Button android:id="@+id/button6" style="@style/dashboard_button" android:drawableTop="@drawable/icon6" android:onClick="clicked" android:text="@string/button6" /> </LinearLayout> </LinearLayout> </ScrollView>
Hay que tener cuidado con esta forma de proceder y no abusar puesto que al anidar elementos con su weight definido la utilizadad lint lanza un warning ya que se pueden tener problemas de rendimiento. En algunos casos, podremos resolverlos recurriendo a RelativeLayout siempre y cuando no necesitemos el weight (recordad nuevamente la existencia del novedoso PercentRelativeLayout que constituye una mejor solución que la solución propuesta en este tutorial).
Ahora sí que hemos conseguido que el dashboard distribuya los botones proporcionalmente entre todo el espacio disponible:
El resultado también es bueno en horizontal en un dipositivo de gran resolución como Nexus 7:
Botones centrados y no expandidos
En los ejemplos anteriores, los botones ocupan todo el ancho disponible lo que podemos comprobar simplemente viendo el cambio del color de fondo al presionarlos. Esto no supone ningún problema puesto que los botones no tienen bordes definidos, pero es posible que necesitemos que el dashboard ocupe toda la pantalla y que los botones no se expandan y conserven siempre el tamaño mínimo que necesiten según su imagen/texto mostrado.
Para resolver este caso, la solución que he implementado consiste en «envolver» cada botón en un LinearLayout y darle a este layout el weight que tenía asignado el botón. Con esto conseguimos que el botón quede centrado con su tamaño dentro de un «contenedor» que es el que se expande y permite que los botones se ubiquen proporcionalmente por toda la pantalla, y además no aumentamos el «anidamiento» de weight. Funciona pero es un solución un tanto «sucia»; otra posible solución mejor es la que veremos en la siguiente sección.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:fillViewport="true" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@color/background" android:orientation="vertical" android:padding="@dimen/top_padding" > <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_marginTop="15dp" android:layout_weight="1" android:baselineAligned="false" android:gravity="center" android:orientation="horizontal" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button1" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon1" android:onClick="clicked" android:text="@string/button1" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button2" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon2" android:onClick="clicked" android:text="@string/button2" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" android:baselineAligned="false"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button3" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon3" android:onClick="clicked" android:text="@string/button3" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button4" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon4" android:onClick="clicked" android:text="@string/button4" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" android:baselineAligned="false"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button5" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon5" android:onClick="clicked" android:text="@string/button5" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" > <Button android:id="@+id/button6" style="@style/dashboard_button_center" android:drawableTop="@drawable/icon6" android:onClick="clicked" android:text="@string/button6" /> </LinearLayout> </LinearLayout> </LinearLayout> </ScrollView>
Los botones reciben un nuevo estilo heredado del anterior:
<style name="dashboard_button_center" parent="@style/dashboard_button"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_weight">0</item> </style>
En la siguiente captura el resaltado del botón confirma que su tamaño es el mínimo necesario y no está ocupando todo el ancho «sobrante», pero el dashboard sigue ocupando toda la pantalla como en el ejemplo anterior.
Grid completa con botón pulsable en toda la celda
La última aproximación que veremos al problema del Dashboard con LinearLayout es la que suele resultar más lógica: todo el área de cada «celda» de nuestra grid será el botón. La implementación propuesta, en lugar de utilizar un botón, utiliza un ImageButton que hará que la imagen se expanda hasta ocupar toda la celda; en este caso no usaremos un texto para el botón, sólo la imagen. Una fila de botones quedará como sigue
<LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" > <ImageButton style="@style/dashboard_image_button" android:contentDescription="@string/button1" android:onClick="clicked" android:src="@drawable/icon1" /> <ImageButton style="@style/dashboard_image_button" android:contentDescription="@string/button2" android:onClick="clicked" android:src="@drawable/icon2" /> </LinearLayout>
aplicándose este estilo al ImageView:
<style name="dashboard_image_button"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_weight">1</item> <item name="android:background">@drawable/buttonselector</item> <item name="android:padding">@dimen/imagebutton_padding</item> <item name="android:scaleType">fitXY</item> <item name="android:adjustViewBounds">true</item> </style>
Demo completa en GitHub
Se encuentra disponible en GitHub la demo completa de todo lo visto en el presente tutorial. Esta demo tiene como dependencia ActionBarCompat que deberá ser importada en Eclipse y definida en el proyecto como una librería (más información aquí). Asimismo, para más información sobre cómo utilizar GitHub, consultar este artículo
Excelente muchas gracias por todos los tutos..!!