您的位置:首页 > 移动开发 > Android开发

Android 悬浮窗口(及解决6.0以上无法显示问题)

2017-07-26 11:23 459 查看
思路实现

通过WindowManager添加一个View,创建一个系统顶级的窗口,实现悬浮窗口的效果。

本篇思路,来源于郭霖大神的悬浮窗口教程

大致介绍WindowManager 类

创建的对象

Context.getSystemService(Context.WINDOW_SERVICE)


常用API:

addView():添加一个View对象

updateViewLayout():更新指定的View对象

removeView():移除一个View对象

使用Kotlin编程,实战开发

1. 编写弹窗中布局文件,item_message.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="@+id/suspension_window_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:text="系统悬浮弹窗"
android:textColor="@android:color/white"
android:textSize="18sp"
android:padding="10dp"
android:background="@color/colorPrimary"
android:layout_height="wrap_content" />
</LinearLayout>


2. 编写悬浮弹窗的View:

定义一个布局,将对应的item_message.xml绑定上,重写onTouche()悬浮弹窗,实现自动拖动,点击关闭的效果。

class SuspensionWindowLayout(context: Context) : RelativeLayout(context) {

/**
* statusbar系统状态栏的高度
*/
var statusbarHeight = 0
/**
* 窗口管理器
*/
val windowManager: WindowManager

init {
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
var childView= View.inflate(context, R.layout.item_message,this)
widget_width = childView.suspension_window_layout.layoutParams.width
widget_height = childView.suspension_window_layout.layoutParams.height
}

/**
*    按下屏幕时手指在x,y轴上的坐标
*/
var down_x = 0.0f
var down_y = down_x
/**
*    移动时候的手指在x,y轴上的坐标
*/
var move_x = down_x
var move_y = down_x
/**
* 按下屏幕时候,控件在x,y轴位置
*/
var widget_x = down_x
var widget_y = down_x

/**
* 重写处理拖动事件
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度
widget_x = event.x
widget_y = event.x
//没有移动,down->up,点击事件
down_x = event.rawX
down_y = event.rawY - getStatusBarHeight()
move_x = event.rawX
move_y = event.rawY - getStatusBarHeight()
}
MotionEvent.ACTION_MOVE -> {
// 手指移动的时候更新小悬浮窗的位置
move_x = event.rawX
move_y = event.rawY - getStatusBarHeight()
updateWidgetPostion()
}
MotionEvent.ACTION_UP -> {//
//坐标没有改变,是点击动作
if (move_x == down_x && move_y == down_y) {
SuspensionWindowManagerUtils.removeSuspensionWindow(context)
}
}
else -> {
}
}
return true
}

/**
* 更新控件位置,在x,y轴的的位置
*/
fun updateWidgetPostion() {
var layoutParams = SuspensionWindowManagerUtils.getWidgetLayoutParams()
layoutParams!!.x = (move_x - widget_x).toInt()
layoutParams!!.y = (move_y - widget_y).toInt()
windowManager.updateViewLayout(this, layoutParams)
}

/**
* 获取系统状态栏,返回状态栏高度的像素值
*/
fun getStatusBarHeight(): Int {
if (statusbarHeight == 0) {
statusbarHeight = resources.getDimensionPixelSize(ViewUtils.getStatusBarHeight())
}
return statusbarHeight
}

companion object {
var widget_width = 0
var widget_height = 0
}
}


一个工具类

public class ViewUtils {
/**
*  反射获取状态栏高度
* @return
*/
public static int getStatusBarHeight(){
int x=0;
try {
Class<?> c = Class.forName("com.android.internal.R$dimen");
Object o = c.newInstance();
Field field = c.getField("status_bar_height");
x = (Integer) field.get(o);
} catch (Exception e) {
e.printStackTrace();
}
return  x;
}
}


3. 编写WindowManager工具类

一些列,检查弹窗,开启弹窗,关闭弹窗的操作封装到该类中。

class SuspensionWindowManagerUtils {
companion object {
var windowManager: WindowManager?=null
var layoutParams: WindowManager.LayoutParams?=null
var suspensionWindowWidget: SuspensionWindowLayout? = null
/**
* 创建悬浮窗口
*/
@JvmStatic
fun createSuspensionWindow(context: Context) {
if (suspensionWindowWidget==null){
suspensionWindowWidget= SuspensionWindowLayout(context)
}
getWindowManager(context)!!.addView(suspensionWindowWidget, getWidgetLayoutParams())
}
/**
* 移除悬浮窗口
*/
fun removeSuspensionWindow(context: Context) {
if (suspensionWindowWidget != null) {
getWindowManager(context)!!.removeView(suspensionWindowWidget)
suspensionWindowWidget = null
}
}
/**
* 悬浮窗口是否已经打开
*/
fun windowIsOpen():Boolean{
if (suspensionWindowWidget!=null)
return true
else  return false
}
/**
* 获取悬浮窗口的布局参数
*/
fun getWidgetLayoutParams(): WindowManager.LayoutParams? {
if (layoutParams == null) {
layoutParams = WindowManager.LayoutParams()
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!!.gravity = Gravity.LEFT or Gravity.TOP
layoutParams!!.x = windowManager!!.defaultDisplay.width
layoutParams!!.y =0
layoutParams!!.width = SuspensionWindowLayout.widget_width
layoutParams!!.height = SuspensionWindowLayout.widget_height
}
return layoutParams
}

/**
* 获取窗口管理器
*/
fun getWindowManager(context: Context): WindowManager ?{
if (windowManager == null) {
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
return windowManager
}
}
}


4. 开启一个悬浮窗口

先进行判断,若是悬浮弹窗为未开启,则进行开启。

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//点击开启悬浮窗口
main_open_window.setOnClickListener {
requestPermission()
}
}

/**
* 开启悬浮弹窗
*/
fun openSuspensionWindow(){
//未开启窗口,则开启
if (!SuspensionWindowManagerUtils.windowIsOpen()) {
SuspensionWindowManagerUtils.createSuspensionWindow(applicationContext)
}
}
}


5. 在AndroidManifest.xml中添加系统弹窗权限

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


6. Android 5.1及其以下系统,运行效果:

在模拟器上运行无问题,但是在红米手机上出现问题。

红米手机需要先开启悬浮权限:显示悬浮窗–>允许。



录制效果如下:



授权 SYSTEM_ALERT_WINDOW Permission 在 android 6.0 及其以上版本的系统

运行设备

AndroidStudio 自带的模拟器,其API 24。

运行结果

在输出台上提示以下错误:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window
android.view.ViewRootImpl$W@b9261f -- permission denied for window type 2002
at android.view.ViewRootImpl.setView(ViewRootImpl.java:702)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)


查看SYSTEM_ALERT_WINDOW权限,可知

若是运用程序的目标API在23及其以上,程序需要通过权限管理界面,开启授权。程序发送ACTION_MANAGE_OVERLAY_PERMISSION的动作,使用Settings.canDrawOverlays()来检查是否授权。

解决方式:

1. 检查权限

使用
Settings.canDrawOverlays()
来检查是否授权。

/**
* 当目标版本大于23时候,检查权限
*/
fun checkPermission():Boolean{
if (Build.VERSION.SDK_INT>=23)
return Settings.canDrawOverlays(this)
else
return true
}


2. 用户授权

发送
ACTION_MANAGE_OVERLAY_PERMISSION
,开启权限授权界面,用户授权,允许悬浮在运用程序之上。

/**
* 申请权限的状态code
*/
var request_code=1
/**
* 开启权限管理界面,授权。
*/
fun requestPermission(){
var intent=Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
startActivityForResult(intent,request_code)
}


3. 响应授权结果

使用
Settings.canDrawOverlays()
来检查授权结果,用户在管理界面是否授权。

/**
* 回调申请结果
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
request_code -> {
if (checkPermission()) {  //用户授权成功
openSuspensionWindow()
} else { //用户拒绝授权
Toast.makeText(application, "弹窗权限被拒绝", Toast.LENGTH_SHORT).show()
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}


4. 在Android6.0及其以上,运行效果

在7.0系统模拟器上,API24运行项目,效果如下



项目代码https://github.com/13767004362/SuspensionWindowDemo

资源参考

郭大大的教程:http://blog.csdn.net/guolin_blog/article/details/8689140/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐