Posted by: Satya Pavan Kantamani
Introduction
In Android, Floating bubbles or ChatHeads allow quick access to core features without fully opening the app. For example, replying to messages through Floating bubbles.
Floating bubbles are just views attached to WindowManager with custom flags. In this post we will see how we can attach them to the window manager, drag them and change their position.
Foreground Service
To do the Floating Bubbles we need the help of a service which will be running in the background. However, background services are restricted for apps that target Android 9 (API level 28) or higher so we need a foreground service. A foreground service is nothing but a service that executes tasks that are noticeable to the user. For notifying users, we need to show notifications in the status bar.
If you want to know more about foreground service please check out my post How To Implement a Foreground Service in Android
Example
Let’s create a simple app where at the click of a button we can start the service and inside the service, we will see how to attach a view to the window manager and make it draggable.
For doing this we might need some permissions and which we will cover below.
Implementation
Step 1
Let’s start with a layout file that has 2 buttons to start and stop a service.
<?xml version="1.0" encoding="utf-8"?> | |
<androidx.constraintlayout.widget.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/btn_start" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Start" | |
android:layout_margin="50dp" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<Button | |
android:id="@+id/btn_stop" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Stop" | |
android:layout_margin="50dp" | |
tools:ignore="MissingConstraints" | |
app:layout_constraintRight_toRightOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
Step 3
Add permissions that are required in Manifest file.
FOREGROUND_SERVICE — This permission is required to start foreground service.
SYSTEM_ALERT_WINDOW — This permission is required to draw over the other apps. We need to check whether we have the permission or navigate to setting to ask this permission
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Step 2
Let’s inflate the layout inside the activity and handle the click events. Before starting the service we have to check if we have draw overlay permissions and then we can proceed.
private fun checkHasDrawOverlayPermissions(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Settings.canDrawOverlays(this) }else{ true } }
Our activity would look like
package com.appz.screen.android_sample_floating_chat_head | |
import android.content.Intent | |
import android.net.Uri | |
import android.os.Build | |
import androidx.appcompat.app.AppCompatActivity | |
import android.os.Bundle | |
import android.provider.Settings | |
import android.view.View | |
class MainActivity : AppCompatActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
initListeners() | |
} | |
private fun initListeners() { | |
findViewById<View>(R.id.btn_start)?.setOnClickListener { | |
if(checkHasDrawOverlayPermissions()) { | |
startService(Intent(this, FloatingControlService::class.java)) | |
}else{ | |
navigateDrawPermissionSetting() | |
} | |
} | |
findViewById<View>(R.id.btn_stop)?.setOnClickListener { | |
val intentStop = Intent(this, FloatingControlService::class.java) | |
intentStop.action = ACTION_STOP_FOREGROUND | |
startService(intentStop) | |
} | |
} | |
private fun navigateDrawPermissionSetting() { | |
val intent1 = Intent( | |
Settings.ACTION_MANAGE_OVERLAY_PERMISSION, | |
Uri.parse("package:$packageName")) | |
startActivityForResult(intent1, REQUEST_CODE_DRAW_PREMISSION) | |
} | |
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | |
super.onActivityResult(requestCode, resultCode, data) | |
if(requestCode == REQUEST_CODE_DRAW_PREMISSION){ | |
checkAndStartService() | |
} | |
} | |
private fun checkAndStartService() { | |
if(checkHasDrawOverlayPermissions()) { | |
startService(Intent(this, FloatingControlService::class.java)) | |
} | |
} | |
private fun checkHasDrawOverlayPermissions(): Boolean { | |
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | |
Settings.canDrawOverlays(this) | |
}else{ | |
true | |
} | |
} | |
companion object{ | |
const val ACTION_STOP_FOREGROUND = "${BuildConfig.APPLICATION_ID}.stopfloating.service" | |
const val REQUEST_CODE_DRAW_PREMISSION = 2 | |
} | |
} |
Step 3
The important part is the service here where we inflate the view and attach it to a window.
Let’s see the part of attaching the view to windows. Suppose floatingControlView
is the required view we inflate that in OnCreate of service
if(floatingControlView == null ){ val li = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater floatingControlView = li.inflate(R.layout.layout_floating_control_view, null) as ViewGroup? }
Now call addFloatingMenu from onStartCommand method
private fun addFloatingMenu() { | |
if (floatingControlView?.parent == null) { | |
//Set layout params to display the controls over any screen. | |
val params = WindowManager.LayoutParams( | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PHONE else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, | |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | |
PixelFormat.TRANSLUCENT | |
) | |
params.height = dpToPx(50) | |
params.width = dpToPx(50) | |
iconWidth = params.width | |
iconHeight = params.height | |
screenHeight = windowManager?.defaultDisplay?.height?:0 | |
screenWidth = windowManager?.defaultDisplay?.width?:0 | |
//Initial position of the floating controls. We can customize things here | |
params.gravity = Gravity.TOP or Gravity.START | |
params.x = 0 | |
params.y = 100 | |
//Add the view to window manager | |
windowManager?.addView(floatingControlView, params) | |
} | |
} |
If you check the snippet above there are various flags of WindowManager being used let’s check some of them
Based on OS version set TYPE_PHONE or TYPE_APPLICATION_OVERLAY. TYPE_APPLICATION_OVERLAY is the one for which we require draw overlay permission.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PHONE else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
TYPE_PHONE windows are used for non-application windows providing user interaction with the phone. These windows are normally placed above all applications, but behind the status bar.
TYPE_APPLICATION_OVERLAY windows are displayed above all activity windows but below critical system windows like the status bar or IME.
The system may change the position, size, or visibility of these windows at anytime to reduce visual clutter to the user and also manage resources.
Note: In case of TYPE_APPLICATION_OVERLAY the system will adjust the importance of processes with this window type to reduce the chance of the low-memory-killer killing them.
Next we define the height and width of the view and positions where it should be displayed these can be customized as per requirement. We used dpToPx just for this conversion.
//Method to convert dp to px private fun dpToPx(dp: Int): Int { val displayMetrics = this.resources.displayMetrics return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)) }
Finally we need to check whether the view is added or not then only we can add the view else it may cause unwanted issues. To do this we can just check whether the view has any parent or not
if (floatingControlView?.parent == null) { windowManager?.addView(floatingControlView, params) }
Now, we need a foreground service for targeting 29 and above so we need to start a foreground service with a notification
private fun generateForegroundNotification() { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
val intentMainLanding = Intent(this, MainActivity::class.java) | |
val pendingIntent = | |
PendingIntent.getActivity(this, 0, intentMainLanding, 0) | |
iconNotification = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) | |
if (mNotificationManager == null) { | |
mNotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
assert(mNotificationManager != null) | |
mNotificationManager?.createNotificationChannelGroup( | |
NotificationChannelGroup("chats_group", "Chats") | |
) | |
val notificationChannel = | |
NotificationChannel("service_channel", "Service Notifications", | |
NotificationManager.IMPORTANCE_MIN) | |
notificationChannel.enableLights(false) | |
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET | |
mNotificationManager?.createNotificationChannel(notificationChannel) | |
} | |
val builder = NotificationCompat.Builder(this, "service_channel") | |
builder.setContentTitle(StringBuilder(resources.getString(R.string.app_name)).append(" service is running").toString()) | |
.setTicker(StringBuilder(resources.getString(R.string.app_name)).append("service is running").toString()) | |
.setContentText("Touch to open") // , swipe down for more options. | |
.setSmallIcon(R.drawable.ic_alaram) | |
.setPriority(NotificationCompat.PRIORITY_LOW) | |
.setWhen(0) | |
.setOnlyAlertOnce(true) | |
.setContentIntent(pendingIntent) | |
.setOngoing(true) | |
if (iconNotification != null) { | |
builder.setLargeIcon(Bitmap.createScaledBitmap(iconNotification!!, 128, 128, false)) | |
} | |
builder.color = resources.getColor(R.color.purple_200) | |
notification = builder.build() | |
startForeground(mNotificationId, notification) | |
} | |
} |
Job Offers
Now putting all this together our service would look like
package com.appz.screen.android_sample_floating_chat_head | |
import android.app.* | |
import android.content.Context | |
import android.content.Intent | |
import android.graphics.Bitmap | |
import android.graphics.BitmapFactory | |
import android.graphics.PixelFormat | |
import android.os.Build | |
import android.os.Handler | |
import android.os.IBinder | |
import android.util.DisplayMetrics | |
import android.view.* | |
import androidx.core.app.NotificationCompat | |
import com.appz.screen.android_sample_floating_chat_head.MainActivity.Companion.ACTION_STOP_FOREGROUND | |
class FloatingControlService :Service() { | |
private var windowManager: WindowManager? = null | |
private var floatingControlView: ViewGroup? = null | |
var iconHeight = 0 | |
var iconWidth = 0 | |
private var screenHeight = 0 | |
private var screenWidth = 0 | |
private var hideHandler: Handler? = null | |
private var hideRunnable: Runnable? = null | |
override fun onBind(intent: Intent?): IBinder? { | |
return null | |
} | |
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | |
if(windowManager == null) { | |
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager | |
} | |
if(floatingControlView == null ){ | |
val li = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater | |
floatingControlView = li.inflate(R.layout.layout_floating_control_view, null) as ViewGroup? | |
} | |
if (intent?.action != null && intent.action.equals( | |
ACTION_STOP_FOREGROUND, ignoreCase = true)) { | |
removeFloatingContro() | |
stopForeground(true) | |
stopSelf() | |
}else { | |
generateForegroundNotification() | |
addFloatingMenu() | |
} | |
return START_STICKY | |
//Normal Service To test sample service comment the above generateForegroundNotification() && return START_STICKY | |
// Uncomment below return statement And run the app. | |
// return START_NOT_STICKY | |
} | |
private fun removeFloatingContro() { | |
if(floatingControlView?.parent !=null) { | |
windowManager?.removeView(floatingControlView) | |
} | |
} | |
private fun addFloatingMenu() { | |
if (floatingControlView?.parent == null) { | |
//Set layout params to display the controls over any screen. | |
val params = WindowManager.LayoutParams( | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PHONE else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, | |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | |
PixelFormat.TRANSLUCENT | |
) | |
params.height = dpToPx(50) | |
params.width = dpToPx(50) | |
iconWidth = params.width | |
iconHeight = params.height | |
screenHeight = windowManager?.defaultDisplay?.height?:0 | |
screenWidth = windowManager?.defaultDisplay?.width?:0 | |
//Initial position of the floating controls | |
params.gravity = Gravity.TOP or Gravity.START | |
params.x = 0 | |
params.y = 100 | |
//Add the view to window manager | |
windowManager?.addView(floatingControlView, params) | |
} | |
} | |
private fun openActivity() { | |
val intent = Intent(this, MainActivity.javaClass) | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
startActivity(intent); | |
} | |
//Notififcation for ON-going | |
private var iconNotification: Bitmap? = null | |
private var notification: Notification? = null | |
var mNotificationManager: NotificationManager? = null | |
private val mNotificationId = 123 | |
private fun generateForegroundNotification() { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
val intentMainLanding = Intent(this, MainActivity::class.java) | |
val pendingIntent = | |
PendingIntent.getActivity(this, 0, intentMainLanding, 0) | |
iconNotification = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) | |
if (mNotificationManager == null) { | |
mNotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
assert(mNotificationManager != null) | |
mNotificationManager?.createNotificationChannelGroup( | |
NotificationChannelGroup("chats_group", "Chats") | |
) | |
val notificationChannel = | |
NotificationChannel("service_channel", "Service Notifications", | |
NotificationManager.IMPORTANCE_MIN) | |
notificationChannel.enableLights(false) | |
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET | |
mNotificationManager?.createNotificationChannel(notificationChannel) | |
} | |
val builder = NotificationCompat.Builder(this, "service_channel") | |
builder.setContentTitle(StringBuilder(resources.getString(R.string.app_name)).append(" service is running").toString()) | |
.setTicker(StringBuilder(resources.getString(R.string.app_name)).append("service is running").toString()) | |
.setContentText("Touch to open") // , swipe down for more options. | |
.setSmallIcon(R.drawable.ic_alaram) | |
.setPriority(NotificationCompat.PRIORITY_LOW) | |
.setWhen(0) | |
.setOnlyAlertOnce(true) | |
.setContentIntent(pendingIntent) | |
.setOngoing(true) | |
if (iconNotification != null) { | |
builder.setLargeIcon(Bitmap.createScaledBitmap(iconNotification!!, 128, 128, false)) | |
} | |
builder.color = resources.getColor(R.color.purple_200) | |
notification = builder.build() | |
startForeground(mNotificationId, notification) | |
} | |
} | |
//Method to convert dp to px | |
private fun dpToPx(dp: Int): Int { | |
val displayMetrics = this.resources.displayMetrics | |
return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)) | |
} | |
} |
Now if we run the app and click on start button it prompts for permission, once granted then check that there will be a view attached to the window
Output
After providing permissions there will be an alarm icon being attached to to the WindowManager
Now that we are done adding the view we have to make sure it is draggable. For this let’s add the OnTouchListener() to the view
private fun addOnTouchListener(params: WindowManager.LayoutParams) { | |
//Add touch listerner to floating controls view to move/close/expand the controls | |
floatingControlView?.setOnTouchListener(object : View.OnTouchListener { | |
private var initialTouchX = 0f | |
private var initialTouchY = 0f | |
private var initialX = 0 | |
private var initialY = 0 | |
override fun onTouch(view: View?, motionevent: MotionEvent): Boolean { | |
val flag3: Boolean | |
flag3 = true | |
var flag = false | |
when (motionevent.action) { | |
MotionEvent.ACTION_DOWN -> { | |
params.alpha = 1.0f | |
initialX = params.x | |
initialY = params.y | |
initialTouchX = motionevent.rawX | |
initialTouchY = motionevent.rawY | |
Log.d( | |
"OnTouchListener", | |
java.lang.StringBuilder("POS: x = ") | |
.append(initialX).append(" y = ") | |
.append(initialY).append(" tx = ") | |
.append(initialTouchX) | |
.append(" ty = ") | |
.append(initialTouchY).toString() | |
) | |
return true | |
} | |
MotionEvent.ACTION_UP -> { | |
flag = flag3 | |
if (Math.abs(initialTouchX - motionevent.rawX) >= 25f) { | |
return flag | |
} else { | |
flag = flag3 | |
if (Math.abs( | |
initialTouchY | |
- motionevent.rawY | |
) >= 25f | |
) { | |
return flag | |
} else { | |
return true | |
} | |
} | |
} | |
MotionEvent.ACTION_MOVE -> { | |
initialX = params.x | |
initialY = params.y | |
if ((motionevent.rawX < (initialX - iconWidth / 2).toFloat()) || (motionevent.rawY < (initialY - iconHeight / 2).toFloat()) || (motionevent.rawX | |
.toDouble() > initialX.toDouble() + iconWidth.toDouble() * 1.2) | |
) { | |
} | |
params.x = (motionevent.rawX - (iconWidth / 2).toFloat()).toInt() | |
params.y = (motionevent.rawY - iconHeight.toFloat()).toInt() | |
try { | |
windowManager?.updateViewLayout(floatingControlView, params) | |
} // Misplaced declaration of an exception | |
// variable | |
catch (e: java.lang.Exception) { | |
e.printStackTrace() | |
// ExceptionHandling(e) | |
return true | |
} | |
return true | |
} | |
else -> { | |
} | |
} | |
return flag | |
} | |
}) | |
} |
Call above method after adding the view. Now the complete class together would be something like below
package com.appz.screen.android_sample_floating_chat_head | |
import android.app.* | |
import android.content.Context | |
import android.content.Intent | |
import android.graphics.Bitmap | |
import android.graphics.BitmapFactory | |
import android.graphics.PixelFormat | |
import android.os.Build | |
import android.os.Handler | |
import android.os.IBinder | |
import android.util.DisplayMetrics | |
import android.util.Log | |
import android.view.* | |
import androidx.core.app.NotificationCompat | |
import com.appz.screen.android_sample_floating_chat_head.MainActivity.Companion.ACTION_STOP_FOREGROUND | |
class FloatingControlService :Service() { | |
private var windowManager: WindowManager? = null | |
private var floatingControlView: ViewGroup? = null | |
var iconHeight = 0 | |
var iconWidth = 0 | |
private var screenHeight = 0 | |
private var screenWidth = 0 | |
private var hideHandler: Handler? = null | |
private var hideRunnable: Runnable? = null | |
override fun onBind(intent: Intent?): IBinder? { | |
return null | |
} | |
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | |
if(windowManager == null) { | |
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager | |
} | |
if(floatingControlView == null ){ | |
val li = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater | |
floatingControlView = li.inflate(R.layout.layout_floating_control_view, null) as ViewGroup? | |
} | |
if (intent?.action != null && intent.action.equals( | |
ACTION_STOP_FOREGROUND, ignoreCase = true)) { | |
removeFloatingContro() | |
stopForeground(true) | |
stopSelf() | |
}else { | |
generateForegroundNotification() | |
addFloatingMenu() | |
} | |
return START_STICKY | |
//Normal Service To test sample service comment the above generateForegroundNotification() && return START_STICKY | |
// Uncomment below return statement And run the app. | |
// return START_NOT_STICKY | |
} | |
private fun removeFloatingContro() { | |
if(floatingControlView?.parent !=null) { | |
windowManager?.removeView(floatingControlView) | |
} | |
} | |
private fun addFloatingMenu() { | |
if (floatingControlView?.parent == null) { | |
//Set layout params to display the controls over any screen. | |
val params = WindowManager.LayoutParams( | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PHONE else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, | |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | |
PixelFormat.TRANSLUCENT | |
) | |
params.height = dpToPx(50) | |
params.width = dpToPx(50) | |
iconWidth = params.width | |
iconHeight = params.height | |
screenHeight = windowManager?.defaultDisplay?.height ?: 0 | |
screenWidth = windowManager?.defaultDisplay?.width ?: 0 | |
//Initial position of the floating controls | |
params.gravity = Gravity.TOP or Gravity.START | |
params.x = 0 | |
params.y = 100 | |
//Add the view to window manager | |
windowManager?.addView(floatingControlView, params) | |
try { | |
addOnTouchListener(params) | |
} catch (e: Exception) { | |
// TODO: handle exception | |
} | |
} | |
} | |
private fun addOnTouchListener(params: WindowManager.LayoutParams) { | |
//Add touch listerner to floating controls view to move/close/expand the controls | |
floatingControlView?.setOnTouchListener(object : View.OnTouchListener { | |
private var initialTouchX = 0f | |
private var initialTouchY = 0f | |
private var initialX = 0 | |
private var initialY = 0 | |
override fun onTouch(view: View?, motionevent: MotionEvent): Boolean { | |
val flag3: Boolean | |
flag3 = true | |
var flag = false | |
when (motionevent.action) { | |
MotionEvent.ACTION_DOWN -> { | |
params.alpha = 1.0f | |
initialX = params.x | |
initialY = params.y | |
initialTouchX = motionevent.rawX | |
initialTouchY = motionevent.rawY | |
Log.d( | |
"OnTouchListener", | |
java.lang.StringBuilder("POS: x = ") | |
.append(initialX).append(" y = ") | |
.append(initialY).append(" tx = ") | |
.append(initialTouchX) | |
.append(" ty = ") | |
.append(initialTouchY).toString() | |
) | |
return true | |
} | |
MotionEvent.ACTION_UP -> { | |
flag = flag3 | |
if (Math.abs(initialTouchX - motionevent.rawX) >= 25f) { | |
return flag | |
} else { | |
flag = flag3 | |
if (Math.abs( | |
initialTouchY | |
- motionevent.rawY | |
) >= 25f | |
) { | |
return flag | |
} else { | |
return true | |
} | |
} | |
} | |
MotionEvent.ACTION_MOVE -> { | |
initialX = params.x | |
initialY = params.y | |
if ((motionevent.rawX < (initialX - iconWidth / 2).toFloat()) || (motionevent.rawY < (initialY - iconHeight / 2).toFloat()) || (motionevent.rawX | |
.toDouble() > initialX.toDouble() + iconWidth.toDouble() * 1.2) | |
) { | |
} | |
params.x = (motionevent.rawX - (iconWidth / 2).toFloat()).toInt() | |
params.y = (motionevent.rawY - iconHeight.toFloat()).toInt() | |
try { | |
windowManager?.updateViewLayout(floatingControlView, params) | |
} // Misplaced declaration of an exception | |
// variable | |
catch (e: java.lang.Exception) { | |
e.printStackTrace() | |
// ExceptionHandling(e) | |
return true | |
} | |
return true | |
} | |
else -> { | |
} | |
} | |
return flag | |
} | |
}) | |
} | |
//Notififcation for ON-going | |
private var iconNotification: Bitmap? = null | |
private var notification: Notification? = null | |
var mNotificationManager: NotificationManager? = null | |
private val mNotificationId = 123 | |
private fun generateForegroundNotification() { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
val intentMainLanding = Intent(this, MainActivity::class.java) | |
val pendingIntent = | |
PendingIntent.getActivity(this, 0, intentMainLanding, 0) | |
iconNotification = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) | |
if (mNotificationManager == null) { | |
mNotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
assert(mNotificationManager != null) | |
mNotificationManager?.createNotificationChannelGroup( | |
NotificationChannelGroup("chats_group", "Chats") | |
) | |
val notificationChannel = | |
NotificationChannel("service_channel", "Service Notifications", | |
NotificationManager.IMPORTANCE_MIN) | |
notificationChannel.enableLights(false) | |
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET | |
mNotificationManager?.createNotificationChannel(notificationChannel) | |
} | |
val builder = NotificationCompat.Builder(this, "service_channel") | |
builder.setContentTitle(StringBuilder(resources.getString(R.string.app_name)).append(" service is running").toString()) | |
.setTicker(StringBuilder(resources.getString(R.string.app_name)).append("service is running").toString()) | |
.setContentText("Touch to open") // , swipe down for more options. | |
.setSmallIcon(R.drawable.ic_alaram) | |
.setPriority(NotificationCompat.PRIORITY_LOW) | |
.setWhen(0) | |
.setOnlyAlertOnce(true) | |
.setContentIntent(pendingIntent) | |
.setOngoing(true) | |
if (iconNotification != null) { | |
builder.setLargeIcon(Bitmap.createScaledBitmap(iconNotification!!, 128, 128, false)) | |
} | |
builder.color = resources.getColor(R.color.purple_200) | |
notification = builder.build() | |
startForeground(mNotificationId, notification) | |
} | |
} | |
//Method to convert dp to px | |
private fun dpToPx(dp: Int): Int { | |
val displayMetrics = this.resources.displayMetrics | |
return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)) | |
} | |
} |
Now run and check the output that float menu can be move across the screen
Output
If you found any difficulty in executing code snippets, please check out the GitHub repo.
Summary
Floating Bubbles are the quick access points for users when they were on different apps. Exceptions are a common case so try handle max possible exceptions. We can also add customize actions like click, long click, etc depending on our requirement
Don’t forget to add the FOREGROUND_SERVICE
permission in the manifest. To start a foreground service, call startForeground()
. To stop it, call stopForeground()
. Check whether the view has parent before attaching and removing
Thank you for reading.
Resources
More Android Articles