Android Floating Widget Like Messenger Bubbles

Filed Under: Android

If you’ve used the Facebook Messenger Application anytime, you must have come across the chat bubbles that can be seen floating on your screen irrespective of which application you’re using currently. In this tutorial, we’ll be discussing and implementing android floating widget that’ll stay on the screen even when the application is in the background. This feature is handy to use for multi tasking such as switching easily between applications.

Android Floating Widget Concept

Android Floating Widget is nothing but overlay views drawn over applications. To allow drawing views over other applications we need to add the following permission inside the AndroidManifest.xml file of our project.

android.permission.SYSTEM_ALERT_WINDOW

To display android floating widget we need to start a background service and add our custom view to an instance of WindowManager so as to keep the custom view at the top of the view hierarchy of the current screen.

The application that we’ll be developing next will have the following features in place:

  1. Display a floating action button as the overlay view when the application is in the background. We’ll be using CounterFab library.
  2. Dragging the widget anywhere on the screen.
  3. Letting the widget position itself along the nearest edge of the screen (instead of keeping it hanging in the middle).
  4. Click the widget to launch back the application and pass the data from the Service to the Activity.
  5. Add android floating widget by clicking a button from our application.
  6. Keep a badge count over the FAB displaying the number of times the widget was created (let’s say it denotes the number of messages).

Android Floating Widget Example Project Structure

android floating widget example project structure

The project consists of a single activity and a background service.

Android Floating Widget Example Code

Before jumping into the business logic of our application let’s see the AndroidManifest.xml file once.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.journaldev.floatingchatheads">


    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="TASKS"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".FloatingWidgetService"
            android:enabled="true"
            android:exported="false" />


    </application>

</manifest>

Add the following dependency inside the build.gradle of your project

compile 'com.github.andremion:counterfab:1.0.1'

The xml code for activity_main.xml layout is given below.


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.journaldev.floatingchatheads.MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />

</RelativeLayout>

The layout for the android floating widget is mentioned in the overlay_layout.xml file as shown below:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:orientation="vertical"
    android:visibility="visible">


    <com.andremion.counterfab.CounterFab
        android:id="@+id/fabHead"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_input_add"
        app:fabSize="normal" />


</RelativeLayout>

The code for the MainActivity.java class is given below :


package com.journaldev.floatingchatheads;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {


    private static final int DRAW_OVER_OTHER_APP_PERMISSION = 123;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        askForSystemOverlayPermission();


    }

    private void askForSystemOverlayPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {

            //If the draw over permission is not available to open the settings screen
            //to grant the permission.
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, DRAW_OVER_OTHER_APP_PERMISSION);
        }
    }


    @Override
    protected void onPause() {
        super.onPause();


        // To prevent starting the service if the required permission is NOT granted.
        if (Build.VERSION.SDK_INT = Build.VERSION_CODES.M) {
                if (!Settings.canDrawOverlays(this)) {
                    //Permission is not available. Display error text.
                    Toast.makeText(this, "Draw over other app permission not available. Can't start the application without the permission.", Toast.LENGTH_LONG).show();
                    finish();
                }
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

}

In the above code, we check if the permission to draw view over other apps is enabled or not.
We start the background service intent namely FloatingWidgetService.java when the onPause() method is invoked(signalling that the application is in background).

The code for FloatingWidgetService.java is given below:


package com.journaldev.floatingchatheads;

import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

import com.andremion.counterfab.CounterFab;

/**
 * Created by anupamchugh on 01/08/17.
 */

public class FloatingWidgetService extends Service {


    private WindowManager mWindowManager;
    private View mOverlayView;
    CounterFab counterFab;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public void onCreate() {
        super.onCreate();

        setTheme(R.style.AppTheme);

        mOverlayView = LayoutInflater.from(this).inflate(R.layout.overlay_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);


        //Specify the view position
        params.gravity = Gravity.TOP | Gravity.LEFT;        //Initially view will be added to top-left corner
        params.x = 0;
        params.y = 100;


        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        mWindowManager.addView(mOverlayView, params);

      
        counterFab = (CounterFab) mOverlayView.findViewById(R.id.fabHead);
        counterFab.setCount(1);

        counterFab.setOnTouchListener(new View.OnTouchListener() {
            private int initialX;
            private int initialY;
            private float initialTouchX;
            private float initialTouchY;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:

                        //remember the initial position.
                        initialX = params.x;
                        initialY = params.y;


                        //get the touch location
                        initialTouchX = event.getRawX();
                        initialTouchY = event.getRawY();


                        return true;
                    case MotionEvent.ACTION_UP:

                        //Add code for launching application and positioning the widget to nearest edge.
                      
                
                         return true;
                    case MotionEvent.ACTION_MOVE:


                        float Xdiff = Math.round(event.getRawX() - initialTouchX);
                        float Ydiff = Math.round(event.getRawY() - initialTouchY);


                        //Calculate the X and Y coordinates of the view.
                        params.x = initialX + (int) Xdiff;
                        params.y = initialY + (int) Ydiff;

                        //Update the layout with new X & Y coordinates
                        mWindowManager.updateViewLayout(mOverlayView, params);


                        return true;
                }
                return false;
            }
        });


    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mOverlayView != null)
            mWindowManager.removeView(mOverlayView);
    }

}

Few inferences drawn from the above code are:

  1. Unlike an Activity, we need to explicitly set the Theme in a Service using setTheme() method. Failing to do so would cause IllegalArgumentException.
  2. We’ve created an instance of WindowManager and added the overlay_layout to the top-left of the screen in the above code.
  3. To drag the floating widget along the screen, we’ve overridden the onTouchListener() to listen to drag events and change the X and Y coordinates of the overlay view on the screen.
  4. We’ve set the badge count of the CounterFab class as 1 by invoking the method setCount().

The output that the above piece of code gives us is given below.
android floating widget example output

There are a few things left to implement to complete the application.

  1. Auto positioning the widget to the nearest edge of the screen (left/right).
  2. Clicking the widget should launch the application. (We’ll be possibly passing data from the service to the activity).
  3. Adding a button inside the activity to create android floating widget. (Instead of creating new view for each invocation we’ll be just incrementing the badge count).

Let’s get started with adding a button inside the activity_main.xml layout as shown below:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.journaldev.floatingchatheads.MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />


    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_margin="16dp"
        android:text="ADD FLOATING BUTTON" />

</RelativeLayout>

The code for FloatingWidgetService.java class is given below :


package com.journaldev.floatingchatheads;

import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import com.andremion.counterfab.CounterFab;

/**
 * Created by anupamchugh on 01/08/17.
 */

public class FloatingWidgetService extends Service {


    private WindowManager mWindowManager;
    private View mOverlayView;
    int mWidth;
    CounterFab counterFab;
    boolean activity_background;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (intent != null) {
            activity_background = intent.getBooleanExtra("activity_background", false);

        }

        if (mOverlayView == null) {

            mOverlayView = LayoutInflater.from(this).inflate(R.layout.overlay_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);


            //Specify the view position
            params.gravity = Gravity.TOP | Gravity.LEFT;        //Initially view will be added to top-left corner
            params.x = 0;
            params.y = 100;


            mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
            mWindowManager.addView(mOverlayView, params);

            Display display = mWindowManager.getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);


            counterFab = (CounterFab) mOverlayView.findViewById(R.id.fabHead);
            counterFab.setCount(1);

           final RelativeLayout layout = (RelativeLayout) mOverlayView.findViewById(R.id.layout);
            ViewTreeObserver vto = layout.getViewTreeObserver();
            vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    int width = layout.getMeasuredWidth();

                   //To get the accurate middle of the screen we subtract the width of the android floating widget.
                   mWidth = size.x - width;

                }
            });

            counterFab.setOnTouchListener(new View.OnTouchListener() {
                private int initialX;
                private int initialY;
                private float initialTouchX;
                private float initialTouchY;

                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:

                            //remember the initial position.
                            initialX = params.x;
                            initialY = params.y;


                            //get the touch location
                            initialTouchX = event.getRawX();
                            initialTouchY = event.getRawY();


                            return true;
                        case MotionEvent.ACTION_UP:


                            if (activity_background) {

                                //xDiff and yDiff contain the minor changes in position when the view is clicked.
                                float xDiff = event.getRawX() - initialTouchX;
                                float yDiff = event.getRawY() - initialTouchY;

                                if ((Math.abs(xDiff) < 5) && (Math.abs(yDiff) = middle ? mWidth : 0;
                            params.x = (int) nearestXWall;


                            mWindowManager.updateViewLayout(mOverlayView, params);


                            return true;
                        case MotionEvent.ACTION_MOVE:


                            int xDiff = Math.round(event.getRawX() - initialTouchX);
                            int yDiff = Math.round(event.getRawY() - initialTouchY);


                            //Calculate the X and Y coordinates of the view.
                            params.x = initialX + xDiff;
                            params.y = initialY + yDiff;

                            //Update the layout with new X & Y coordinates
                            mWindowManager.updateViewLayout(mOverlayView, params);


                            return true;
                    }
                    return false;
                }
            });
        } else {

            counterFab.increase();

        }


        return super.onStartCommand(intent, flags, startId);


    }

    @Override
    public void onCreate() {
        super.onCreate();

        setTheme(R.style.AppTheme);


    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mOverlayView != null)
            mWindowManager.removeView(mOverlayView);
    }

}

In the above code, we’ve moved the logic from onCreate() to the onStartCommand() method. Why?
We’ll be starting the FloatingWidgetService multiple times. onCreate() method of the Service class is called only the first time. In order to update the widget and retrieve intent extras we need to shift our code into onStartCommand()

  1. We need to detect whether the activity is running in the background or not. Only if the application is running in the background we’ll launch our Activity from the Service(No point launching another instance of the activity if it’s in foreground). activity_background bundle extra is passed with the value of true when onPause() is invoked in the activity that we’ll be seeing shortly.
    
    if (intent != null) {
                activity_background = intent.getBooleanExtra("activity_background", false);
    
    }
    
  2. We’d placed a null checker on the mOverlayView instance to update the CounterFab badge count if it already exists.
  3. To auto position the view along the nearest edge we first need to find the width of the screen and store it(mWidth is the variable we’ve used). It’s done using the below code snippet.
    
    Display display = mWindowManager.getDefaultDisplay();
                Point size = new Point();
                display.getSize(size);
                
                // mWidth = size.x; //Inaccurate width of the screen since it doesn't take the width of the android floating widget in consideration.
    
  4. We need to subtract the width of the android floating widget from the display width of the screen.
    We use GlobalLayoutListener for this. It calculates the width of the view only after the view is properly laid.

    
    final RelativeLayout layout = (RelativeLayout) mOverlayView.findViewById(R.id.layout);
                ViewTreeObserver vto = layout.getViewTreeObserver();
                vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        int width = layout.getMeasuredWidth();
    
                       //To get the accurate middle of the screen we subtract the width of the floating widget in android.
                       mWidth = size.x - width;
    
                    }
                });
    

    Note: Calling getWidth() on a view directly without using GlobalLayoutListener such as layout.getWidth() or counterFab.getWidth() would return 0 since the view hasn’t been laid yet. GlobalLayoutListener callback gets triggered only after the view is properly laid

  5. Updating the view to be along the nearest edge, as well as detecting if the view was clicked, both these features would be triggered only when the user lifts his/her finger from the screen and the MotionEvent.ACTION_UP is triggered. The code for the ACTION_UP case is given below:
    
    //Only start the activity if the application is in the background. Pass the current badge_count to the activity
                                if (activity_background) {
                                    
                                    float xDiff = event.getRawX() - initialTouchX;
                                    float yDiff = event.getRawY() - initialTouchY;
    
                                    if ((Math.abs(xDiff) < 5) && (Math.abs(yDiff) = middle ? mWidth : 0;
                                params.x = (int) nearestXWall;
    
    
                                mWindowManager.updateViewLayout(mOverlayView, params);
    

    The badge_count extra is passed onto the activity when the view is clicked.
    stopSelf() is invoked to kill the service which invokes onDestroy() where the floating widget is removed from the WindowManager.

The code for MainActivity.java class is given below:


package com.journaldev.floatingchatheads;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {


    private static final int DRAW_OVER_OTHER_APP_PERMISSION = 123;
    private Button button;

    private TextView textView;


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

        askForSystemOverlayPermission();

        button = (Button) findViewById(R.id.button);
        textView = (TextView) findViewById(R.id.textView);


        int badge_count = getIntent().getIntExtra("badge_count", 0);

        textView.setText(badge_count + " messages received previously");

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (Build.VERSION.SDK_INT = Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {

            //If the draw over permission is not available to open the settings screen
            //to grant the permission.
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, DRAW_OVER_OTHER_APP_PERMISSION);
        }
    }


    @Override
    protected void onPause() {
        super.onPause();


        // To prevent starting the service if the required permission is NOT granted.
        if (Build.VERSION.SDK_INT = Build.VERSION_CODES.M) {
                if (!Settings.canDrawOverlays(this)) {
                    //Permission is not available. Display error text.
                    errorToast();
                    finish();
                }
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    private void errorToast() {
        Toast.makeText(this, "Draw over other app permission not available. Can't start the application without the permission.", Toast.LENGTH_LONG).show();
    }

}

The above code now allows starting a service by the button click. Also, it displays the current badge_count value returned from the FloatingWidgetService, the default value being 0.

The output of the above application in action is given below.

android floating widget

This brings an end to this tutorial. We’ve extracted the power of Services to display floating widgets for good use. There’s one interesting bit left though: Killing the application by moving the floating widget to a trash. We’ll look into that in a later tutorial. You can download the final Android FloatingChatHeads Project from the link below.

Comments

  1. sAm says:

    I got an error on AndroidStudio for this line:

    `super.onActivityResult(requestCode, resultCode, data);`

    Error: Cannot find symbol variable requestCode (same for resultCode and data).
    Anyone has a clue, what the issue here?

  2. mahesh says:

    code not working button app closing and showing error in this line “mWindowManager.addView(mOverlayView,params);”

  3. Badr CHOUFFAI says:

    Nice code. I like it.

  4. Bharat says:

    super.onActivityResult(requestCode, resultCode, data);
    what is the value of the requestCode , resultCode and data

    1. sukirti says:

      did you find the answer?

  5. Priyank Kasera says:

    Thanks For the tutorial but i want to do a task or i have to do different task in Floating Widget while youtube player is working just below our widget. but when i open the widget, youtube stops working. i have to play youtube player while i open the floating widget. can you help me out for this.
    thanks in advance.

  6. Jerin says:

    WindowManager.LayoutParams.TYPE_PHONE,
    This will not work in android O, it is crashing.
    We can use TYPE_APPLICATION_OVERLAY, but the problem is , i cant interact with other apps,
    is there any alternatives?

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages