最近想做一个悬浮窗秒表的功能,所以看下悬浮窗具体的实现步骤
1、初识WindowManager
实现悬浮窗主要用到的是WindowManager
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
...
}
WindowManager是接口类,继承自接口ViewManager,可以通过获取WINDOW_SERVICE系统服务得到。而ViewManager接口有addView方法,我们就是通过这个方法将悬浮窗控件加入到屏幕中去。
2、设置权限
当API Level >= 23,显示悬浮窗功能,需要在清单文件AndroidManifest.xml中添加SYSTEM_ALERT_WINDOW权限,添加这个权限后才可以在其他应用上显示悬浮窗。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
通过getSystemService方式获取WindowManager
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
3、LayoutParam设置
WindowManager的addView方法有两个参数,一个是需要加入的控件对象,另一个参数是WindowManager.LayoutParams对象。
// view – The view to be added to this window.
// params – The LayoutParams to assign to view.
public void addView(View view, ViewGroup.LayoutParams params);
其中LayoutParams的type变量,这个变量是用来指定窗口的类型。在设置这个变量时,需要对不同版本的Android系统进行适配。
val layoutParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE
}
在Android 8.0之前,悬浮窗口设置可以为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口,现在这个类型已弃用了。
而Android 8.0对系统和API行为做了修改,包括使用SYSTEM_ALERT_WINDOW权限的应用无法再使用窗口类型来在其他应用和窗口上方显示提醒窗口:
-
TYPE_PHONE(已弃用)
-
TYPE_PRIORITY_PHONE
-
TYPE_SYSTEM_ALERT
-
TYPE_SYSTEM_OVERLAY
-
TYPE_SYSTEM_ERROR
如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须该为TYPE_APPLICATION_OVERLAY的新类型。
4、检测是否允许开启悬浮窗
开启悬浮窗之前,还需要检测用户是否允许开启悬浮窗,通过系统提供的canDrawOverlays来检测
//检测是否允许开启悬浮窗
Settings.canDrawOverlays(context)
如果没有允许开启,需要跳转开启页面,让用户允许开启悬浮窗
startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
5、FloatingService服务
悬浮窗一直显示在其他应用上层,需要新建FloatingService服务类,用于处理悬浮窗相关逻辑。
class FloatingService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
/**
* 显示悬浮窗
*/
private fun showFloatingWindow() {
// 获取WindowManager服务
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 新建悬浮窗控件
val button = Button(applicationContext)
button.text = "Floating Window"
button.setBackgroundColor(Color.BLUE)
// 设置LayoutParam
val layoutParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE
}
layoutParams.format = PixelFormat.RGBA_8888
layoutParams.flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
layoutParams.width = ActionBar.LayoutParams.WRAP_CONTENT
layoutParams.height = ActionBar.LayoutParams.WRAP_CONTENT
layoutParams.x = 300
layoutParams.y = 300
// 将悬浮窗控件添加到WindowManager
windowManager.addView(button, layoutParams);
}
}
6、启动FloatingService
viewBinding.btnFloating.setOnClickListener {
if (Settings.canDrawOverlays(this)) {//检测是否具有悬浮窗权限
startService(Intent(this,FloatingService::class.java))
} else {
startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
}
}
开启效果如下:
7、增加拖动功能
悬浮窗显示的位置可能会遮挡其他信息,这时就需要新增拖动功能,可以拖动到任何位置,实现的逻辑就是给布局View添加触摸事件,根据触摸和移动的位置来决定悬浮窗显示的位置。
var x = 0
var y = 0
button.setOnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
x = event.rawX.toInt()
y = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val nowX = event.rawX.toInt()
val nowY = event.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
layoutParams.x = layoutParams.x + movedX
layoutParams.y = layoutParams.y + movedY
// 更新悬浮窗控件布局
windowManager.updateViewLayout(view, layoutParams)
}
else -> {}
}
false
}
8、图片自动播放
效果图如下:
页面布局layout_floating_image.xml如下:
<?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"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imgView"
android:layout_width="wrap_content"
android:contentDescription="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
FloatingImageService服务如下:
class FloatingImageService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
/**
* 显示悬浮窗
*/
@SuppressLint("ClickableViewAccessibility", "InflateParams")
private fun showFloatingWindow() {
// 获取WindowManager服务
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 获取悬浮窗布局
val viewBinding = LayoutFloatingImageBinding.inflate(LayoutInflater.from(this))
val imageArray = intArrayOf(R.drawable.pic1, R.drawable.pic2, R.drawable.pic3)
var imageIndex = 0
viewBinding.imgView.setImageResource(imageArray[imageIndex])
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
while (true) {
delay(2000)
imageIndex++
if (imageIndex == imageArray.size) {
imageIndex = 0
}
withContext(Dispatchers.Main) {
viewBinding.imgView.setImageResource(imageArray[imageIndex])
}
}
}
// 设置LayoutParam
val layoutParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE
}
layoutParams.format = PixelFormat.RGBA_8888
layoutParams.gravity = Gravity.START or Gravity.TOP
layoutParams.flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
layoutParams.width = ActionBar.LayoutParams.WRAP_CONTENT
layoutParams.height = ActionBar.LayoutParams.WRAP_CONTENT
layoutParams.x = 0
layoutParams.y = 0
// 将悬浮窗控件添加到WindowManager
windowManager.addView(viewBinding.root, layoutParams)
var x = 0
var y = 0
viewBinding.root.setOnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
x = event.rawX.toInt()
y = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val nowX = event.rawX.toInt()
val nowY = event.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
layoutParams.x = layoutParams.x + movedX
layoutParams.y = layoutParams.y + movedY
// 更新悬浮窗控件布局
windowManager.updateViewLayout(view, layoutParams)
}
else -> {}
}
false
}
}
}