Floating Widget in Android Sep 30th 2020 Words: 1k

I want to add a floating widget that draws over other apps to my application, and allow the user to change it position by dragging, as the following demo shows.

The answer I found on the Internet is either outdated or not working at all, that is why I write this post.

My app have a main activity, that controls the start and the stop of a service who will add the widget using the WindowManager.

Android Manifest

To draw over the screen, android.permission.SYSTEM_ALERT_WINDOW must be declared in the Android manifest.

AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nogroup.prinputmapping">

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

<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">
<service
android:name=".FloatingWidgetService"
android:enabled="true"
android:exported="false"></service>

<activity android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</manifest>

Main Activity

activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/click_to_test_service"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/make_it_rain"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />

<Button
android:id="@+id/button_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_start" />

</androidx.constraintlayout.widget.ConstraintLayout>

Check if the app can draw over other apps with Settings.canDrawOverlays()

MainActivity.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.nogroup.prinputmapping

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

const val REQ_DRAW_OVERLAY_PERMISSION = 0

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val buttonStart: Button = findViewById(R.id.button_start)
buttonStart.setOnClickListener {
if (!Settings.canDrawOverlays(this)) { // If permission not granted, take user to the settings
Toast.makeText(this, "Please grant permission", Toast.LENGTH_SHORT).show()
startActivityForResult(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
), REQ_DRAW_OVERLAY_PERMISSION
)
} else {
startService(Intent(applicationContext, FloatingWidgetService::class.java))
}
}
val buttonStop: Button = findViewById(R.id.button_stop)
buttonStop.setOnClickListener {
stopService(Intent(applicationContext, FloatingWidgetService::class.java))
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQ_DRAW_OVERLAY_PERMISSION -> {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
startService(Intent(this@MainActivity, FloatingWidgetService::class.java))
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
}

Floating Widget Service

floating_widget.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<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/float_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
android:id="@+id/button_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
FloatingWidgetService.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.nogroup.prinputmapping

import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.PixelFormat
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.view.*
import android.widget.Button
import android.widget.Toast


const val DEFAULT_NOTIFICATION_CHANNEL_ID = "DEFAULT"

class FloatingWidgetService : Service() {
var isMoving = false
lateinit var mLayout: View
lateinit var mLayoutParams: WindowManager.LayoutParams
lateinit var mWindowManager: WindowManager

override fun onBind(intent: Intent): IBinder {
return object : Binder() {}
}

@SuppressLint("ClickableViewAccessibility")
override fun onCreate() {
super.onCreate()

// create overlay
mLayout = LayoutInflater.from(applicationContext).inflate(
R.layout.floating_widget,
null
)
// add view
mWindowManager = (getSystemService(WINDOW_SERVICE) as WindowManager)
WindowManager.LayoutParams().apply {
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
format = PixelFormat.TRANSLUCENT
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.TOP or Gravity.START
x = 0
y = 100
flags = flags or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
}.let { layoutParams ->
mLayoutParams = layoutParams
mWindowManager.addView(mLayout, layoutParams)
}
// set button listener
mLayout.findViewById<Button>(R.id.button_start).apply {
setOnClickListener {
Toast.makeText(applicationContext, "widget clicked", Toast.LENGTH_SHORT)
.show()
}
setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isMoving = false
}
MotionEvent.ACTION_MOVE -> {
isMoving = true
mLayoutParams.x = event.rawX.toInt() - mLayout.measuredWidth / 2
mLayoutParams.y = event.rawY.toInt() - mLayout.measuredHeight / 2
mWindowManager.updateViewLayout(mLayout, mLayoutParams)
return@setOnTouchListener true
}
MotionEvent.ACTION_UP -> {
return@setOnTouchListener isMoving
}
}
return@setOnTouchListener false
}
}
}

override fun onDestroy() {
unbindService(mConnection)
mWindowManager.removeView(mLayout)
super.onDestroy()
}
}