android 悬浮调试窗口

322 阅读2分钟

预览效果

screenshot 00_00_00-00_00_30.gif

需要申请的权限

 <!-- 用于调试模式 -->
<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()
    }
}