Inicio Android Bottom Sheets en Android

Bottom Sheets en Android

por sgoliver

La llegada de la nueva versión 23.2 de la librería de soporte de Android nos trae un nuevo componente con el que podremos enriquecer la interfaz de usuario de algunas de nuestras aplicaciones, las llamadas Bottom Sheets. Podéis encontrar una descripción detallada de este componente en las guías de diseño de Material Design.

Un bottom sheet no es más que un panel deslizante, al estilo del navigation drawer, pero que aparece desde la parte inferior de la pantalla.

bottom-sheet-android-material-design
En este artículo vamos a ver como utilizar de forma sencilla las dos modalidades principales de este componente.

Por un lado tenemos los persistent bottom sheet, utilizados normalmente para mostrar información complementaria a la mostrada en la vista principal. Estos paneles tienen la misma elevación que el contenido principal y permanecen visibles incluso cuando no se están utilizando activamente.

La segunda modalidad son los modal bottom sheet, que suelen utilizarse como alternativa a menús o diálogos sencillos. Tienen una elevación superior al contenido principal, lo oscurecen al mostrarse, y además es necesario ocultarlos para poder seguir interactuando con el resto de la aplicación.

Dado que este componente se proporciona como parte de la librería de diseño de android, lo primero que tendremos que hacer como siempre es añadir la referencia a dicha librería en nuestro fichero build.gradle:

dependencies {
    ...
    compile 'com.android.support:appcompat-v7:23.2.0'
    compile 'com.android.support:design:23.2.0'
}

Hecho esto, vamos a empezar creando una actividad principal de ejemplo, consistente en un simple toolbar y tres botones, pero apoyándonos en los componentes CoordinatorLayout y AppBarLayout como ya explicamos en un artículo anterior:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbarLayout"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" >

        <android.support.v7.widget.Toolbar
            android:id="@+id/appbar"
            android:layout_height="?attr/actionBarSize"
            android:layout_width="match_parent"
            android:minHeight="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:elevation="4dp"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" >

        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

    <!-- Contenido Principal -->
    <include layout="@layout/contenido_main" />

    <!-- Bottom Sheet -->
    <include layout="@layout/bottom_sheet_main" />

</android.support.design.widget.CoordinatorLayout>

En el layout anterior podemos ver dos include en la parte final. El primero de ellos, @layout/contenido_main, es simplemente el contenido principal de nuestra interfaz, que como ya hemos dicho consistirá en tres botones a los que después daremos funcionalidad:

<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/contenido"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#999999"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/activity_main">

    <Button
        android:id="@+id/btnExpBottomSheet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Expandir Bottom Sheet" />

    <Button
        android:id="@+id/btnConBottomSheet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Contraer Bottom Sheet" />

    <Button
        android:id="@+id/btnOcuBottomSheet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Ocultar Bottom Sheet" />

</LinearLayout>

Hasta aquí ninguna novedad. Lo realmente nuevo estaría en el segundo de los include del layout principal, @layout/bottom_sheet_main, que es donde definiremos nuestro bottom sheet. Como bottom sheet podemos utilizar cualquier vista, normalmente alguna de tipo contenedor (FrameLayout, LinearLayout, …) y por supuesto dentro de ella incluiremos los controles adicionales que queramos mostrar en el panel. En mi caso de ejemplo incluiré tan sólo dos etiquetas de texto, una a modo de título del panel y otra de contenido:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/bottomSheet"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:orientation="vertical"
    android:background="#FFFFFF"
    app:layout_behavior="@string/bottom_sheet_behavior"
    app:behavior_hideable="true"
    app:behavior_peekHeight="64dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Title"
        android:padding="16dp"
        android:text="BOTTOM SHEET" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        android:padding="16dp"
        android:text="Texto de prueba" />

</LinearLayout>

El detalle novedoso, y lo que convierte realmente a este nuevo layout en un bottom sheet, es el atributo layout_behavior, al que hemos asignado el valor @string/bottom_sheet_behavior. Este simple cambio convierte a nuestro LinearLayout en un bottom sheet, añadiendo toda la funcionalidad necesaria para responder automáticamente a los gestos del usuario para expandir o contraer el panel.

Un bottom sheet puede estar en varios estados, los principales:

  • Expandido (STATE_EXPANDED). El panel se muestra completo.
  • Contraído (STATE_COLLAPSED). Solo se muestra una pequeña parte de la zona superior de nuestro panel.
  • Oculto (STATE_HIDDEN). El panel se encuentra completamente oculto.

Además de los estados anteriores existen otros dos (STATE_SETTLING y STATE_DRAGGING) que son transitorios y por los que pasa el panel cuando se está desplazando entre los estados anteriores.

La diferencia entre «contraído» y «oculto» va a estar en otro de los atributos que hemos añadido a nuestro bottom sheet, llamado behavior_peekHeight, que determina la porción mayor o menor de nuestro bottom sheet que será visible cuando el panel esté contraído. Si indicamos un número de píxeles (en dp) mayor que cero en este atributo, nuestro panel podrá desplazarse a una posición intermedia entre expandido y oculto. Si lo establecemos en 0dp el panel solo se mostrará completo o se ocultará totalmente (en la práctica, en este caso parece que sólo transiciona entre los estados EXPANDED y COLLAPSED, no se utilizará HIDDEN). En nuestro ejemplo, hemos establecido este atributo a 64dp, de modo que inicialmente se mostrará esa porción del panel, quedando así visible sólo la etiqueta de título.

La posibilidad de ocultar completamente o no el bottom sheet también podemos controlarla mediante el atributo behavior_hideable, que en nuestro caso a efectos de demostración hemos establecido a true para que el panel se pueda ocultar totalmente.

Con esto ya podríamos ejecutar la aplicación y ver cómo el panel responde a los gestos de deslizamiento verticales.

Además de controlar el panel mediante gestos, también podremos hacerlo desde nuestro código. Es para esto para lo que hemos añadido los tres botones en el layout principal de la aplicación, destinando cada uno de ellos a pasar el panel a uno de sus tres estados principales.

Esta tarea es muy sencilla. Empezaremos obteniendo una referencia a la vista de nuestro layout que actúa como bottom sheet y a partir de ésta obtendremos un objeto de tipo BottomSheetBehavior llamando a su método from(). Una vez obtenido este objeto, cambiar de estado nuestro panel sería tan fácil como llamar al método setState() pasando como parámetro el valor del estado que deseemos (BottomSheetBehavior.STATE_EXPANDED, STATE_COLLPSED o STATE_HIDDEN). Por ejemplo, para el botón de expandir nuestro código quedaría de la siguiente forma:

public class MainActivity extends AppCompatActivity {

    //...
    private Button btnExpBottomSheet;
    private LinearLayout bottomSheet;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //...

        bottomSheet = (LinearLayout)findViewById(R.id.bottomSheet);

        final BottomSheetBehavior bsb = BottomSheetBehavior.from(bottomSheet);

        btnExpBottomSheet = (Button)findViewById(R.id.btnExpBottomSheet);
        btnExpBottomSheet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                bsb.setState(BottomSheetBehavior.STATE_EXPANDED);
            }
        });
    }
}

Por último, vamos a ver cómo podríamos también responder a los eventos de desplazamiento y cambio de estado del bottom sheet. Para ello, asignaremos a nuestro BottomSheetBehavior un nuevo callback de tipo BottomSheetCallback, implementando sus dos métodos principales onStateChanged() y onSlide(). El primero de ellos se llama cuando el panel cambia de estado (a cualquiera de los cinco indicados) y el segundo se llama reiteradamente mientras el panel está moviéndose, es decir, mientras está en transición entre alguno de sus estados.

Para el evento de cambio de estado recibimos como parámetro el nuevo estado del panel, y dependiendo de éste podremos actuar en consecuencia. Para este ejemplo escribo simplemente un mensaje en el log.

En cuanto al evento de desplazamiento, lo que recibimos es un valor de offset. Este offset será un valor real comprendido entre -1.0 y 1.0, con el siguiente significado:

  • Valor 1.0 –> El panel está expandido.
  • Valor 0.0 –> El panel está contraído, es decir, se muestra sólo la porción definida como peekHeight.
  • Valor -1.0 –> El panel está oculto (este valor no se alcanza si peekHeight = 0dp)

Para nuestro ejemplo, escribiremos también al log las posiciones de offset que recibamos en este evento.

bsb.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
    @Override
    public void onStateChanged(@NonNull View bottomSheet, int newState) {

        String nuevoEstado = "";

        switch(newState) {
            case BottomSheetBehavior.STATE_COLLAPSED:
                nuevoEstado = "STATE_COLLAPSED";
                break;
            case BottomSheetBehavior.STATE_EXPANDED:
                nuevoEstado = "STATE_EXPANDED";
                break;
            case BottomSheetBehavior.STATE_HIDDEN:
                nuevoEstado = "STATE_HIDDEN";
                break;
            case BottomSheetBehavior.STATE_DRAGGING:
                nuevoEstado = "STATE_DRAGGING";
                break;
            case BottomSheetBehavior.STATE_SETTLING:
                nuevoEstado = "STATE_SETTLING";
                break;
        }

        Log.i("BottomSheets", "Nuevo estado: " + nuevoEstado);
    }

    @Override
    public void onSlide(@NonNull View bottomSheet, float slideOffset) {
        Log.i("BottomSheets", "Offset: " + slideOffset);
    }
});

Os dejo un video donde puede verse en funcionamiento la aplicación de ejemplo desarrollada, aunque os recomiendo como siempre descargar el código completo del ejemplo (al final del artículo) y experimentar por vosotros mismos.

Para terminar, veamos como crear bottom sheet modales, es decir, aquellos que oscurecen el contenido que queda en segundo plano. Para esto, Android proporciona en la nueva versión de las librerías de soporte dos nuevas clases con las que conseguir fácilmente el efecto indicado: BottomSheetDialog y BottomSheetDialogFragment.

Para crear un panel de este tipo empezaremos como siempre por crear su layout. En este caso de ejemplo crearé uno muy similar al ya utilizado para la modalidad anterior, consistente en una etiqueta de título y tres etiquetas adicionales a modo de opciones de menú.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/modalBottomSheet"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#FFFFFF">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Title"
        android:padding="16dp"
        android:text="MODAL BOTTOM SHEET" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        android:padding="16dp"
        android:text="Opción 1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        android:padding="16dp"
        android:text="Opción 2" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        android:padding="16dp"
        android:text="Opción 3" />

</LinearLayout>

Hecho esto, tendremos que crear una nueva clase que extienda de BottomSheetDialogFragment, donde únicamente implementaremos su método onCreateView() . En dicho método únicamente nos preocuparemos de inflar el layout anterior y devolverlo como resultado.

package net.sgoliver.android.bottomsheets;

import android.os.Bundle;
import android.support.design.widget.BottomSheetDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MiBottomSheetDialogFragment extends BottomSheetDialogFragment {

    static MiBottomSheetDialogFragment newInstance() {
        return new MiBottomSheetDialogFragment();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.bs_dialog_fragment, container, false);
        return v;
    }
}

Por último, vamos a añadir un botón más al layout principal de la aplicación de ejemplo, que utilizaremos para mostrar nuestro nuevo panel modal. Al pulsarlo, crearemos una instancia de nuestra clase MiBottomSheetDialogFragment y llamaremos a su método show() para mostrar el panel, pasandole como parámetros una referencia al fragment manager y una etiqueta que identifique al diálogo.

btnModalBottomSheet = (Button)findViewById(R.id.btnModalBottomSheet);
btnModalBottomSheet.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        BottomSheetDialogFragment bsdFragment =
             MiBottomSheetDialogFragment.newInstance();

        bsdFragment.show(
             MainActivity.this.getSupportFragmentManager(), "BSDialog");
    }
});

Y con esto sería suficiente. Veamos una demostración en video de cómo se comporta el nuevo panel:

Tan sólo una nota para terminar. Existen actualmente varios bugs ya registrados que parecen provocar que estos nuevos componentes no se comporten en ocasiones de la forma prevista, sobre todo en su versión modal. Por tanto quizá sea buena idea esperar a una nueva versión de la librería antes de comenzar a utilizar estos elementos en vuestras aplicaciones.

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la página del curso en GitHub.

También te puede interesar

11 comentarios

Josep 29/02/2016 - 13:05

Hola

He hecho alguna prueba y funciona bien, es muy facil de implementar.

Pero hay una cosa que no encuentro, no hay nada preparado para oscurecer el contenido que queda en segundo plano?

Gracias.

Responder
sgoliver 29/02/2016 - 14:30

Para oscurecer el contenido en segundo plano debes utilizar un bottom sheet «modal», algo que se consigue muy facilmente utilizando las nuevas clases BottomSheetDialog/BottomSheetDialogFragment. Actualizaré en breve el artículo para incluir esto, aunque advierto que el estado actual de dichos componentes es algo precario, se comportan de un modo extraño debido probablemente a varios bugs que ya hay registrados. Saludos.

Responder
Irwin Ortiz 29/02/2016 - 17:46

Hola Salvador, como siempre un muy buen post. Una pregunta: Estoy armando una app para carrito de compras, ¿Podría ser esta en tu opinión la mejro opción para mostrar el estatus del carrito al momento que un usuario añade un producto?. Gracias!

Responder
Saúl Molinero 01/03/2016 - 20:10

Muy buen artículo Salvador. Felicidades!

Responder
Francesc 02/03/2016 - 9:18

Buenas Salvador, estoy trabajando con una Activity que tiene: CoordinatorLayout – Tablayout – Viewpager y dentro van dos fragments uno de Mapa y otro de listado. Mi idea es que en el de mapa aparezca el bottom sheet al clicar a un Marker.

Donde pondrías el BottomSheet? En el Fragment o la Activity?
1) Si lo pongo en el fragment, tengo que poner otro Coordinator Layout dentro porqué dice (The view is not a child of CoordinatorLayout) pero entonces no funciona bien.
2) Si lo pongo en la Activity tengo que estar todo el rato comunicando Fragment con Activity y viceversa.

Saludos y gracias!

Responder
Yeray Leon 02/03/2016 - 12:27

Muy buenas Salvador, soy un seguidor tuyo desde hace ya tiempo y profesionalmente me dedico al desarrollo de apps para android y empece aprendiendo cosas básicas gracias a ti y ya me metí a estudiar. Al margen de esto darte las gracias por el trabajo que haces.
Mi pregunta es, ¿ como sabes de la implementacion de todas las cosas que añade google? Las conoces a traves de la pagina oficial de google developer? o de otros blogs ingleses? Te pregunto esto porque me gusta estar enterado de todo lo que añade android ya que me gusta mucho innovar en mis apps y siempre estoy a la espera de ver con que nos sorprendes, pero es comprensible que desde que sale hasta que realizas el tutorial lleva su tiempo…
Muchas gracias y un saludo!

Responder
Josep Viciana 07/03/2016 - 18:27

Hola Yeray.

No sé si es aquí donde lo vio Salvador, pero yo lo vi en el blog oficial:

http://android-developers.blogspot.com.es/2016/02/android-support-library-232.html

Responder
Gabriel BC 19/03/2016 - 3:19

Hola, primero que todo felicitaciones por el tutorial. Tengo una duda que me tiene detenido mi trabajo. Como disparo el evento de varios fragments que tengo en una activity desde una opcion del menu de la misma activity.
Agradeceria cualquier recomendacion. Saludos

Responder
Alex 25/04/2016 - 16:15

Una pregunta: Cuando acabo la aplicación, la guardo, sincronizo y le doy a Make project y, posteriormente, genero el APK aparentemente sin errores. Luego en el móvil se instala correctamente pero, al final, no deja abrir ni aparece en el menú de aplicaciones. ¿POR QUÉ NO DEJA USARLA?

Responder
Joel Colmenares 11/11/2016 - 22:03

Buenas, muy bueno el post.
Pero tengo un problema. Cuando aplico esta tecnologia en mi layout el LinearLayout que queda como BottomSheet queda por debajo del navigationBar al momento de iniciar la actividad.

Una vez que procedo a arrastrar el BottomSheet y se expande y lo cierro el mismo queda en su estatus colapsado mostrando solo los 64dp que uno asigna en el behavior_peekHeight.

Pero como arreglo ese problema? Si estoy usando android:fitsSystemWindows=»true»
en el CoordinatorLayout? Se me corre la vista hasta abajo. No logro arreglar ese problema.

Responder
Néstor 09/11/2020 - 20:25

Yo obtengo el siguiente error (AndroidX):
The view is not associated with BottomSheetBehavior

Responder

Dejar un comentario

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies