预览效果
需要申请的权限
<!-- 用于调试模式 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>
主界面
package com.lujianfei.kotlindemo.ui.floating.view
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.lujianfei.kotlindemo.DemoApplication
import com.lujianfei.kotlindemo.R
import com.lujianfei.kotlindemo.base.Page
/**
* Author: lujianfei
* Date: 2023/12/29 9:43
* Description:
*/
@ExperimentalUnitApi
class FloatingActivity: AppCompatActivity() {
companion object {
const val REQUEST_OVERLAY_PERMISSION = 100
fun start(context:Context) {
context.startActivity(Intent(context, FloatingActivity::class.java))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Page(
leftIcon = R.drawable.ic_back,
onLeftClick = {
onBackPressed()
},
title = "悬浮窗",
onRightClick = {
},
rightText = null
) {
Content()
}
}
}
private var floatingView: View?= null
@ExperimentalUnitApi
@Composable
@Preview(showSystemUi = true)
fun Content() {
Column {
ItemButton(text = "显示悬浮窗", onClick = {
requestPermission(hasPermission = {
showFloatingView()
})
})
ItemButton(text = "关闭悬浮窗", onClick = {
closeFloatingView()
})
}
}
/**
* 关闭悬浮窗
*/
private fun closeFloatingView() {
floatingView?.let {
DemoApplication.instance.floatWindowManager.removeView(it)
}
}
/**
* 显示悬浮窗
*/
private fun showFloatingView() {
if (floatingView == null) {
floatingView = LayoutInflater.from(this@FloatingActivity)
.inflate(R.layout.floating_layout, null, false)
}
floatingView?.let {
DemoApplication.instance.floatWindowManager.addView(it)
}
}
/**
* 请求权限
*/
private fun requestPermission(hasPermission:()->Unit) {
if (!Settings.canDrawOverlays(this)) {
// 启动Activity让用户授权
kotlin.runCatching {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
intent.data = Uri.parse("package:${packageName}")
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION)
}
} else {
hasPermission.invoke()
}
}
/**
* 封装按钮
*/
@Composable
private fun ItemButton(text:String, onClick:()->Unit) {
Button(content = {
Text(text = text, modifier = Modifier.fillMaxWidth(), fontSize = 25.sp)
}, onClick = {
onClick.invoke()
}, shape = RoundedCornerShape(0.dp),
contentPadding = PaddingValues(15.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.White)
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_OVERLAY_PERMISSION-> {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this@FloatingActivity, "权限授予失败,无法开启悬浮窗", Toast.LENGTH_SHORT).show()
} else {
showFloatingView()
}
}
}
}
}
悬空窗布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="100dp"
android:layout_height="100dp"
android:text="悬浮窗"
android:gravity="center"
android:background="@color/black"
android:textColor="@color/white"
android:textSize="20sp"
/>
</FrameLayout>
配合用到的 Application
package com.lujianfei.kotlindemo
import android.app.Application
import com.lujianfei.kotlindemo.ui.floating.manager.FloatWindowManager
import kotlin.properties.Delegates
/**
* Author: lujianfei
* Date: 2023/12/28 15:05
* Description:
*/
class DemoApplication: Application() {
companion object {
var instance by Delegates.notNull<DemoApplication>()
}
val floatWindowManager by lazy { FloatWindowManager() }
override fun onCreate() {
super.onCreate()
instance = this
}
}
悬浮窗管理器
FloatWindowManager.kt
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.view.*
import android.widget.Toast
import org.atoto.autotest.MyApplication
class FloatWindowManager private constructor() {
companion object {
val instance by lazy { FloatWindowManager() }
}
private var statusHeight = 0
get() {
if (field == 0) {
field = getStatusBarHeight()
}
return field
}
private var wm: WindowManager? = null
private var wmParams: WindowManager.LayoutParams? = null
private var mTouchStartX = 0f
private var mTouchStartY = 0f
private var mX = 0f
private var mY = 0f
private var mView:View ?= null
init {
initParams()
}
private fun initParams() {
wm = MyApplication.instance.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
// 设置LayoutParams(全局变量)相关参数
wmParams = WindowManager.LayoutParams()
wmParams?.apply {
type = if (Build.VERSION.SDK_INT >= 26) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
flags = this.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
gravity = Gravity.START or Gravity.TOP // 调整悬浮窗口至左上角
// 以屏幕左上角为原点,设置x、y初始值
x = 0
y = 0
// 设置悬浮窗口长宽数据
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
format = PixelFormat.RGBA_8888
}
}
@SuppressLint("ClickableViewAccessibility")
fun addView(view:View):Boolean {
if (mView?.isAttachedToWindow == true) return false
mView = view
mView?.setOnTouchListener(mOnTouchListener)
kotlin.runCatching {
wm?.addView(view, wmParams)
return true
}.onFailure {
if (it is IllegalStateException) {
toast("请先退出后打开")
} else {
toast("请先打开悬浮窗权限")
}
}
return false
}
@SuppressLint("ClickableViewAccessibility")
fun removeView(view:View) {
kotlin.runCatching {
mView = view
mView?.setOnTouchListener(null)
wm?.removeView(mView)
}
}
private fun updateViewPosition() {
// 更新浮动窗口位置参数
wmParams?.apply {
x = (mX - mTouchStartX).toInt()
y = (mY - mTouchStartY).toInt()
}
mView?.let { mView->
wm?.updateViewLayout(mView, wmParams)
}
}
private fun getStatusBarHeight(): Int {
var result = 0
val resourceId: Int = MyApplication.instance.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = MyApplication.instance.resources.getDimensionPixelSize(resourceId)
}
return result
}
private var mGestureDetector = GestureDetector(MyApplication.instance, object :
GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
return super.onDoubleTap(e)
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
return super.onSingleTapUp(e)
}
})
@SuppressLint("ClickableViewAccessibility")
private val mOnTouchListener = View.OnTouchListener { view, event ->
mGestureDetector.onTouchEvent(event)
// 获取相对屏幕的坐标,即以屏幕左上角为原点
when(event.action) {
MotionEvent.ACTION_DOWN -> {
// 获取相对View的坐标,即以此View左上角为原点
mTouchStartX = event.x
mTouchStartY = event.y
}
MotionEvent.ACTION_MOVE -> {
mX = event.rawX
mY = event.rawY - statusHeight // 系统状态栏的高度
updateViewPosition()
}
MotionEvent.ACTION_UP -> {
updateViewPosition()
mTouchStartX = 0F
mTouchStartY = 0F
}
}
true
}
private fun toast(msg:String) {
Toast.makeText(MyApplication.instance, msg, Toast.LENGTH_SHORT).show()
}
}