Diseño Android: Floating Action Button con Design Support Library

Última actualización : 20/02/2016
android

Floating Action Button, FAB para los amigos, es un elemento visual introducido en Material Design cuyo objetivo es destacar la acción principal de una pantalla siempre y cuando esta constituya una operación muy habitual y «natural» como por ejemplo redactar un nuevo email en una aplicación de correos.

La acción flotante debe ser única y se muestra como un botón circular y plano en la parte inferior derecha de la pantalla aunque en ocasiones es posible que el FAB se «incruste» en otro elemento de la interfaz:

floating action button

Material Design contempla también cómo mostrar las subacciones u opciones de la acción a realizar desde una FAB (no se tratarán en este artículo):

  • Mostrar una toolbar en la parte inferior de la pantalla.
  • Desplegar un menú contextual
  • Desplegar las acciones también como FAB utilizando botones más pequeños. Es el caso de Google Keep: el FAB para añadir una nueva nota muestra los distintos tipos de notas para seleccionar una.

Se pueden encontrar varias implementaciones de FAB en GitHub pero recientemente (mayo de 2015) Google ha publicado la Design Support Library para poder utilizar fácilmente varios elementos de Material Design incluyendo FAB. En este tutorial se utilizará esta librería.

El proyecto de ejemplo desarrollado en Android Studio consistirá en una aplicación compatible con Gingerbread y con una ActionBar implementada con una Toolbar. El objetivo será mostrar un FloatingActionButton sobre una ListView, una de las interfaces de usuario más típicas en las que se suele mostrar un FAB.

La primera tarea a realizar será incluir el módulo de diseño de la librería de compatibilidad en el proyecto utilizando el fichero /app/build.gradle. Se incluye también el módulo appcompat para utilizar la Toolbar.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
}

Para más información sobre la importación de librerías en Eclipse ADT y Android Studio, consultar este artículo.

Cursos aplicaciones móviles

La mayor complicación que encontraremos a la hora de incluir el FAB es su posicionamiento en pantalla el cual dependerá del layout que estemos utilizando. En la introducción vimos que la posición a utilizar será en la parte inferior derecha de la pantalla (se recomienda utilizar un margen de 16dp) a menos que el FAB se «incruste» en otro elemento de la pantalla como por ejemplo su cabecera. En el caso de la ListView nos encontramos en la primera situación y para superponer el FAB sobre el ListView se puede utilizar un FrameLayout que ya fue empleado en el Tip Android #10: mostrar una imagen como background.

Así pues, el layout con la Toolbar quedará como sigue.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

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

    <FrameLayout
        android:id="@+id/main_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:src="@mipmap/ic_action_edit"
            app:borderWidth="0dp"
            app:elevation="6dp"
            android:layout_marginBottom="@dimen/fab_margin"
            android:layout_marginRight="@dimen/fab_margin"/>

    </FrameLayout>

</LinearLayout>

Existen un «bug», aunque en Google no lo consideran como tal, a la hora de posicionar y mostrar la sombra del FloatingActionButton en Android pre-Lollipop. Para ello aplicamos las soluciones propuestas en varios blogs definiendo en el FAB los parámetros resaltados. Las dimensiones que se aplicarán dependen de que la versión de Android sea API 21 por lo que definimos valores en los ficheros /res/values-v21/dimens.xml (API 21+) y /res/values/dimens.xml (versiones anteriores).

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="fab_margin">0dp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="fab_margin">16dp</dimen>
</resources>

El ListView simplemente mostrará una cadena de texto, por lo que creamos el layout de cada row y en la Activity usaremos directamente la implementación de ArrayAdapter de Android.

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:textColor="@android:color/black"
    android:textSize="20sp"
    android:layout_width="fill_parent"
    android:padding="10dp"
    android:layout_height="wrap_content" />

En la Activity, además del código habitual para la Toolbar y la ListView, tan sólo tendremos que establecer el OnClickListener para atender la pulsación del FAB.

package com.danielme.android.fab;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.LinkedList;
import java.util.List;


public class MainActivity extends AppCompatActivity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        List<String> list = new LinkedList<String>();
        for (int i = 1; i < 30; i++) {
            list.add(String.valueOf(i));
        }
        ListView listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(new ArrayAdapter<String>(this, R.layout.list_item, android.R.id.text1, list));

        FloatingActionButton floatingActionButton = (FloatingActionButton) findViewById(R.id.fab);
        floatingActionButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, R.string.action, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

Tenemos tres opciones para definir el color del FAB.

  • De forma global para toda la aplicación definiendo la propiedad accentColor
        <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="colorPrimary">@color/primary</item>
            <item name="colorPrimaryDark">@color/primaryDark</item>
            <item name="android:textColorPrimary">@color/textColorPrimary</item>
            <item name="colorAccent">@color/accentColor</item>
        </style>
    
  • En la declaración del widget
           <android.support.design.widget.FloatingActionButton
                android:id="@+id/fab"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end|bottom"
                android:src="@mipmap/ic_action_edit"
                app:backgroundTint="@color/accentColor" />
    
  • Programáticamente
    floatingActionButton.setBackgroundTintList(getResources().getColorStateList(R.color.accentColor));
    

FloatingActionButtonDemo

En el caso de que se quiera utilizar la versión «mini» del FAB basta con añadir la propiedad app:fabSize=»mini»

Mostrar\ocultar el botón al hacer scroll

En algunas aplicaciones que utilizan FloatingActionButton en una pantalla con un ListView o similar (RecyclerView, ScrollView), el botón se oculta automáticamente al hacer scroll hacia abajo y permanece visible al hacer scroll hacia arriba para facilitar la consulta de los datos.

La implementación de FloatingActionButton de la design support library, a diferencia de algunas que podemos encontrar en GitHub, no proporciona esta funcionalidad. Por fortuna su implementación no es excesivamente díficil y una vez realizada se puede aplicar fácilmente a cualquier app.

Para simplificar la implementación de esta funcionalidad dividiremos el problema en dos partes: primero se deberá detectar el movimiento del scroll del ListView y si es necesario realizar la ocultación/visualización del botón, y luego aplicar una animación que haga aparecer/desaparecer de la pantalla el elemento de forma elegante.

El control del scroll se puede realizar implementando el listener OnScrollListener. El objetivo es detectar la dirección movimiento del scroll y para ello contamos con el método onScroll que proporciona la posición del primer elemento o item de la lista mostrado en pantalla. La estrategia a seguir, basada en esta clase, será comprobar si la posición del elemento actual es posterior a la posición del elemento recibido en la anterior ejecución del método en cuyo caso se está realizando un scroll hacia arriba, mientras que si la posición del elemento actual es inferior se realiza un scroll hacia abajo. En caso que los elementos sean el mismo, detectaremos la dirección del scroll comprobando la posición actual del elemento en pantalla con la posición que tenía la vez anterior que se ejecutó el método, así se evita que sólo se pueda ocultar el FAB si cambia el primer elemento mostrado en la ListView lo que sería problemático si las filas del ListView son muy altas.

El listener se ha implementado de forma genérica y debe recibir la implementación de una interfaz llamada Action con las operaciones a realizar al detectarse un movimiento de scroll en la lista que cumpla con lo expuesto anteriormente.

package com.danielme.android.fab;

import android.support.annotation.NonNull;
import android.widget.AbsListView;
import android.widget.ListView;


public class OnScrollUpDownListener implements AbsListView.OnScrollListener {
    private int previousScrollPosition;
    private int previousItemPosition;
    private int minDistance;
    private ListView listView;
    private Action action;

    public interface Action {
        void up();

        void down();
    }

    public OnScrollUpDownListener(@NonNull ListView listView, int minDistance, @NonNull Action action) {
        this.listView = listView;
        this.minDistance = minDistance;
        this.action = action;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        //nothing here
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        int currentScrollPosition = getCurrentScrollPosition();
        if (firstVisibleItem == previousItemPosition) {
            int scrolled = Math.abs(previousScrollPosition - currentScrollPosition);
            if (scrolled > minDistance) {
                if (previousScrollPosition > currentScrollPosition) {
                    action.up();
                } else {
                    action.down();
                }
            }
        } else if (firstVisibleItem > previousItemPosition) {
            action.up();
        } else {
            action.down();
        }
        previousScrollPosition = currentScrollPosition;
        previousItemPosition = firstVisibleItem;

    }

    private int getCurrentScrollPosition() {
        int pos = 0;
        if (listView.getChildAt(0) != null) {
            pos = listView.getChildAt(0).getTop();
        }
        return pos;
    }

}

En la Activity se implementa la interfaz Action definida en el anterior listener. Para la animación mostrar/ocultar tenemos dos opciones:

  • Utilizar los métodos show/hide de FAB que ya incluyen una animación de tipo zoom in/zoom out.
  •     OnScrollUpDownListener.Action scrollAction = new OnScrollUpDownListener.Action() {
    
                @Override
                public void up() {
                    floatingActionButton.hide();
                }
    
                @Override
                public void down() {
                    floatingActionButton.show();
                }
    
            };
    listView.setOnScrollListener(new OnScrollUpDownListener(listView, 8, scrollAction));
    

    android fab

  • Aplicar una animación personalizada recurriendo a ViewPropertyAnimator. Puesto que en Android 2 no tenemos esta API de animaciones, si se necesita retrocompatibilidad como en el presente proyecto se puede recurrir a NineOldAndroids que proporciona una implementación de esta API para versiones antigüas de Android.

        compile 'com.nineoldandroids:library:2.4.0+'
    

Vamos a crear una animación que consistirá en una simple traslación vertical del FAB teniéndose en cuenta su tamaño y margen.

        OnScrollUpDownListener.Action scrollAction = new OnScrollUpDownListener.Action() {

            private boolean hidden = true;

            @Override
            public void up() {
                if (hidden) {
                    hidden = false;
                    animate(floatingActionButton)
                            .translationY(floatingActionButton.getHeight() +
                                    getResources().getDimension(R.dimen.fab_margin))
                            .setInterpolator(new LinearInterpolator())
                            .setDuration(DURATION);
                }
            }

            @Override
            public void down() {
                if (!hidden) {
                    hidden = true;
                    animate(floatingActionButton)
                            .translationY(0)
                            .setInterpolator(new LinearInterpolator())
                            .setDuration(DURATION);

                }
            }

        };

        listView.setOnScrollListener(new OnScrollUpDownListener(listView, 8, scrollAction));
...

El siguiente video muestra el resultado final en Lollipop



Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

2 comentarios sobre “Diseño Android: Floating Action Button con Design Support Library

  1. Buenos días,

    Lo primero agradecer este contenido de tanto valor, muy buena información y bien redactado, por ello lo comparto en twitter para que le pueda ser de utilidad a muchas más personas.(@AlvaroIVM)

    -Pero tengo un problemilla…

    Mi archivo build.gradle al incluirle la linea «compile» quedo así:

    apply plugin: ‘com.android.application’

    android {
    compileSdkVersion 23
    buildToolsVersion «23.0.2»

    defaultConfig {
    applicationId «com.javs.incidence»
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName «1.0»
    }
    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
    }
    }
    }

    dependencies {
    compile fileTree(dir: ‘libs’, include: [‘*.jar’])
    testCompile ‘junit:junit:4.12’
    compile ‘com.android.support:appcompat-v7:23.1.1’
    compile ‘com.android.support:design:22.2.0’
    }

    pero al sincronizar me da este error:

    Error:Execution failed for task ‘:app:mergeDebugResources’.

    > Some file crunching failed, see logs for details

    cualquier ayuda o indicación sera de gran ayuda.

    De antemano muchas gracias y un saludo!

    1. Este tipo de errores suelen deberse a problemas en el propio Android Studio, a veces se solucionan construyendo de nuevo el proyecto y/o actualizando Gradle o el IDE.

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.