一.使用悬浮窗功能的原因:
二.悬浮窗的具体实现步骤:
一.添加权限:
二.动态申请权限:
三.WindowManager.LayoutParams的TYPE类型:
四.编写代码:
1.XML代码:
2.Activity相关代码:
3.WindowManager相关代码
在Android开发中我们想要做到提醒用户关键信息的作用的时候.例如App更新信息之类的,有很多种方式去实现,主要的话还是三种方式Dialog,AlertDialog,PopupWindow,但是他们都有一个共同的缺点那就是依赖于Activity,而悬浮窗是不依赖Activity的,甚至,App在后台运行,悬浮窗依旧会弹出来,只要App进程不被杀死,但是悬浮窗也有缺点,那就是权限问题
<!-- 悬浮窗需要添加该权限-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
private fun initFloatWindow() {
// 权限判断
if (!Settings.canDrawOverlays(applicationContext)) {
// 启动Activity让用户授权
val mIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${packageName}"))
startActivityForResult(mIntent, 10)
} else {
// 已经有权限了,就去初始化对应的视图或者悬浮窗弹窗的初始化
initView()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == 10) {
if (Settings.canDrawOverlays(applicationContext)) {
initView()
} else {
ToastUtils.showToast(this, "请设置对应权限")
}
}
}
这段代码是用于在Activity中动态申请悬浮窗权限的,上面那个 ToastUtils是我自己封装的吐司类,这个很简单,相信大家都会,最终我们进入对应的Activity当中我们会跳转到这样一个界面来获取该应用的悬浮窗权限,像电视或者车载开发之类的对于权限已经做了静默处理了,总不能我们在使用车载导航的时候,弹出一个是否开启定位吧哈哈
为什么要将这个TYPE类型放在第三位呢,因为这个就是导致各种问题出现的罪魁祸首
type = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 -> WindowManager.LayoutParams.TYPE_PHONE
else -> WindowManager.LayoutParams.TYPE_TOAST
}
我举一个例子,如下:
这是因为我的手机为Android7我用的是被淘汰的TYPE类型,这个情况下我们需要更换TYPE类型就能解决对应的问题
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:background="#f46d43"
android:paddingVertical="10dp"
android:gravity="center"
android:textSize="28sp"
android:textColor="#ffffff"
android:text="WindowManager与悬浮框"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:textAllCaps="false"
android:id="@+id/activityshow_btn"
android:text="Activity显示悬浮框"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:textAllCaps="false"
android:id="@+id/serviceshow_btn"
android:text="Service显示悬浮框"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:textAllCaps="false"
android:id="@+id/activityshow_btn_cancle"
android:text="Activity隐藏悬浮框"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
/**
* 悬浮窗: 不依赖Activity, 当Activity销毁的时候悬浮窗仍然可以显示,注意悬浮窗需要手动获取权限以及声明权限
*/
class FloatingWindowActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var mActivityFloatingWindow: ActivityFloatingWindow
private val mServiceshowBtn by lazy { findViewById<Button>(R.id.serviceshow_btn) }
private val mActivityshowBtn by lazy { findViewById<Button>(R.id.activityshow_btn) }
private val m_activityshow_btn_cancle by lazy { findViewById<Button>(R.id.activityshow_btn_cancle) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_floating_window)
initFloatWindow()
initListener()
}
private fun initFloatWindow() {
// 权限判断
if (!Settings.canDrawOverlays(applicationContext)) {
// 启动Activity让用户授权
val mIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${packageName}"))
startActivityForResult(mIntent, 10)
} else {
// 已经有权限了
initView()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == 10) {
if (Settings.canDrawOverlays(applicationContext)) {
initView()
} else {
ToastUtils.showToast(this, "请设置对应权限")
}
}
}
private fun initView() {
mActivityFloatingWindow = ActivityFloatingWindow(this)
}
private fun initListener() {
mServiceshowBtn.setOnClickListener(this)
mActivityshowBtn.setOnClickListener(this)
m_activityshow_btn_cancle.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v?.id) {
R.id.serviceshow_btn -> {
}
R.id.activityshow_btn -> {
activityShowFloatingWindow()
}
R.id.activityshow_btn_cancle -> {
mActivityFloatingWindow.remove()
}
}
}
private fun activityShowFloatingWindow() {
mActivityFloatingWindow.showFloatWindow()
}
}
class ActivityFloatingWindow(context: Context) : View.OnTouchListener {
private var mContext: Context
private lateinit var mWindowParams: WindowManager.LayoutParams
private lateinit var mWindowManager: WindowManager
private lateinit var rootLayout: View
init {
mContext = context
initFloatWindow()
}
private var mInViewX = 0f
private var mInViewY = 0f
private var mDownInScreenX = 0f
private var mDownInScreenY = 0f
private var mInScreenX = 0f
private var mInScreenY = 0f
var isMoving = false
/**
* 初始化布局
*/
private fun initFloatWindow() {
rootLayout = LayoutInflater.from(mContext)
.inflate(R.layout.floatingwidow_in_activity, null)
rootLayout.setOnTouchListener(this)
mWindowParams = WindowManager.LayoutParams()
mWindowManager = MyApp.mApplicationContext?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE
mWindowParams.format = PixelFormat.RGBA_8888
// 设置悬浮窗不获取焦点的原因就是为了传递事件
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
mWindowParams.gravity = Gravity.START or Gravity.TOP
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT
mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT
defaultPosition()
}
fun showFloatWindow() {
if (null == rootLayout.parent) {
mWindowManager.addView(rootLayout, mWindowParams)
}
}
fun remove() {
if (null != rootLayout.parent) {
mWindowManager.removeView(rootLayout)
}
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return floatLayoutTouch(event!!)
}
private fun floatLayoutTouch(motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
// 获取相对View的坐标,即以此View左上角为原点
mInViewX = motionEvent.x
mInViewY = motionEvent.y
// 获取相对屏幕的坐标,即以屏幕左上角为原点
mDownInScreenX = motionEvent.rawX
mDownInScreenY = motionEvent.rawY
mInScreenX = motionEvent.rawX
mInScreenY = motionEvent.rawY
isMoving = true
}
MotionEvent.ACTION_MOVE -> {
// 更新浮动窗口位置参数
mInScreenX = motionEvent.rawX
mInScreenY = motionEvent.rawY
mWindowParams.x = (mInScreenX - mInViewX).toInt()
mWindowParams.y = (mInScreenY - mInViewY).toInt()
// 手指移动的时候更新小悬浮窗的位置
mWindowManager.updateViewLayout(rootLayout, mWindowParams)
isMoving = true
}
MotionEvent.ACTION_UP -> {
isMoving = false
// 如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件。
if (mDownInScreenX == mInScreenX && mDownInScreenY == mInScreenY) {
Toast.makeText(mContext, "Click", Toast.LENGTH_SHORT).show()
}
}
}
return true
}
private fun defaultPosition() {
val metrics = DisplayMetrics()
// 默认固定位置,靠屏幕左边缘的中间
mWindowManager.defaultDisplay.getMetrics(metrics)
mWindowParams.x = 0
mWindowParams.y = metrics.heightPixels/2
}
}
!!!这里的WIndowManager相关的代码我推荐大家使用单例,我这里没有使用单例,运行效果如下: