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 an android floating widget that’ll stay on the screen even when the application is in the background. This feature is handy to use for multitasking such as switching easily between applications.
Table of Contents
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 an 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:
- Display a floating action button as the overlay view when the application is in the background. We’ll be using CounterFab library.
- Dragging the widget anywhere on the screen.
- Letting the widget position itself along the nearest edge of the screen (instead of keeping it hanging in the middle).
- Click the widget to launch back the application and pass the data from the Service to the Activity.
- Add android floating widget by clicking a button from our application.
- 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
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="https://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="https://schemas.android.com/apk/res/android"
xmlns:tools="https://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="https://schemas.android.com/apk/res/android"
xmlns:app="https://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.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(MainActivity.this)) {
startService(new Intent(MainActivity.this, FloatingWidgetService.class));
} else {
errorToast();
}
}
});
}
private void askForSystemOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
//If the draw over permission is not available 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 || Settings.canDrawOverlays(this)) {
startService(new Intent(MainActivity.this, FloatingWidgetService.class).putExtra("activity_background", true));
finish();
} else {
errorToast();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DRAW_OVER_OTHER_APP_PERMISSION) {
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();
}
}
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:
- Unlike an Activity, we need to explicitly set the Theme in a Service using
setTheme()
method. Failing to do so would cause IllegalArgumentException. - We’ve created an instance of WindowManager and added the
overlay_layout
to the top-left of the screen in the above code. - 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. - 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.
There are a few things left to implement to complete the application.
- Auto positioning the widget to the nearest edge of the screen (left/right).
- Clicking the widget should launch the application. (We’ll be possibly passing data from the service to the activity).
- 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="https://schemas.android.com/apk/res/android"
xmlns:tools="https://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()
- 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 whenonPause()
is invoked in the activity that we’ll be seeing shortly.if (intent != null) { activity_background = intent.getBooleanExtra("activity_background", false); }
- We’d placed a null checker on the
mOverlayView
instance to update the CounterFab badge count if it already exists. - 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.
- We need to subtract the width of the android floating widget from the display width of the screen.
We useGlobalLayoutListener
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 usingGlobalLayoutListener
such aslayout.getWidth()
orcounterFab.getWidth()
would return 0 since the view hasn’t been laid yet.GlobalLayoutListener
callback gets triggered only after the view is properly laid - 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 invokesonDestroy()
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(MainActivity.this)) {
startService(new Intent(MainActivity.this, FloatingWidgetService.class));
} else {
errorToast();
}
}
});
}
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 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 || Settings.canDrawOverlays(this)) {
startService(new Intent(MainActivity.this, FloatingWidgetService.class).putExtra("activity_background", true));
finish();
} else {
errorToast();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DRAW_OVER_OTHER_APP_PERMISSION) {
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.
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 trash. We’ll look into that in a later tutorial. You can download the final Android FloatingChatHeads Project from the link below.
For the people who wonder what are “requestCode, resultCode, data” related to in the tutorial, you need to download the final project source code at the end of the tutorial.
MainActivity.java code in the explained tutorial is wrong and full of errors, only the file in the .zip is correct.
this was not working in android 10 because of background services what to do next sir kindly fix it
because of – permission denied for window type 2002?
Can we do the continues location update through this widget ?
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?
code not working button app closing and showing error in this line “mWindowManager.addView(mOverlayView,params);”
Nice code. I like it.
super.onActivityResult(requestCode, resultCode, data);
what is the value of the requestCode , resultCode and data
did you find the answer?
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.
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?