Desde hace algún tiempo, la aplicación Facebook Messenger dispone de la funcionalidad de las “Chat Heads” o “Burbujas de Chat” para facilitar el acceso a las conversaciones activas desde cualquier aplicación que se esté ejecutando. Detrás de esta funcionalidad existen cosas realmente interesantes que nos pueden servir en aplicaciones que estemos desarrollando. ¡Vamos a ver cómo puedes crear tus propias burbujas de Chat como Facebook Messenger!.

Facebook Messenger introdujo, algo más de hace un año, el concepto de las “burbujas” para notificar al usuario de mensajes no leídos en su App. Esta funcionalidad, junto a la de notificaciones Push, potenciaba de manera notable la facilidad de uso con dicha herramienta. Podíamos tener disponible, sin que tuviéramos que entrar en la APP, las conversaciones más importantes a mano mientras estábamos utilizando nuestro smartphone.

El concepto

La idea es sencilla: mostrar un círculo con la imagen de nuestro interlocutor, en la parte de la pantalla que más nos convenga, siempre que sea necesario. Al pulsar ese círculo se abrirá la conversación y podremos interactuar con ella.

Estos botones se pueden mover de un lado para otro pero siempre estarán encima de las demás aplicaciones que estemos utilizando. En caso de que no nos interese mostrarla podremos eliminarla de la pantalla cuando así lo deseemos.

Pero… ¿Cómo funciona esto? ¿Qué debemos tener en cuenta si queremos implementar esta funcionalidad en una de nuestras aplicaciones?

Cómo funciona

A continuación veremos, con la ayuda de un ejemplo, cómo podemos utilizar esta opción pero antes comentaremos cuáles son las claves para que todo funcione correctamente.

Por un lado tenemos que diseñar la parte que queremos mostrar siempre, lo que es la burbuja. Este será una layout normal que tendrá su diseño y comportamiento propios. En nuestro caso será una imagen circular con otro elemento que se utilizara para eliminar el elemento.

Por otro lado, tendremos que tener un Servicio que se encargue de mostrar el layout diseñado y gestione los eventos que en se den en él. Por otra parte, en este servicio tendremos que controlar el movimiento y posicionamiento del elemento en la pantalla, una cosa que dará mucha libertad al usuario.

Y como tercera pata de este ejemplo tendremos que añadir el permiso de tipo SYSTEM_ALERT_WINDOW en el Manifest del proyecto. Sin este permiso la app no podrá posicionar elementos sobre otras aplicaciones por lo que es de vital importancia para que todo funcione correctamente.

Teniendo en consideración estas tres cosas pongámonos manos a la obra con el ejemplo.

App “Burbuilak”

  1. Creamos una nueva aplicación con una Empty Activity
  2. Añadimos el permiso SYSTEM_ALERT_WINDOW
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  3. Crearemos un layout que utilizaremos en la burbuja, vamos, lo que se verá sobre todo lo demás. La llamaremos bubble_layout y tendrá este código. (como imágenes a mostrar utilizad los del ejemplo o descargaros las vuestras propias).
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="65dp"
        android:id="@+id/buble_head"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    
        <ImageView
            android:id="@+id/chat_head_profile_iv"
            android:layout_width="75dp"
            android:layout_height="75dp"
            android:layout_marginTop="8dp"
            android:src="@drawable/example"
            tools:ignore="ContentDescription"/>
    
        <ImageView
            android:id="@+id/close_btn"
            android:layout_width="26dp"
            android:layout_height="26dp"
            android:layout_marginLeft="50dp"
            android:layout_marginTop="8dp"
            android:src="@drawable/ic_close"
            tools:ignore="ContentDescription"/>
    </RelativeLayout>
  4. Teniendo ya el layout a mostrar, debemos crear el servicio. Este servicio inflará el layout del paso anterior y controlará los eventos que se den en ella, así como el movimiento y posicionamiento. Deberemos implementar un touchlistener que controle diferentes MotionEvents. Estos eventos servirán para saber si estamos moviendo la burbuja o haciendo una pulsación. Cuando se detecta una pulsación se abrirá una actividad para verificar que todo ha ido bien (lo crearemos más adelante).
    Además de eso, a la imagen close_btn, dentro de la función onCreate, le asociaremos un escuchador de clicks. Dentro cerrará la burbuja y la app.
    Creamos una nueva clase Java llamada BubbleHeadService y que extienda de Service. El código será este:

    public class BubbleHeadService extends Service {
        private WindowManager mWindowManager;
        private View mChatHeadView;
    
    
        public BubbleHeadService() {
        }
    
    
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
    
        @Override
        public void onCreate() {
            super.onCreate();
            mChatHeadView = LayoutInflater.from(this).inflate(R.layout.bubble_layout, null);
    
    
            final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                    WindowManager.LayoutParams.WRAP_CONTENT,
                    WindowManager.LayoutParams.WRAP_CONTENT,
                    WindowManager.LayoutParams.TYPE_PHONE,
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    PixelFormat.TRANSLUCENT);
    
            params.gravity = Gravity.TOP | Gravity.LEFT;
            params.x = 0;
            params.y = 100;
    
            mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
            mWindowManager.addView(mChatHeadView, params);
    
            mageView closeButton = (ImageView) mChatHeadView.findViewById(R.id.close_btn);
            closeButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    stopSelf();
                }
            });
    
            final ImageView chatHeadImage = (ImageView) mChatHeadView.findViewById(R.id.chat_head_profile_iv);
            chatHeadImage.setOnTouchListener(new View.OnTouchListener() {
                private int lastAction;
                private int initialX;
                private int initialY;
                private float initialTouchX;
                private float initialTouchY;
                private static final int MAX_CLICK_DURATION = 200;
                private long startClickTime;
    
    
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            startClickTime = Calendar.getInstance().getTimeInMillis();
                            initialX = params.x;
                            initialY = params.y;
    
                            initialTouchX = event.getRawX();
                            initialTouchY = event.getRawY();
    
                            lastAction = event.getAction();
    
                            return true;
                        case MotionEvent.ACTION_UP:
                            
                            long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                            if(clickDuration < MAX_CLICK_DURATION){
                                Intent intent = new Intent(BubbleHeadService.this, BubbleActivity.class);
                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                startActivity(intent);
    
                                stopSelf();
                            }
                            lastAction = event.getAction();
                            return true;
                        case MotionEvent.ACTION_MOVE:
                            params.x = initialX + (int) (event.getRawX() - initialTouchX);
                            params.y = initialY + (int) (event.getRawY() - initialTouchY);
    
                            mWindowManager.updateViewLayout(mChatHeadView, params);
                            lastAction = event.getAction();
                            return true;
                    }
                    return false;
                }
            });
    
        }
    
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            if (mChatHeadView != null) mWindowManager.removeView(mChatHeadView);
        }
  5. Debemos añadir en el Manifest, dentro del application, el servicio que acabamos de crear.
    <service
                android:name=".BubbleHeadService"
                android:enabled="true"
                android:exported="false"/>
  6. Como hemos comentado en el paso 4, debemos crear la actividad que se cargará cuando pulsemos la burbuja. Para que concuerde con el código que estamos utilizando como ejemplo le vamos a poner de nombre BubbleActivity. Nos aseguramos de que esté incluído en el Manifest.
  7. Por ahora hemos creado todo lo relacionado con la burbuja que vamos a mostrar y su comportamiento. Pero todavía no hemos hecho nada relacionado con “cómo mostrarlo”. Para ello debemos de crear un botón en el MainActivity que muestre la burbuja cuando se pulse.
    En el layout activity_main reemplazamos el código por este:

    <android.support.constraint.ConstraintLayout 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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/bubble_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:gravity="center"
            android:text="Crear burbuja"/>
    
    </android.support.constraint.ConstraintLayout>
  8. Ya solo nos falta meter algo de código en el MainActivity y ya está. Pero en esta parte debemos incluir algo importante para nuestro objetivo. Aparte del control del botón que acabamos de crear, debemos gestionar la solicitud de permisos para que elementos que nos interesen aparezcan encima de otros. Para ello debemos crear un Intent que nos muestre la pregunta sobre los permisos, y dependiendo de lo que introduzca el usuario, inicializar la burbuja o cerrar la aplicación.
    El código del MainActivity quedaría parecido a este:

    public class MainActivity extends AppCompatActivity {
    
        private static final int CODE_DRAW_OVER_OTHER_APP_PERMISSION = 2084;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
    
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, CODE_DRAW_OVER_OTHER_APP_PERMISSION);
            } else {
                initializeView();
            }
        }
    
        private void initializeView() {
            findViewById(R.id.bubble_button).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    startService(new Intent(MainActivity.this, BubbleHeadService.class));
                    finish();
                }
            });
        }
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (requestCode == CODE_DRAW_OVER_OTHER_APP_PERMISSION) {
    
                if (Settings.canDrawOverlays(this)) {
                    initializeView();
                } else { 
                    Toast.makeText(this,
                            "El permiso para posicionar elementos sobre otras aplicaciones no está disponible. Cerrando aplicación",
                            Toast.LENGTH_SHORT).show();
    
                    finish();
                }
            } else {
                super.onActivityResult(requestCode, resultCode, data);
            }
        }
    }
    
  9. Ahora ya tenemos todo listo para realizar la prueba. Ejecutamos y jugamos con la burbuja XD.