Diseño Android: Dashboard UI con LinearLayout

Última actualización: 11/09/2015

android

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.

yelp

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

Cursos aplicaciones móviles

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 :

    cbksinweight

    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:

    cbkconweight

  • 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:
  • cbkconweight3

    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:

    width

    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:

    android-dashboard1-width

    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>
    

    android-dashboard-width-landscape

    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).

    Lint warning

    Ahora sí que hemos conseguido que el dashboard distribuya los botones proporcionalmente entre todo el espacio disponible:

    android-dashboard-full

    El resultado también es bueno en horizontal en un dipositivo de gran resolución como Nexus 7:

    android-dashboard-full-landscape

    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.

    center

    <?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.

    android-dashboard-center

    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>
    

    imagebutton

    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

Un comentario sobre “Diseño Android: Dashboard UI con LinearLayout

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.