android 悬浮窗口和主界面同时显示,Android 悬浮窗口(及解决6.0以上无法显示问题)...

思路实现

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

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

大致介绍WindowManager 类

创建的对象:

Context.getSystemService(Context.WINDOW_SERVICE)

常用API:

addView():添加一个View对象

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

removeView():移除一个View对象

使用Kotlin编程,实战开发

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

android:orientation="vertical"

android:id="@+id/suspension_window_layout"

android:layout_width="wrap_content"

android:layout_height="wrap_content">

android:text="系统悬浮弹窗"

android:textColor="@android:color/white"

android:textSize="18sp"

android:padding="10dp"

android:background="@color/colorPrimary"

android:layout_height="wrap_content" />

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中添加系统弹窗权限:

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

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

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

819f1601c4922aa0dedfbaedbf992ac6.png

录制效果如下:

ce9b7db09ff2b9892a9200c7a4fa5706.gif

授权 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运行项目,效果如下

c8639f137ead14e9c7c4e2b4e27ccb20.gif

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

资源参考:

郭大大的教程:http://blog..net/guolin_blog/article/details/8689140/