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

思路实现

通过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

资源参考

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android悬浮窗(Floating Window)可以让应用在后台运行时,在屏幕上显示出一个可移动的窗口,用户可以在窗口中执行相应的操作。以下是一个简单的实现示例: 1. 在AndroidManifest.xml文件中添加悬浮窗权限 ``` <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> ``` 2. 新建一个Service类,并在onCreate()方法中创建悬浮窗 ``` public class FloatingWindowService extends Service { private WindowManager windowManager; private View floatingView; @Override public void onCreate() { super.onCreate(); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); // 创建悬浮窗View floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null); // 设置悬浮窗参数 WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); // 设置悬浮窗初始位置 params.gravity = Gravity.CENTER | Gravity.START; params.x = 0; params.y = 0; // 将悬浮窗添加到WindowManager windowManager.addView(floatingView, params); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { super.onDestroy(); // 移除悬浮窗 if (floatingView != null) { windowManager.removeView(floatingView); } } } ``` 3. 在悬浮窗布局文件(floating_window.xml)中添加需要显示的内容 ``` <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/floating_window" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent"> <!-- 添加需要显示的内容 --> </RelativeLayout> ``` 4. 在Activity中启动Service ``` Intent intent = new Intent(this, FloatingWindowService.class); startService(intent); ``` 这样就可以实现一个简单的Android悬浮窗了。需要注意的是,悬浮窗权限在Android 6.0以上的系统中需要动态申请,具体实现可以参考Android官方文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值